それは私のせいです。(半分冗談、半分そうではない)。
最初にムーブ代入演算子の実装例を示したとき、スワップを使用しました。次に、賢い人(誰だったか覚えていません)が、割り当ての前に lhs を破壊することの副作用が重要である可能性があることを指摘しました(例の unlock() など)。そのため、移動割り当てにスワップを使用するのをやめました。しかし、スワップの使用の歴史はまだ残っています。
この例で swap を使用する理由はありません。あなたが提案したものよりも効率が悪いです。実際、libc++ では、私はあなたが提案したことを正確に行います:
unique_lock& operator=(unique_lock&& __u)
{
if (__owns_)
__m_->unlock();
__m_ = __u.__m_;
__owns_ = __u.__owns_;
__u.__m_ = nullptr;
__u.__owns_ = false;
return *this;
}
一般に、ムーブ代入演算子は次のことを行う必要があります。
- 目に見えるリソースを破棄します (ただし、実装の詳細リソースは保存してください)。
- すべてのベースとメンバーを移動して割り当てます。
- ベースとメンバーの移動割り当てが rhs をリソースレスにしなかった場合は、そうします。
そのようです:
unique_lock& operator=(unique_lock&& __u)
{
// 1. Destroy visible resources
if (__owns_)
__m_->unlock();
// 2. Move assign all bases and members.
__m_ = __u.__m_;
__owns_ = __u.__owns_;
// 3. If the move assignment of bases and members didn't,
// make the rhs resource-less, then make it so.
__u.__m_ = nullptr;
__u.__owns_ = false;
return *this;
}
アップデート
コメントには、移動コンストラクターの処理方法に関するフォローアップの質問があります。そこに(コメントで)回答し始めましたが、フォーマットと長さの制約により、明確な回答を作成することが難しくなっています。したがって、私はここに私の応答を入れています。
問題は、ムーブ コンストラクターを作成するための最適なパターンは何かということです。デフォルトのコンストラクターに委譲してからスワップしますか? これには、コードの重複を減らすという利点があります。
私の回答は次のとおりです。最も重要なポイントは、プログラマーは無意識にパターンに従うことに慎重であるべきだということです。デフォルト+スワップとして移動コンストラクターを実装することがまさに正しい答えであるいくつかのクラスがあるかもしれません。クラスは大きくて複雑かもしれません。はA(A&&) = default;
間違ったことをするかもしれません。各クラスの選択をすべて考慮することが重要だと思います。
OP の例を詳しく見てみましょう std::unique_lock(unique_lock&&)
。
所見:
A. このクラスはかなり単純です。次の 2 つのデータ メンバーがあります。
mutex_type* __m_;
bool __owns_;
B. このクラスは汎用ライブラリにあり、不明な数のクライアントによって使用されます。このような状況では、パフォーマンスの問題が優先されます。クライアントがパフォーマンスが重要なコードでこのクラスを使用するかどうかはわかりません。したがって、それらがそうであると仮定する必要があります。
C. このクラスのムーブ コンストラクターは、少数のロードとストアで構成されます。したがって、パフォーマンスを調べる良い方法は、ロードとストアをカウントすることです。たとえば、あなたが 4 つの店舗で何かを行い、他の誰かが 2 つの店舗だけで同じことを行う場合、どちらの実装も非常に高速です。しかし、彼らのはあなたの2 倍の速さです! この違いは、一部のクライアントのタイトなループでは重要になる可能性があります。
最初に、デフォルトのコンストラクターとメンバー swap 関数でロードとストアをカウントします。
// 2 stores
unique_lock()
: __m_(nullptr),
__owns_(false)
{
}
// 4 stores, 4 loads
void swap(unique_lock& __u)
{
std::swap(__m_, __u.__m_);
std::swap(__owns_, __u.__owns_);
}
移動コンストラクターを 2 つの方法で実装しましょう。
// 4 stores, 2 loads
unique_lock(unique_lock&& __u)
: __m_(__u.__m_),
__owns_(__u.__owns_)
{
__u.__m_ = nullptr;
__u.__owns_ = false;
}
// 6 stores, 4 loads
unique_lock(unique_lock&& __u)
: unique_lock()
{
swap(__u);
}
最初の方法は、2 番目の方法よりもはるかに複雑に見えます。また、ソース コードはより大きく、別の場所で既に記述されている可能性がある (たとえば、move 代入演算子で) 多少重複するコードがあります。つまり、バグの可能性が高くなります。
2 番目の方法はより単純で、既に作成したコードを再利用します。したがって、バグの可能性が低くなります。
最初の方法は高速です。ロードとストアのコストがほぼ同じであれば、おそらく 66% 高速です!
これは、古典的なエンジニアリングのトレードオフです。フリーランチはありません。また、エンジニアは、トレードオフについて決定を下さなければならないという重荷から解放されることはありません。その瞬間、飛行機が空中から落下し始め、原子力発電所が溶け始めます。
libc++については、より高速なソリューションを選択しました。私の理論的根拠は、このクラスについては、何があっても正しく理解したほうがよいということです。クラスは十分に単純なので、正しく理解できる可能性は高いです。クライアントはパフォーマンスを重視するようになります。別の文脈の別のクラスについては、別の結論に達する可能性があります。