40

したがって、移動セマンティクスを調べた後、所有権を譲渡する場合は、一般的なコンセンサスが値を渡すことであることがわかります。しかし、UniversalReferencesに関するScottMeyerの講演で、std::vector::push_back 2つのオーバーロードがあることに気づきました。

void push_back( const T& value );
void push_back( T&& value );

だから私は自分自身に思いました、void push_back( T value );十分ではないでしょうか?私は最終的に次のテストケースにつながる数人に尋ねました:

#include <memory>
#include <iostream>
#include <type_traits>

struct A
{
    A() { std::cout << "A Default constructor\n"; }
    A(const A &) { std::cout << "A Copy\n"; }
    A(A &&) { std::cout << "A Move\n"; }
};

std::aligned_storage<sizeof(A)> contents;
A& alias = *reinterpret_cast<A*>(&contents);

void ByVal(A a)
{
    new (&contents) A(std::move(a));
    alias.~A();
}

void ByLCRef(A const& a)
{
    new (&contents) A(a);
    alias.~A();
}

void ByRRef(A&& a)
{
    new (&contents) A(std::move(a));
    alias.~A();
}

int main()
{
    A a;
    std::cout << "\n";
    std::cout << "ByVal(a);\n";
    ByVal(a);
    std::cout << "ByVal(std::move(a));\n";
    ByVal(std::move(a));
    std::cout << "ByVal(A());\n";
    ByVal(A());
    std::cout << "ByLCRef(a);\n";
    ByLCRef(a);
    std::cout << "ByRRef(std::move(a));\n";
    ByRRef(std::move(a));
    std::cout << "ByRRef(A());\n";
    ByRRef(A());
}

これにより、次のようになります。

A Default constructor

ByVal(a);
A Copy
A Move
ByVal(std::move(a));
A Move
A Move
ByVal(A());
A Default constructor
A Move
ByLCRef(a);
A Copy
ByRRef(std::move(a));
A Move
ByRRef(A());
A Default constructor
A Move

ご覧のとおりByVal、参照オーバーロードのペアと比較して、1つの余分な動きが生成されます。だから問題は:それはそれだけの価値があるのか​​?1つの単純な値渡し関数ではなく、2つのオーバーロードを作成するのはいつですか。

4

3 に答える 3

23

ご覧のとおり、ByValは、参照オーバーロードのペアと比較して1つの余分な動きを生成します。だから問題は:それはそれだけの価値があるのか​​?1つの単純な値渡し関数ではなく、2つのオーバーロードを作成するのはいつですか。

+1この質問をするほとんどの人は、わざわざ分析をしません。だからあなたはあなた自身の宿題をするための私の賛成を得ます。:-)

それが価値があるかどうかは、moveコンストラクターのコストと、関数が取る引数の数に依存します。極端な例として、moveコンストラクターがそれほど高速でない場合は、それらを削除することに多くの注意を払うことができます(const&、&&オーバーロードソリューションを優先します)。もう一方の極端な例として、関数に4つのパラメーターがあり、それぞれが左辺値/右辺値の処理を必要とする場合、すべてのケースをカバーするために16のオーバーロードを記述したくない場合があります。これは維持する必要のあるコードが多く、固有のコードの複雑さはバグの誘因です。したがって、値によるアプローチはより魅力的に見えます(オーバーロードは必要ありません)。

ですから、私見ですが、「それだけの価値があるか」という質問に対する一般的な答えはありません。最善の答えは、すでに行ったように、各ソリューションのコストに関する知識を身に付け、ケースバイケースで工学的な判断を下すことです。

アップデート

imhoの場合vector<T>::push_back、const&、&&オーバーロードソリューションはそれだけの価値があります。パラメータは1つだけであり、moveコンストラクタがどれほど高価かはわかりません。確かに、moveコンストラクターがあるかどうかさえわかりません。後者の場合をテストするために実験を変更する(moveコンストラクターを削除する):

ByVal(a);
A Copy
A Copy

ByLCRef(a);
A Copy

あなたはあなたをにコピーするために1つまたは2つのコピーを支払いたいAですvectorか?

つまり、パラメータについての知識が少ないほど、特に。のように頻繁に使用されるものを作成している場合は、パフォーマンスの側面に傾倒する必要がありますstd::vector

于 2013-01-06T21:07:02.703 に答える
5

重要な点の1つは、値の受け渡しとオーバーロードを切り替えるときにクライアントコードを変更する必要がないことです。つまり、実際にはパフォーマンスとメンテナンスの違いになります。また、通常はメンテナンスの方が好まれるので、次の経験則を考え出しました。

次の場合を除いて、値を渡し
ます。1.ムーブコンストラクタまたはムーブ代入は簡単ではありません。
2.オブジェクトはコピー可能ですが、移動できません。
3.テンプレートライブラリを作成していて、オブジェクトのタイプがわかりません。
4.オブジェクトに些細な移動コンストラクターと割り当てがあるにもかかわらず、プロファイラーは、プログラムが移動内で多くの時間を費やしていることを示しています。

于 2013-01-07T02:30:27.627 に答える
5

移動可能でコピー可能なクラスの保存

あなたがこのクラスを持っていると想像してください:

