43

一番下のUPDATE

q1:かなり重いリソースを管理するクラスに 5 のルールをどのように実装しますか? その使用法を大幅に簡素化して美しくするために、値によって渡す必要がありますか? それとも、ルールの 5 つの項目すべてが必要ではないでしょうか?

実際には、画像が通常 128*128*128 double である 3D イメージングで何かを始めています。このようなものを書くことができれば、数学はずっと簡単になります:

Data a = MakeData();
Data c = 5 * a + ( 1 + MakeMoreData() ) / 3;

q2: copy elision / RVO / move セマンティクスの組み合わせを使用すると、コンパイラは最小限のコピーでこれを実行できるはずですよね?

これを行う方法を理解しようとしたので、基本から始めました。コピーと割り当てを実装する従来の方法を実装するオブジェクトを想定します。

class AnObject
{
public:
  AnObject( size_t n = 0 ) :
    n( n ),
    a( new int[ n ] )
  {}
  AnObject( const AnObject& rh ) :
    n( rh.n ),
    a( new int[ rh.n ] )
  {
    std::copy( rh.a, rh.a + n, a );
  }
  AnObject& operator = ( AnObject rh )
  {
    swap( *this, rh );
    return *this;
  }
  friend void swap( AnObject& first, AnObject& second )
  {
    std::swap( first.n, second.n );
    std::swap( first.a, second.a );
  }
  ~AnObject()
  {
    delete [] a;
  }
private:
  size_t n;
  int* a;
};

ここで右辺値を入力し、セマンティクスを移動します。私が知る限り、これは実用的な実装になるでしょう:

AnObject( AnObject&& rh ) :
  n( rh.n ),
  a( rh.a )
{
  rh.n = 0;
  rh.a = nullptr;
}

AnObject& operator = ( AnObject&& rh )
{
  n = rh.n;
  a = rh.a;
  rh.n = 0;
  rh.a = nullptr;
  return *this;
}

ただし、コンパイラ (VC++ 2010 SP1) はこれにあまり満足しておらず、コンパイラは通常正しいです。

AnObject make()
{
  return AnObject();
}

int main()
{
  AnObject a;
  a = make(); //error C2593: 'operator =' is ambiguous
}

q3:これを解決するにはどうすればよいですか? AnObject& operator = ( const AnObject& rh ) に戻ると確かに修正されますが、かなり重要な最適化の機会を失っていませんか?

それとは別に、move コンストラクターと代入のコードが重複していることは明らかです。したがって、今のところあいまいさを忘れて、コピーとスワップを使用してこれを解決しようとしますが、今度は右辺値を使用します。ここで説明したように、カスタム スワップは必要なく、代わりに std::swap にすべての作業を任せることができます。これは非常に有望に思えます。そこで、std::swap が move コンストラクターを使用して一時的に構成をコピーし、それを *this と交換することを期待して、次のように書きました。

AnObject& operator = ( AnObject&& rh )
{
  std::swap( *this, rh );
  return *this;
}

しかし、それはうまくいかず、 std::swap が演算子 = ( AnObject&& rh ) を再度呼び出すため、無限再帰によるスタック オーバーフローが発生します。q4:誰かがその例で何を意味するかの例を提供できますか?

これは、2 番目の swap 関数を提供することで解決できます。

AnObject( AnObject&& rh )
{
  swap( *this, std::move( rh ) );
}

AnObject& operator = ( AnObject&& rh )
{
  swap( *this, std::move( rh ) );
  return *this;
}

friend void swap( AnObject& first, AnObject&& second )
{
  first.n = second.n;
  first.a = second.a;
  second.n = 0;
  second.a = nullptr;
}

これでコードの量はほぼ 2 倍になりましたが、その移動部分はかなり安価な移動を許可することで報われます。しかし一方で、通常の代入はもはやコピー省略の恩恵を受けることができません。この時点で、私は本当に混乱しており、何が正しくて何が間違っているのかわからなくなっているので、ここで何らかの情報を得たいと思っています..

更新したがって、2つのキャンプがあるようです。

  • 移動代入演算子をスキップして、C++03 で教えられたこと、つまり、引数を値で渡す単一の代入演算子を作成することを続行するようにというものです。
  • もう1つは、移動代入演算子を実装し(結局のところ、現在はC ++ 11です)、コピー代入演算子に引数を参照渡しさせるように言っています。

(わかりました、そしてベクトルを使用するように私に言っている3番目のキャンプがありますが、それはこの架空のクラスの範囲外です。実際には私はベクトルを使用し、他のメンバーもいるでしょうが、ムーブコンストラクタ/割り当ては自動的に生成されません (まだ?) 質問はまだ保持されます)

残念ながら、このプロジェクトは開始されたばかりであり、データが実際にどのように流れるかはまだわかっていないため、実際のシナリオで両方の実装をテストすることはできません。そのため、単純に両方を実装し、割り当てなどのカウンターを追加して、約 2 回の反復を実行しました。このコードで、T は実装の 1 つです。

