web-dev-qa-db-ja.com

一部の人が移動割り当てにスワップを使用するのはなぜですか?

たとえば、stdlibc ++には以下が含まれます。

unique_lock& operator=(unique_lock&& __u)
{
    if(_M_owns)
        unlock();
    unique_lock(std::move(__u)).swap(*this);
    __u._M_device = 0;
    __u._M_owns = false;
    return *this;
}

2つの__uメンバーを* thisに直接割り当てないのはなぜですか?スワップは、__ uに* thisメンバーが割り当てられていることを意味するのではなく、後で0とfalseを割り当てただけです...この場合、スワップは不要な作業を行っています。何が欠けていますか? (unique_lock :: swapは、各メンバーでstd :: swapを実行するだけです)

58
Display Name

それは私のせいです。 (半冗談、半ばない)。

最初に移動代入演算子の実装例を示したとき、私はスワップを使用しました。次に、賢い人(私は誰だか思い出せない)が、割り当ての前にlhを破棄することの副作用が重要である可能性があることを指摘しました(例では、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;
    }
_

一般に、移動割り当て演算子は次のようにする必要があります。

  1. 可視リソースを破棄します(実装の詳細リソースはおそらく保存されます)。
  2. 移動は、すべてのベースとメンバーを割り当てます。
  3. 拠点とメンバーの移動割り当てで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;
    }
_

更新

コメントには、移動コンストラクターの処理方法に関するフォローアップの質問があります。私はそこに(コメントで)回答し始めましたが、フォーマットと長さの制約により、明確な応答を作成することが困難です。したがって、私はここに私の応答を入れています。

問題は、移動コンストラクターを作成するための最良のパターンは何ですか?デフォルトのコンストラクターに委任してからスワップしますか?これには、コードの重複を減らすという利点があります。

私の返答は次のとおりです。最も重要な要点は、プログラマーは考えずにパターンをたどらないことです。 move +コンストラクターをdefault + swapとして実装することが正解であるクラスがいくつかあるかもしれません。クラスは大きくて複雑かもしれません。 A(A&&) = default;は間違った動作をする可能性があります。クラスごとにすべての選択肢を検討することが重要だと思います。

OPの例を詳しく見てみましょう:std::unique_lock(unique_lock&&)

観察:

A.このクラスはかなり単純です。 2つのデータメンバーがあります。

_mutex_type* __m_;
bool __owns_;
_

B.このクラスは汎用ライブラリにあり、未知の数のクライアントが使用します。このような状況では、パフォーマンスの問題が優先されます。クライアントがパフォーマンスクリティカルなコードでこのクラスを使用するかどうかはわかりません。だから私たちは彼らがそうであると仮定する必要があります。

C.このクラスの移動コンストラクターは、何があっても少数のロードとストアで構成されます。したがって、パフォーマンスを確認する良い方法は、ロードとストアをカウントすることです。たとえば、4つのストアで何かを行い、他の誰かが2つのストアのみで同じことを行う場合、どちらの実装も非常に高速です。しかし、それらはtwiceがあなたのものと同じ速さです!一部のクライアントのタイトループでは、この違いが重要になる場合があります。

最初に、デフォルトのコンストラクターとメンバースワップ関数でロードとストアをカウントします。

_// 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つの方法でmoveコンストラクターを実装します。

_// 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番目の方法よりもはるかに複雑に見えます。また、ソースコードが大きくなり、他の場所(たとえば、移動割り当て演算子)で既に記述したコードと多少重複している可能性があります。つまり、バグが発生する可能性が高くなります。

2番目の方法はより簡単で、すでに記述したコードを再利用します。したがって、バグの可能性が低くなります。

最初の方法はより高速です。ロードとストアのコストがほぼ同じであれば、おそらく66%速くなります!

これは典型的なエンジニアリングのトレードオフです。無料の昼食はありません。そして、エンジニアはトレードオフについての決定をしなければならないという負担から解放されることは決してありません。その瞬間、飛行機が空から落ち始め、原子力発電所が溶け始めます。

libc ++ の場合、より高速なソリューションを選択しました。私の理論的根拠は、このクラスの場合、何があっても正しく理解することです。クラスは非常にシンプルなので、正しく理解できる可能性が高いです。クライアントはパフォーマンスを重視します。別のコンテキストの別のクラスについては、別の結論に達するかもしれません。

