12

C++ で小さな数値解析ライブラリをコーディングしています。移動セマンティクスを含む最新の C++11 機能を使用して実装しようとしています。C++11 の右辺値と移動セマンティクスの混乱 (return ステートメント)の議論とトップの回答は理解していますが、まだ頭を抱えているシナリオが 1 つあります。

Tオーバーロードされた演算子を完全に備えたクラスを持っています。また、コピー コンストラクターとムーブ コンストラクターの両方があります。

T (const T &) { /*initialization via copy*/; }
T (T &&) { /*initialization via move*/; }

私のクライアント コードでは演算子を多用しているため、複雑な算術式がムーブ セマンティクスから最大限のメリットを得られるようにしています。次の点を考慮してください。

T a, b, c, d, e;
T f = a + b * c - d / e;

移動セマンティクスがない場合、私のオペレーターは毎回コピー コンストラクターを使用して新しいローカル変数を作成しているため、合計 4 つのコピーがあります。私は、移動セマンティクスを使用して、これを 2 つのコピーといくつかの移動に削減できることを望んでいました。括弧内のバージョン:

T f = a + (b * c) - (d / e);

それぞれがコピーを使用して通常の方法で一時的なものを作成する必要が(b * c)あり(d / e)ますが、これらの一時的なものの 1 つを活用して、移動のみで残りの結果を蓄積できれば素晴らしいと思います。

g++ コンパイラを使用してこれを行うことができましたが、私の手法は安全ではないのではないかと考えており、その理由を完全に理解したいと考えています。

加算演算子の実装例を次に示します。

T operator+ (T const& x) const
{
    T result(*this);
    // logic to perform addition here using result as the target
    return std::move(result);
}
T operator+ (T&& x) const
{
    // logic to perform addition here using x as the target
    return std::move(x);
}

への呼び出しがなければ、各オペレーターstd::moveのバージョンのみが呼び出されます。const &ただし、std::move上記のように使用すると、後続の演算 (最も内側の式の後) は&&各演算子のバージョンを使用して実行されます。

RVO が抑制される可能性があることは知っていますが、非常に計算コストの高い現実世界の問題では、ゲインが RVO の欠如をわずかに上回っているようです。つまり、何百万回もの計算で、 を含めると非常にわずかな速度向上が得られますstd::move。正直なところ、それがなくても十分に高速です。ここでセマンティクスを完全に理解したいだけです。

私の std::move の使用がここで悪いことであるかどうか、またその理由を簡単な方法で説明するために時間を割いてくれる親切な C++ の第一人者はいますか? よろしくお願いします。

4

3 に答える 3

8

完全な型の対称性を得るには、演算子をフリー関数としてオーバーロードすることをお勧めします (左側と右側で同じ変換を適用できます)。これにより、質問から何が欠けているかが少し明らかになります。あなたが提供している無料の機能としてあなたのオペレーターを言い換えてください:

T operator+( T const &, T const & );
T operator+( T const &, T&& );

しかし、左側が一時的であることを処理するバージョンを提供できていません。

T operator+( T&&, T const& );

また、両方の引数が右辺値である場合にコードのあいまいさを避けるために、さらに別のオーバーロードを提供する必要があります。

T operator+( T&&, T&& );

一般的なアドバイスは+=、現在のオブジェクトを変更するメンバー メソッドとして実装しoperator+、インターフェイス内の適切なオブジェクトを変更するフォワーダーとして記述することです。

私はあまり考えたことはありませんが、(r/lvalue 参照なし) を使用する代替手段があるかもしれませんが、すべての状況で効率的にTするために提供する必要があるオーバーロードの数が減らないのではないかと心配しています。operator+

于 2012-10-31T19:55:36.693 に答える
5

他の人が言ったことに基づいて構築するには:

  • std::moveinの呼び出しT::operator+( T const & )は不要であり、RVO を妨げる可能性があります。
  • operator+に委任する非メンバーを提供することが望ましいでしょうT::operator+=( T const & )

operator+また、完全な転送を使用して、必要な非メンバーのオーバーロードの数を減らすことができることも付け加えたいと思います。

template< typename L, typename R >
typename std::enable_if<
  std::is_convertible< L, T >::value &&
  std::is_convertible< R, T >::value,
  T >::type operator+( L && l, R && r )
{
  T result( std::forward< L >( l ) );
  result += r;
  return result;
}

一部の演算子では、この「ユニバーサル」バージョンで十分ですが、通常、加算は交換可能であるため、右側のオペランドが右辺値であることを検出し、左側のオペランドを移動/コピーするのではなく、それを変更したいと考えています。これには、左辺値である右側のオペランドに対して 1 つのバージョンが必要です。

template< typename L, typename R >
typename std::enable_if<
  std::is_convertible< L, T >::value &&
  std::is_convertible< R, T >::value &&
  std::is_lvalue_reference< R&& >::value,
  T >::type operator+( L && l, R && r )
{
  T result( std::forward< L >( l ) );
  result += r;
  return result;
}

また、右辺値である右側のオペランドの場合:

template< typename L, typename R >
typename std::enable_if<
  std::is_convertible< L, T >::value &&
  std::is_convertible< R, T >::value &&
  std::is_rvalue_reference< R&& >::value,
  T >::type operator+( L && l, R && r )
{
  T result( std::move( r ) );
  result += l;
  return result;
}

最後に、 Boris KolpackovSumant Tambe によって提案された手法と、そのアイデアに対するScott Meyers の反応にも興味があるかもしれません。

于 2012-11-02T21:59:47.203 に答える
3

非メンバー関数を使用する方が良い設計であるというDavidRodríguezに同意しoperator+ますが、それは脇に置いて、あなたの質問に焦点を合わせます。

書き込み時にパフォーマンスが低下することに驚いています

T operator+(const T&)
{
  T result(*this);
  return result;
}

それ以外の

T operator+(const T&)
{
  T result(*this);
  return std::move(result);
}

前者の場合、コンパイラはRVOを使用resultして、関数の戻り値のメモリを構築できる必要があるためです。後者の場合、コンパイラはresult関数の戻り値に移動する必要があるため、移動の追加コストが発生します。

一般に、この種のルールは、オブジェクト(つまり、参照ではない)を返す関数があると仮定すると次のようになります。

  • ローカルオブジェクトまたは値によるパラメータを返す場合は、適用std::moveしないでください。これにより、コンパイラはRVOを実行できます。これは、コピーや移動よりも安価です。
  • 右辺値参照型のパラメーターを返す場合は、それに適用std::moveします。これにより、パラメーターが右辺値に変わり、コンパイラーがパラメーターから移動できるようになります。パラメーターを返すだけの場合、コンパイラーは戻り値へのコピーを実行する必要があります。
  • ユニバーサル参照であるパラメーター(つまり、&&右辺値参照または左辺値参照である可能性のある推定型の ""パラメーター)を返す場合は、それに適用std::forwardします。これがないと、コンパイラは戻り値へのコピーを実行する必要があります。これを使用すると、参照が右辺値にバインドされている場合、コンパイラーは移動を実行できます。
于 2012-10-31T22:54:50.123 に答える