template< class T >
T make() { return T( narraySize ); }

template< class T >
void assign( T& r ) { r = make< T >(); }

template< class T >
void Test()
{
  T a;
  T b;
  for( size_t i = 0 ; i < numIter ; ++i )
  {
    assign( a );
    assign( b );
    T d( a );
    T e( b );
    T f( make< T >() );
    T g( make< T >() + make< T >() );
  }
}

このコードは、私が求めているものをテストするのに十分ではないか、コンパイラがあまりにも賢すぎる: arraySize と numIter に何を使用しても、両方の陣営の結果はほとんど同じです: 同じ数の割り当て、タイミングにわずかな変動がありますが、再現可能な有意差はありません。

したがって、誰かがこれをテストするためのより良い方法を指摘できない限り (実際の使用シナリオがまだわかっていないことを考えると)、それは問題ではなく、したがって開発者の好みに任されていると結論付けなければなりません。その場合、私は#2を選びます。

4

6 に答える 6

17

コピー代入演算子の重要な最適化を見逃しています。そしてその後、状況は混乱しました。

  AnObject& operator = ( const AnObject& rh )
  {
    if (this != &rh)
    {
      if (n != rh.n)
      {
         delete [] a;
         n = 0;
         a = new int [ rh.n ];
         n = rh.n;
      }
      std::copy(rh.a, rh.a+n, a);
    }
    return *this;
  }

同じサイズの を割り当てることをまったく考えていない場合を除きAnObject、これははるかに優れています。リサイクルできる資源は、決して捨てないでください。

AnObjectのコピー代入演算子には、強力な例外安全性ではなく、基本的な例外安全性しかないことに不満を言う人もいるかもしれません。ただし、次のことを考慮してください。

クライアントは常に高速代入演算子を使用して、強力な例外安全性を与えることができます。しかし、遅い代入演算子を高速化することはできません。

template <class T>
T&
strong_assign(T& x, T y)
{
    swap(x, y);
    return x;
}

ムーブ コンストラクターは問題ありませんが、ムーブ代入演算子にメモリ リークがあります。そのはず:

  AnObject& operator = ( AnObject&& rh )
  {
    delete [] a;
    n = rh.n;
    a = rh.a;
    rh.n = 0;
    rh.a = nullptr;
    return *this;
  }

...

Data a = MakeData();
Data c = 5 * a + ( 1 + MakeMoreData() ) / 3;

q2: copy elision / RVO / move セマンティクスの組み合わせを使用すると、コンパイラは最小限のコピーでこれを実行できるはずですよね?

右辺値のリソースを利用するには、演算子をオーバーロードする必要がある場合があります。

Data operator+(Data&& x, const Data& y)
{
   // recycle resources in x!
   x += y;
   return std::move(x);
}

最終的には、関心のあるリソースごとに 1 回だけリソースを作成する必要がありDataます。new/delete物を動かすためだけに必要なものはありません。

于 2011-05-12T12:00:38.337 に答える
13

オブジェクトがリソースを大量に消費する場合は、完全にコピーすることを避け、move コンストラクターと move 代入演算子のみを提供することをお勧めします。ただし、本当にコピーも必要な場合は、すべての操作を簡単に提供できます。

コピー操作は理にかなっているように見えますが、移動操作はそうではありません。まず、右辺値参照パラメーターは右辺値にバインドされますが、関数内では左辺値であるため、移動コンストラクターは次のようにする必要があります。

AnObject( AnObject&& rh ) :
  n( std::move(rh.n) ),
  a( std::move(rh.a) )
{
  rh.n = 0;
  rh.a = nullptr;
}

もちろん、ここにあるような基本的なタイプの場合、実際には違いはありませんが、習慣を身につけるのも良いことです。

move-constructor を指定すると、copy-assignment を定義するときに move-assignment 演算子は必要ありません --- パラメータをvalueで受け入れるため、右辺値はコピーではなくパラメータに移動されます。 .

あなたが見つけstd::swap()たように、移動代入演算子内のオブジェクト全体で使用することはできません。これは、移動代入演算子に再帰的に戻るためです。リンク先の投稿のコメントのポイントは、移動操作を使用swapする場合と同様に、移動操作を提供する場合、カスタムを実装する必要がないということですstd::swap。残念ながら、別の移動代入演算子を定義しないと、これは機能せず、再帰します。もちろんstd::swap、メンバーを交換するために使用できます。

AnObject& operator=(AnObject other)
{
    std::swap(n,other.n);
    std::swap(a,other.a);
    return *this;
}

したがって、最終的なクラスは次のとおりです。