88
Howard Hinnant

例外的な安全性についてです。 __uは、演算子が呼び出されたときに既に構築されており、例外がないことがわかっており、swapはスローしません。

メンバーの割り当てを手動で行った場合、それらのそれぞれが例外をスローする可能性があり、部分的に移動割り当てされたものを救済する必要があることに対処する必要があります。

多分この簡単な例ではこれは表示されませんが、一般的な設計原則です。

  • コピー-構築およびスワップによるコピー割り当て。
  • Move-constructとswapによる移動割り当て。
  • 書く +構成要素と+=など.

基本的には、「実際の」コードの量を最小限に抑え、コア機能に関して他の機能をできるだけ多く表現しようとします。

unique_ptrは、コピーの構築/割り当てを許可しないため、割り当てで明示的な右辺値参照を使用します。したがって、この設計原則の最良の例ではありません。)

8
Kerrek SB

トレードオフに関して考慮すべきもう1つのこと:

Default-construct + swapの実装は遅く見えるかもしれませんが、コンパイラーでのデータフロー分析は、一部の無意味な割り当てを排除し、手書きのコードと非常によく似たものになる場合があります。これは、「賢い」値のセマンティクスを持たない型に対してのみ機能します。例として、

 struct Dummy
 {
     Dummy(): x(0), y(0) {} // suppose we require default 0 on these
     Dummy(Dummy&& other): x(0), y(0)
     {
         swap(other);             
     }

     void swap(Dummy& other)
     {
         std::swap(x, other.x);
         std::swap(y, other.y);
         text.swap(other.text);
     }

     int x, y;
     std::string text;
 }

最適化なしのムーブctorで生成されたコード:

 <inline std::string() default ctor>
 x = 0;
 y = 0;
 temp = x;
 x = other.x;
 other.x = temp;
 temp = y;
 y = other.y;
 other.y = temp;
 <inline impl of text.swap(other.text)>

これはひどいように見えますが、データフロー分析により、コードと同等であると判断できます。

 x = other.x;
 other.x = 0;
 y = other.y;
 other.y = 0;
 <overwrite this->text with other.text, set other.text to default>

多分実際にはコンパイラは常に最適なバージョンを生成するとは限りません。それを試して、議会をちらっと見たいと思うかもしれません。

また、クラスのメンバーの1つがstd :: shared_ptrである場合など、「賢い」値のセマンティクスのために、スワッピングが割り当てよりも優れている場合もあります。移動コンストラクターがアトミック参照カウンターを混乱させる理由はありません。

2
yonil

ヘッダーの質問に答えます。「なぜ移動割り当てにスワップを使用する人がいるのですか?」です。

swapを使用する主な理由は、移動以外の割り当てを提供しないです。

ハワード・ヒナントのコメントから:

一般に、移動割り当て演算子は次のようにする必要があります。
1。可視リソースを破棄します(ただし、実装の詳細リソースは保存される可能性があります)。

しかし、一般的に破棄/解放関数は失敗して例外をスローする可能性があります

次に例を示します。

_class unix_fd
{
    int fd;
public:
    explicit unix_fd(int f = -1) : fd(f) {}
    ~unix_fd()
    {
        if(fd == -1) return;
        if(::close(fd)) /* !!! call is failed! But we can't throw from destructor so just silently ignore....*/;
    }

    void close() // Our release-function
    {
        if(::close(fd)) throw system_error_with_errno_code;
    }
};
_

次に、移動割り当ての2つの実装を比較してみましょう。

_// #1
void unix_fd::operator=(unix_fd &&o) // Can't be noexcept
{
    if(&o != this)
    {
        close(); // !!! Can throw here
        fd = o.fd;
        o.fd = -1;
    }
    return *this;
}
_

そして

_// #2
void unix_fd::operator=(unix_fd &&o) noexcept
{
    std::swap(fd, o.fd);
    return *this;
}
_

_#2_は完全に例外ではありません!

はい、close()の呼び出しは、_#2_の場合に「遅延」できます。だが!厳密なエラーチェックが必要な場合は、デストラクタではなく明示的なclose()呼び出しを使用する必要があります。デストラクタは、「緊急」の状況でのみリソースを解放します。例外はとにかく例外をスローできません。

追伸コメントのディスカッションも参照 ここ コメント内

2