コピーアンドスワップイディオムがC ++ 11でコピーアンドマイクのIDIOMになるべきですか?
-
21-12-2019 - |
質問
class MyClass
{
private:
BigClass data;
UnmovableClass *dataPtr;
public:
MyClass()
: data(), dataPtr(new UnmovableClass) { }
MyClass(const MyClass& other)
: data(other.data), dataPtr(new UnmovableClass(*other.dataPtr)) { }
MyClass(MyClass&& other)
: data(std::move(other.data)), dataPtr(other.dataPtr)
{ other.dataPtr= nullptr; }
~MyClass() { delete dataPtr; }
friend void swap(MyClass& first, MyClass& second)
{
using std::swap;
swap(first.data, other.data);
swap(first.dataPtr, other.dataPtr);
}
MyClass& operator=(MyClass other)
{
swap(*this, other);
return *this;
}
};
.
MyClassの値をoperator=のパラメータ=のパラメータとして持つことで、コピーコンストラクタまたはMoveコンストラクタのいずれかによってパラメータを構築できます。その後、パラメータから安全にデータを抽出できます。これにより、コードの重複を防ぎ、例外の安全性を支援します。
回答の表現は、一時的な変数を交換または移動することができます。それはスワッピングを主に議論します。ただし、コンパイラによって最適化されていない場合は、スワップは3つの移動操作を含み、より複雑なケースで追加の追加作業を行います。希望するすべての場合、 move が割り当てられたオブジェクトに一時的なものに移動します。
このより複雑な例を考慮して、オブザーバパターンを含む。この例では、代入演算子コードを手動で書きました。強調は、移動コンストラクタ、割り当て演算子、およびスワップ方式の方式の上にあります:
class MyClass : Observable::IObserver
{
private:
std::shared_ptr<Observable> observable;
public:
MyClass(std::shared_ptr<Observable> observable) : observable(observable){ observable->registerObserver(*this); }
MyClass(const MyClass& other) : observable(other.observable) { observable.registerObserver(*this); }
~MyClass() { if(observable != nullptr) { observable->unregisterObserver(*this); }}
MyClass(MyClass&& other) : observable(std::move(other.observable))
{
observable->unregisterObserver(other);
other.observable.reset(nullptr);
observable->registerObserver(*this);
}
friend void swap(MyClass& first, MyClass& second)
{
//Checks for nullptr and same observable omitted
using std::swap;
swap(first.observable, second.observable);
second.observable->unregisterObserver(first);
first.observable->registerObserver(first);
first.observable->unregisterObserver(second);
second.observable->registerObserver(second);
}
MyClass& operator=(MyClass other)
{
observable->unregisterObserver(*this);
observable = std::move(other.observable);
observable->unregisterObserver(other);
other.observable.reset(nullptr);
observable->registerObserver(*this);
}
}
.
明らかに、この手動で書かれた割り当て演算子のコードの重複部分は、移動コンストラクタのそれと同じです。割り当て演算子にスワップを実行でき、動作は正しいですが、潜在的に動きを実行し、(スワップ内の)登録(スワップ内)と登録(デストラクタ内)を実行します。
Steadでの移動コンストラクタのコードを再利用するのははるかに理にかなっていませんか?
private:
void performMoveActions(MyClass&& other)
{
observable->unregisterObserver(other);
other.observable.reset(nullptr);
observable->registerObserver(*this);
}
public:
MyClass(MyClass&& other) : observable(std::move(other.observable))
{
performMoveActions(other);
}
MyClass& operator=(MyClass other)
{
observable->unregisterObserver(*this);
observable = std::move(other.observable);
performMoveActions(other);
}
.
このアプローチがスワップアプローチに劣ることはないように私に見えます。 C ++ 11のコピーアンドマイクの慣用句として、コピーアンドスワップの慣用句がより良くなると思います。
解決 3
私がこの質問をしてから長い時間が経ちました、そして私は今の間答えを知っていましたが、私はそれに対する答えを書いています。ここでは。
答えはいいえです。コピーアンドスワップイディオムはコピーアンドマイクの慣用句にはなりません。
コピーアンドスワップ(移動コンストラクトアンドスワップ)の重要な部分は、安全なクリーンアップで代入演算子を実装する方法です。古いデータは、コピー構築または移動構築の一時的な一時的に交換されます。操作が終わったら、一時的なものが削除され、そのデストラクタは呼び出されます。
スワップ動作はデストラクタを再利用できるようにするため、割り当て演算子にクリーンアップコードを作成する必要はありません。
実行するクリーンアップ動作がなく、割り当てのみがない場合は、デフォルトでコピーアンドスワップが必要ではないため、割り当て演算子を宣言できるはずです。
Move Constructor自体は通常、新しいオブジェクトであるため、クリーンアップ動作を必要としません。一般的な簡単なアプローチは、Moveコンストラクタがデフォルトのコンストラクタを起動し、次にすべてのメンバーを移動からのオブジェクトと交換します。移動したオブジェクトは、Blandのデフォルト構築オブジェクトのようになります。
しかし、この質問のオブザーバーパターンの例では、古いオブジェクトへの参照を変更する必要があるため、実際には追加のクリーンアップ作業を行う必要がある例外です。一般的に、私はあなたのオブザーバーや観測可能なもの、そして他の設計構築物、そして可能な限り動いていない他のデザイン構成要素を作ることをお勧めします。
他のヒント
各特別なメンバーに、それが値する優しい愛情のある世話をして、できるだけデフォルトのものを試してください。
class MyClass
{
private:
BigClass data;
std::unique_ptr<UnmovableClass> dataPtr;
public:
MyClass() = default;
~MyClass() = default;
MyClass(const MyClass& other)
: data(other.data)
, dataPtr(other.dataPtr ? new UnmovableClass(*other.dataPtr)
: nullptr)
{ }
MyClass& operator=(const MyClass& other)
{
if (this != &other)
{
data = other.data;
dataPtr.reset(other.dataPtr ? new UnmovableClass(*other.dataPtr)
: nullptr);
}
return *this;
}
MyClass(MyClass&&) = default;
MyClass& operator=(MyClass&&) = default;
friend void swap(MyClass& first, MyClass& second)
{
using std::swap;
swap(first.data, second.data);
swap(first.dataPtr, second.dataPtr);
}
};
.
必要に応じて、デストラクタは上に暗黙的にデフォルトでデフォルト設定されています。この例では、他のすべてが明示的に定義されているかデフォルト設定される必要があります。
リファレンス:> http://accu.org/content/conf2014/howard_hinnant_accu_2014.pdf
コピー/スワップの慣用句はあなたがパフォーマンスを費やす可能性が高いでしょう(スライドを参照)。たとえば、std::vector
やstd::string
のような高性能/頻繁に使用されるSTD :: Typesはなぜ疑問に思うのか疑問に思いますか。パフォーマンスが低いのは理由です。 BigClass
にstd::vector
またはstd::string
sが含まれている場合(それは可能性が高いように思われる)、あなたの特別なメンバーからの特別なメンバーと呼ぶことです。上記はそれをする方法です。
課題に強い例外安全性が必要な場合は、パフォーマンスに加えてそのスライドを参照してください( "strong_assign"の検索)。
まず、クラスが移動可能である限り、C ++ 11にswap
関数を書くことは一般的に不要です。デフォルトのswap
は移動に頼るでしょう:
void swap(T& left, T& right) {
T tmp(std::move(left));
left = std::move(right);
right = std::move(tmp);
}
.
それはそれであり、要素は交換されます。
2秒、コピーアンドスワップは実際にはまだ保持されています。
T& T::operator=(T const& left) {
using std::swap;
T tmp(left);
swap(*this, tmp);
return *this;
}
// Let's not forget the move-assignment operator to power down the swap.
T& T::operator=(T&&) = default;
.
はコピーしてスワップ(つまり、移動)または移動してスワップ(つまり、移動)で、常に最適なパフォーマンスに近づくべきです。冗長な任務があるかもしれませんが、うまくいけばあなたのコンパイラはそれを大事にするでしょう。
編集:コピー割り当て演算子のみを実装します。デフォルトでも、別の移動割り当て演算子も必要とされます。