class Data {
 public:
  Data() { }
  Data(const Data& data)            { std::cout << "  copy constructor\n";} 
  Data(Data&& data)                 { std::cout << "  move constructor\n";}
  Data& operator=(const Data& data) { std::cout << "  copy assignment\n"; return *this;}
  Data& operator=(Data&& data)      { std::cout << "  move assignment\n"; return *this;}  
};

優れたC++11コンパイラは、これらすべての関数を定義する必要があります( Visual Studioの一部の古いバージョンでは定義されていません)が、ここではデバッグ出力用に定義しています。

さて、これらのクラスの1つを格納するクラスを作成したい場合は、次のように値渡しを使用できます。

class DataStore {
  Data data_;
 public: 
  void setData(Data data) { data_ = std::move(data); }
};

C ++ 11の移動セマンティクスを利用して、値を目的の場所に移動しています。DataStore次に、次のように使用できます。

  Data d;   
  DataStore ds;
  
  std::cout << "DataStore test:\n";
  ds.setData(d);
  
  std::cout << "DataStore test with rvalue:\n";
  ds.setData(Data{});
  
  Data d2;
  std::cout << "DataStore test with move:\n";
  ds.setData(std::move(d2));

次の出力があります。

DataStore test:
  copy constructor
  move assignment
DataStore test with rvalue:
  move assignment
DataStore test with move:
  move constructor
  move assignment

どちらでも構いません。私は最後のテストで2つの動きを持っていますが、これは最適ではないかもしれませんが、動きは通常安価なので、それと一緒に暮らすことができます。それをより最適にするために、setData後で行う関数をオーバーロードする必要がありますが、それはおそらく現時点では時期尚早の最適化です。

動かせないクラスを保存する

しかし、ここで、コピー可能であるが移動できないクラスがあると想像してください。

class UnmovableData {
 public:
  UnmovableData() { }
  UnmovableData(const UnmovableData& data) { std::cout << "  copy constructor\n";}
  UnmovableData& operator=(const UnmovableData& data) { std::cout << "  copy assignment\n"; return *this;}  
};

C ++ 11より前は、すべてのクラスが移動できなかったため、今日、多くのクラスが実際に見つかることを期待してください。これを格納するクラスを作成する必要がある場合、移動セマンティクスを利用できないため、おそらく次のように作成します。

class UnmovableDataStore {
  UnmovableData data_;
 public:
  void setData(const UnmovableData& data) { data_ = data; }
};

そして、constへの参照を渡します。私がそれを使うとき:

  std::cout << "UnmovableDataStore test:\n";
  UnmovableData umd;
  UnmovableDataStore umds;
  umds.setData(umd);

出力を取得します:

UnmovableDataStore test:
  copy assignment

あなたが期待するようにたった1つのコピーで。

コピーできないクラスの保存

また、移動可能であるがコピー不可能なクラスを持つこともできます。

class UncopyableData {
 public:
  UncopyableData() { } 
  UncopyableData(UncopyableData&& data) { std::cout << "  move constructor\n";}
  UncopyableData& operator=(UncopyableData&& data) { std::cout << "  move assignment\n"; return *this;}    
};

std::unique_ptr移動可能であるがコピー不可能なクラスの例です。この場合、私はおそらく次のようにそれを格納するためのクラスを書くでしょう:

class UncopyableDataStore {
  UncopyableData data_;
 public:
  void setData(UncopyableData&& data) { data_ = std::move(data); }
};

ここで、右辺値参照を渡し、次のように使用します。

  std::cout << "UncopyableDataStore test:\n";
  UncopyableData ucd;
  UncopyableDataStore ucds;
  ucds.setData(std::move(ucd));

次の出力で:

UncopyableDataStore test:
  move assignment

そして今、私たちには良い動きが1つしかないことに注意してください。

ジェネリックコンテナ

ただし、STLコンテナーは汎用である必要があり、すべてのタイプのクラスで機能し、可能な限り最適である必要があります。上記のデータストアの一般的な実装が本当に必要な場合は、次のようになります。

template<class D>
class GenericDataStore {
  D data_;
 public:
  void setData(const D& data) { data_ = data; }
  void setData(D&& data) { data_ = std::move(data); }   
};

このようにして、コピー不可能なクラスと移動不可能なクラスのどちらを使用していても、可能な限り最高のパフォーマンスが得られますが、setDataメソッドのオーバーロードが少なくとも2つ必要であり、重複コードが発生する可能性があります。使用法:

  std::cout << "GenericDataStore<Data> test:\n";
  Data d3;
  GenericDataStore<Data> gds;
  gds.setData(d3);
  
  std::cout << "GenericDataStore<UnmovableData> test:\n";
  UnmovableData umd2;
  GenericDataStore<UnmovableData> gds3;
  gds3.setData(umd2); 
  
  std::cout << "GenericDataStore<UncopyableData> test:\n";
  UncopyableData ucd2;
  GenericDataStore<UncopyableData> gds2;
  gds2.setData(std::move(ucd2));

出力:

GenericDataStore<Data> test:
  copy assignment
GenericDataStore<UnmovableData> test:
  copy assignment
GenericDataStore<UncopyableData> test:
  move assignment

ライブデモ。お役に立てば幸いです。

于 2014-05-22T13:38:42.773 に答える