11

移動のセマンティクスとコピー/移動の省略を理解しようとしています。

いくつかのデータをラップするクラスが欲しいです。コンストラクターでデータを渡したいのですが、データを所有したいと思います。

thisthis、およびthisを読んだ後、C++ 11 でコピーを保存する場合、値渡しは少なくとも他のオプションと同じくらい効率的である必要があるという印象を受けました (コードサイズの増加という小さな問題は別として) )。

次に、呼び出し元のコードがコピーを回避したい場合は、左辺値の代わりに右辺値を渡すことでできます。(例: std::move を使用)

だから私はそれを試しました:

#include <iostream>

struct Data {
  Data()                 { std::cout << "  constructor\n";}
  Data(const Data& data) { std::cout << "  copy constructor\n";} 
  Data(Data&& data)      { std::cout << "  move constructor\n";}
};

struct DataWrapperWithMove {
  Data data_;
  DataWrapperWithMove(Data&& data) : data_(std::move(data)) { }
};

struct DataWrapperByValue {
  Data data_;
  DataWrapperByValue(Data data) : data_(std::move(data)) { }
};

Data
function_returning_data() {
  Data d;
  return d;
}

int main() {
  std::cout << "1. DataWrapperWithMove:\n"; 
  Data d1;
  DataWrapperWithMove a1(std::move(d1));

  std::cout << "2. DataWrapperByValue:\n";  
  Data d2;
  DataWrapperByValue a2(std::move(d2));

  std::cout << "3. RVO:\n";
  DataWrapperByValue a3(function_returning_data());
}

出力:

1. DataWrapperWithMove:
  constructor
  move constructor
2. DataWrapperByValue:
  constructor
  move constructor
  move constructor
3. RVO:
  constructor
  move constructor

これらのケースのいずれでもコピー コンストラクターが呼び出されないことを嬉しく思いますが、2 番目のケースで呼び出される余分な移動コンストラクターがあるのはなぜですか? まともな移動コンストラクターはDataかなり速いはずだと思いますが、それでも私はくすくす笑っています。代わりに右辺値参照渡し (最初のオプション) を使用したくなります。これにより、移動コンストラクターの呼び出しが 1 つ少なくなるように思われますが、可能であれば、値渡しとコピー省略を採用したいと考えています。

4

3 に答える 3

4

DataWrapperByValue::data_から に移動されるDataWrapperByValue::DataWrapperByValue(Data data)s 引数からdata 移動されd2ます。

左辺値を取得する場合の値バージョンと一緒に右辺値参照で渡すという結論は、最高のパフォーマンスをもたらします。ただし、これは時期尚早の最適化であると広く考えられています。Howard Hinnant ( C++11 で STL コンテナーを保持するクラスのコンストラクターを作成する最良の方法) および Sean Parent ( http://channel9.msdn.com/Events/GoingNative/2013/Inheritance-Is-The-Base-悪のクラス) は両方とも、この時期尚早な最適化を検討していることに注目しています。その理由は、移動は非常に安価であると想定されており、この場合に移動を回避するとコードの重複が発生するためです。特に、r または l 値のいずれかになる引数が複数ある場合はそうです。プロファイリングまたはテストによって、これが実際にパフォーマンスを低下させることがわかった場合は、事後にいつでも簡単に右辺値参照による渡しを追加できます。

追加のパフォーマンスが必要な場合に役立つパターンは次のとおりです。

struct DataWrapperByMoveOrCopy {
  Data data_;
  template<typename T, 
    typename = typename std::enable_if<    //SFINAE check to make sure of correct type
        std::is_same<typename std::decay<T>::type, Data>::value
    >::type
  >
  DataWrapperByMoveOrCopy(T&& data) : data_{ std::forward<T>(data) } { }
};

ここでは、私のライブの例で見られるように、コンストラクターは常に正しいことを行います: http://ideone.com/UsltRA

この議論の余地のある複雑なコードの利点は、おそらく単一の引数には関係ありませんが、コンストラクターに r または l 値である可能性のある 4 つの引数がある場合、これは 16 の異なるコンストラクターを記述するよりもはるかに優れています。

struct CompositeWrapperByMoveOrCopy {
  Data data_;
  Foo foo_;
  Bar bar_;
  Baz baz_;
  template<typename T, typename U, typename V, typename W, 
    typename = typename std::enable_if<
        std::is_same<typename std::decay<T>::type, Data>::value &&
        std::is_same<typename std::decay<U>::type, Foo>::value &&
        std::is_same<typename std::decay<V>::type, Bar>::value &&
        std::is_same<typename std::decay<W>::type, Baz>::value
    >::type
  >
  CompositeWrapperByMoveOrCopy(T&& data, U&& foo, V&& bar, W&& baz) : 
  data_{ std::forward<T>(data) },
  foo_{ std::forward<U>(foo) },
  bar_{ std::forward<V>(bar) },
  baz_{ std::forward<W>(baz) } { }
};

SFINAE チェックは省略できますが、明示的なコンストラクターを使用して暗黙的に変換するなどの微妙な問題が発生する可能性があることに注意してください。また、引数の型をチェックせずに、異なるアクセス権、異なる ADL などがあるコンストラクター内で変換が延期されます。実際の例を参照してください: http://ideone.com/yb4e3Z

于 2014-03-15T16:50:46.533 に答える
3

DataWrapperByValueこのコンストラクタがあります:

DataWrapperByValue(Data data);

引数を値で受け取ります。つまり、左辺値か右辺値かに応じて、dataパラメーターのコピーまたはムーブ コンストラクターが呼び出されます。特に、左辺値の場合はコピーされます。右辺値の場合、移動されます。

を介して右辺値を渡しているためstd::move(d2)、move コンストラクターが呼び出さd2れてパラメーターに移動します。2 番目のムーブ コンストラクターの呼び出しは、もちろんdata_データ メンバーの初期化によって行われます。

残念ながら、ここではコピー省略は発生しません。移動にコストがかかり、それらを制限したい場合は、完全な転送を許可して、少なくとも1 つの移動または 1 つのコピーが存在するようにすることができます。

template<class U>
DataWrapperByValue(U&& u) : data_(std::forward<U>(u)) { }
于 2014-03-15T16:55:51.957 に答える
0

本質的にこのコードを実行しているためだと思います。

std::cout << "2. DataWrapperByValue:\n";  
Data d2;
DataWrapperByValue a2(Data(std::move(d2))); // Notice a Data object is constructed. 

DataWrapperByValue には、左辺値を受け入れるコンストラクターしかないことに注意してください。std::move(d2) を実行すると、右辺値が渡されるため、別の Data オブジェクトが作成されて DataWrapperByValue コンストラクターに渡されます。これは、Data(Data&&) コンストラクターを使用して作成されます。次に、DataWrapperByValue のコンストラクター中に 2 番目の移動コンストラクターが呼び出されます。

于 2014-03-15T16:11:52.890 に答える