class AnObject
{
public:
  AnObject( size_t n = 0 ) :
    n( n ),
    a( new int[ n ] )
  {}
  AnObject( const AnObject& rh ) :
    n( rh.n ),
    a( new int[ rh.n ] )
  {
    std::copy( rh.a, rh.a + n, a );
  }
  AnObject( AnObject&& rh ) :
    n( std::move(rh.n) ),
    a( std::move(rh.a) )
  {
    rh.n = 0;
    rh.a = nullptr;
  }
  AnObject& operator = ( AnObject rh )
  {
    std::swap(n,rh.n);
    std::swap(a,rh.a);
    return *this;
  }
  ~AnObject()
  {
    delete [] a;
  }
private:
  size_t n;
  int* a;
};
于 2011-05-12T10:46:12.433 に答える
4

手伝わせてください:

#include <vector>

class AnObject
{
public:
  AnObject( size_t n = 0 ) : data(n) {}

private:
  std::vector<int> data;
};

C++0x FDIS から、[class.copy]注 9:

クラス X の定義でムーブ コンストラクターが明示的に宣言されていない場合、1 つが暗黙的にデフォルトとして宣言されます。

  • X にはユーザー宣言のコピー コンストラクターがありません。

  • X には、ユーザー宣言のコピー代入演算子がありません。

  • X には、ユーザー宣言の移動代入演算子がありません。

  • X にはユーザー宣言のデストラクタがなく、

  • 移動コンストラクターは、暗黙的に削除済みとして定義されません。

[ 注: ムーブ コンストラクターが暗黙的に宣言されていないか、明示的に提供されていない場合、そうでなければムーブ コンストラクターを呼び出す式は、代わりにコピー コンストラクターを呼び出すことができます。—終わりのメモ]

個人的にはstd::vector、リソースを正しく管理し、作成できるコードでのコピー/移動を最適化することに自信を持っています。

于 2011-05-12T12:28:32.027 に答える
1

元のポスターのq3

あなた (および他の応答者の一部) は、コンパイラ エラーの意味を誤解し、そのために間違った結論に達したと思います。コンパイラは、(move) 代入呼び出しがあいまいであると考えていますが、その通りです! 同等に修飾された複数のメソッドがあります。

クラスの元のバージョンではAnObject、コピー コンストラクターはconst(左辺値) 参照によって古いオブジェクトを受け取りますが、代入演算子は (非修飾) 値によってその引数を受け取ります。値引数は、演算子の右側にあるものから、適切な転送コンストラクターによって初期化されます。転送コンストラクターは 1 つしかないため、元の右側の式が左辺値か右辺値かに関係なく、そのコピー コンストラクターが常に使用されます。これにより、代入演算子がコピー代入の特別なメンバー関数として機能します。

移動コンストラクターが追加されると、状況が変わります。代入演算子が呼び出されるたびに、転送コンストラクターには 2 つの選択肢があります。左辺値式には引き続きコピー コンストラクターが使用されますが、代わりに右辺値式が指定された場合はいつでもムーブ コンストラクターが使用されます。これにより、代入演算子が move-assignment 特殊メンバー関数として同時に機能します。

従来の移動代入演算子を追加したときに、クラスに同じ特別なメンバー関数の 2 つのバージョンを指定しましたが、これはエラーです。必要なものは既にあるので、従来の移動代入演算子を取り除くだけで、他の変更は必要ありません。

あなたの最新情報に記載されている 2 つのキャンプでは、技術的には最初のキャンプにいると思いますが、理由はまったく異なります。((従来の) move-assignment 演算子は、クラスにとって「壊れている」という理由でスキップしないでください。ただし、不要であるためです。)

ところで、私は C++11 と StackOverflow について読むのが初めてです。これを見る前に、別のSOの質問を閲覧して、この回答を思いつきました。(更新:実際には、まだページを開いていました。リンクは、テクニックを示すFredOverflowによる特定の応答に移動します。)

ハワード・ヒナントによる 2011 年 5 月 12 日の回答について

(私はあまりにも初心者なので、回答に直接コメントすることはできません。)

後のテストですでに選択されている場合は、自己割り当てを明示的にチェックする必要はありません。この場合、n != rh.nはすでにほとんどの処理を行っています。ただし、std::copy呼び出しはその (現在の) inner の外にあるため、コンポーネント レベルの自己割り当てifを取得します。n自己割り当てはめったに行われないはずですが、これらの割り当てが最適ではないかどうかを判断するのはあなた次第です。

于 2011-10-31T13:09:04.323 に答える
1

他の誰かがこれを明示的に指摘しているのを見たことがないので...

値によって引数を取るコピー代入演算子は、コピーの省略により、右辺値が渡された場合 (およびその場合にのみ) 重要な最適化の機会です。しかし、右辺値のみを明示的に受け取る代入演算子を持つクラス(つまり、移動代入演算子を持つクラス) では、これは無意味なシナリオです。したがって、他の回答ですでに指摘されているメモリリークをモジュロすると、コピー代入演算子を単純に変更して引数を const 参照で取得する場合、クラスはすでに理想的であると言えます。

于 2011-05-12T18:50:37.017 に答える