9

これは、 + 演算子のオーバーロード (一般に、左のパラメーターに直接再割り当てしない演算子) を使用して移動セマンティクスを利用する方法について、Matthieu M. によって提供されたこの回答に関連しています。

彼は、次の 3 つの異なるオーバーロードを実装することを提案しました。

inline T operator+(T left, T const& right) { left += right; return left; }
inline T operator+(T const& left, T right) { right += left; return right; } // commutative
inline T operator+(T left, T&& right) { left += right; return left; } // disambiguation

番号 1 と 3 は理にかなっていますが、目的 2 が何をするのかわかりません。コメントは可換処理を示唆していますが、1 と 2 は相互に排他的であるようです (つまり、両方の結果を実装するとあいまいになります)。

たとえば、3 つすべてを実装した場合:

T a, b, c;
c = a + b;

コンパイラ出力:

1> エラー C2593: 「演算子 +」があいまいです
1> 'T operator +(const T &,T)' である可能性があります
1> または 'T operator +(T,const T &)'
1> 引数リスト '(T, T)' を照合しようとしている間

1 または 2 のいずれかを削除すると、プログラムは期待どおりに動作します。1 は一般的なケースであり、2 は可換演算子でのみ正しく機能するため、なぜ 2 が使用されるのかわかりません。足りないものはありますか?

4

2 に答える 2

12

私はあなたが何も欠けているとは思わない - あなたの質問のコードは確かに問題です。彼の回答の前半部分は理にかなっていますが、「4 つの望ましいケース」と実際の例の間で何かが失われています。

これはより良いかもしれません:

inline T operator+(T left, T const& right) { left += right; return left; }
inline T operator+(const T& left, T&& right) { right += left; return right; }

これは次のルールを実装します: RHS が期限切れになる場合を除き、LHS のコピーを作成します (移動構築を使用することが望ましい)。

非可換演算子の場合、2 番目のオーバーロードを省略するか、複合代入に委譲しない実装を提供します。

クラスに重いリソースが埋め込まれている場合 (効率的に移動できないため)、値渡しは避けたほうがよいでしょう。ダニエルは、彼の答えでいくつかの良い点を挙げています。T&&しかし、彼が示唆するように戻ってはいけません。

于 2013-04-21T21:40:33.610 に答える
6

この回答に関する重要な更新/警告!

実際に、以下の合理的な現実世界のコードでダングリング参照を静かに作成する説得力のある例があります。追加の一時ファイルが作成されるという犠牲を払っても、他の回答の手法を使用してこの問題を回避してください。今後の参考のために、この回答の残りの部分はそのままにしておきます。


可換ケースの正しいオーバーロードは次のとおりです。

T   operator+( const T& lhs, const T& rhs )
{
  T nrv( lhs );
  nrv += rhs;
  return nrv;
}

T&& operator+( T&& lhs, const T& rhs )
{
  lhs += rhs;
  return std::move( lhs );
}

T&& operator+( const T& lhs, T&& rhs )
{
  rhs += lhs;
  return std::move( rhs );
}

T&& operator+( T&& lhs, T&& rhs )
{
  lhs += std::move( rhs );
  return std::move( lhs );
}

それはなぜですか、どのように機能しますか? まず、右辺値参照をパラメーターとして使用する場合、それを変更して返すことができることに注意してください。それが由来する式は、 を含む完全な式の終了前に右辺値が破棄されないことを保証する必要がありますoperator+。これは、式が完全に評価されて一時変数 (ravlues) が破棄される前に、呼び出し元が (同じ式の一部である)operator+の結果を使用する必要があるため、単純に右辺値参照を返すことができることも意味します。operator+

2 番目の重要な観察結果は、これにより一時的な操作と移動操作がさらに節約されることです。次の式を検討してください。

T a, b, c, d; // initialized somehow...

T r = a + b + c + d;

上記の場合、次と同等です。

T t( a );    // T operator+( const T& lhs, const T& rhs );
t += b;      // ...part of the above...
t += c;      // T&& operator+( T&& lhs, const T& rhs );
t += d;      // T&& operator+( T&& lhs, const T& rhs );
T r( std::move( t ) ); // T&& was returned from the last operator+

これを他のアプローチで何が起こるかを比較してください。

T t1( a );   // T operator+( T lhs, const T& rhs );
t1 += b;     // ...part of the above...
T t2( std::move( t1 ) ); // t1 is an rvalue, so it is moved
t2 += c;
T t3( std::move( t2 ) );
t3 += d;
T r( std::move( t3 );

つまり、まだ 3 つの一時ファイルがあり、それらはコピーされるのではなく移動されますが、上記のアプローチは一時ファイルを完全に回避するのにはるかに効率的です。

のサポートを含む完全なライブラリについては、 df.operatorsnoexceptを参照してください。そこには、非可換ケースと混合型の操作のバージョンもあります。


これをテストするための完全なテスト プログラムを次に示します。

#include <iostream>
#include <utility>

struct A
{
  A() { std::cout << "A::A()" << std::endl; }
  A( const A& ) { std::cout << "A::A(const A&)" << std::endl; }
  A( A&& ) { std::cout << "A::A(A&&)" << std::endl; }
  ~A() { std::cout << "A::~A()" << std::endl; }

  A& operator+=( const A& ) { std::cout << "+=" << std::endl; return *this; }
};

// #define BY_VALUE
#ifdef BY_VALUE
A operator+( A lhs, const A& rhs )
{
  lhs += rhs;
  return lhs;
}
#else
A operator+( const A& lhs, const A& rhs )
{
  A nrv( lhs );
  nrv += rhs;
  return nrv;
}

A&& operator+( A&& lhs, const A& rhs )
{
  lhs += rhs;
  return std::move( lhs );
}
#endif

int main()
{
  A a, b, c, d;
  A r = a + b + c + d;
}
于 2013-04-21T22:41:01.837 に答える