5

リソースを管理する次のクラスを想像してください (私の質問は移動代入演算子についてのみです)。

struct A
{
    std::size_t s;
    int* p;
    A(std::size_t s) : s(s), p(new int[s]){}
    ~A(){delete [] p;}
    A(A const& other) : s(other.s), p(new int[other.s])
    {std::copy(other.p, other.p + s, this->p);}
    A(A&& other) : s(other.s), p(other.p)
    {other.s = 0; other.p = nullptr;}
    A& operator=(A const& other)
    {A temp = other; std::swap(*this, temp); return *this;}
    // Move assignment operator #1
    A& operator=(A&& other)
    {
        std::swap(this->s, other.s);
        std::swap(this->p, other.p);
        return *this;
    }
    // Move assignment operator #2
    A& operator=(A&& other)
    {
        delete [] p;
        s = other.s;
        p = other.p;
        other.s = 0;
        other.p = nullptr;
        return *this;
     } 
};

質問:

上記の 2 つの Move 代入演算子 #1 と #2 の長所と短所は何ですか? 私が見ることができる唯一の違いはstd::swap、lhsのストレージを保持することだと思いますが、とにかく右辺値が破棄されるため、それがどのように役立つかわかりません。たぶん唯一の場合は のようなものですa1 = std::move(a2);が、この場合でも #1 を使用する理由はありません。

4

3 に答える 3

9

これは、実際に測定する必要があるケースです。

そして、OPのコピー代入演算子を見て、非効率性を見ています:

A& operator=(A const& other)
    {A temp = other; std::swap(*this, temp); return *this;}

*thisotherが同じ場合はどうなりsますか?

if の場合、よりスマートなコピー割り当てにより、ヒープへの移動を回避できるように思えs == other.sます。それがしなければならないのはコピーだけです:

A& operator=(A const& other)
{
    if (this != &other)
    {
        if (s != other.s)
        {
            delete [] p;
            p = nullptr;
            s = 0;
            p = new int[other.s];
            s = other.s;
        }
        std::copy(other.p, other.p + s, this->p);
    }
    return *this;
}

強力な例外安全性を必要とせず、コピー割り当て ( 、 など) の基本的な例外安全性のみが必要な場合std::stringは、std::vector上記の方法でパフォーマンスが向上する可能性があります。いくら?測定。

このクラスを次の 3 つの方法でコーディングしました。

デザイン 1:

上記のコピー代入演算子と OP のムーブ代入演算子 #1 を使用します。

デザイン 2:

上記のコピー代入演算子と OP のムーブ代入演算子 #2 を使用します。

デザイン 3:

コピー代入と移動代入の両方に対する DeadMG のコピー代入演算子。

テストに使用したコードは次のとおりです。

#include <cstddef>
#include <algorithm>
#include <chrono>
#include <iostream>

struct A
{
    std::size_t s;
    int* p;
    A(std::size_t s) : s(s), p(new int[s]){}
    ~A(){delete [] p;}
    A(A const& other) : s(other.s), p(new int[other.s])
    {std::copy(other.p, other.p + s, this->p);}
    A(A&& other) : s(other.s), p(other.p)
    {other.s = 0; other.p = nullptr;}
    void swap(A& other)
    {std::swap(s, other.s); std::swap(p, other.p);}
#if DESIGN != 3
    A& operator=(A const& other)
    {
        if (this != &other)
        {
            if (s != other.s)
            {
                delete [] p;
                p = nullptr;
                s = 0;
                p = new int[other.s];
                s = other.s;
            }
            std::copy(other.p, other.p + s, this->p);
        }
        return *this;
    }
#endif
#if DESIGN == 1
    // Move assignment operator #1
    A& operator=(A&& other)
    {
        swap(other);
        return *this;
    }
#elif DESIGN == 2
    // Move assignment operator #2
    A& operator=(A&& other)
    {
        delete [] p;
        s = other.s;
        p = other.p;
        other.s = 0;
        other.p = nullptr;
        return *this;
     } 
#elif DESIGN == 3
    A& operator=(A other)
    {
        swap(other);
        return *this;
    }
#endif
};

int main()
{
    typedef std::chrono::high_resolution_clock Clock;
    typedef std::chrono::duration<float, std::nano> NS;
    A a1(10);
    A a2(10);
    auto t0 = Clock::now();
    a2 = a1;
    auto t1 = Clock::now();
    std::cout << "copy takes " << NS(t1-t0).count() << "ns\n";
    t0 = Clock::now();
    a2 = std::move(a1);
    t1 = Clock::now();
    std::cout << "move takes " << NS(t1-t0).count() << "ns\n";
}

これが私が得た出力です:

$ clang++ -std=c++11 -stdlib=libc++ -O3 -DDESIGN=1  test.cpp 
$ a.out
copy takes 55ns
move takes 44ns
$ a.out
copy takes 56ns
move takes 24ns
$ a.out
copy takes 53ns
move takes 25ns
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DDESIGN=2  test.cpp 
$ a.out
copy takes 74ns
move takes 538ns
$ a.out
copy takes 59ns
move takes 491ns
$ a.out
copy takes 61ns
move takes 510ns
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DDESIGN=3  test.cpp 
$ a.out
copy takes 666ns
move takes 304ns
$ a.out
copy takes 603ns
move takes 446ns
$ a.out
copy takes 619ns
move takes 317ns

DESIGN 1私にはかなり良さそうです。

警告: ミューテックス ロックの所有権やファイルのオープン状態の所有権など、"迅速に" 割り当てを解除する必要があるリソースがクラスにある場合は、正確性の観点から design-2 移動代入演算子の方が適している可能性があります。ただし、リソースが単なるメモリの場合は、(OP のユースケースのように) できるだけ長く割り当て解除を遅らせることが有利な場合がよくあります。

警告 2: 重要であることがわかっている他のユース ケースがある場合は、それらを測定します。あなたは、私がここで得たものとは異なる結論に達するかもしれません。

注:「DRY」よりもパフォーマンスを重視します。ここにあるすべてのコードは、1 つのクラス ( struct A) 内にカプセル化されます。struct Aできるだけ良くしてください。そして、あなたが十分に質の高い仕事をすれば、あなたのクライアントstruct A(あなた自身かもしれません)は「RIA」(Reinvent It Again)の誘惑に駆られることはありません。私は、クラス全体の実装を何度も繰り返すよりも、1 つのクラス内で小さなコードを繰り返すことを好みます。

于 2012-03-24T02:48:01.577 に答える
8

#2 を使用すると、DRY に違反し、デストラクタ ロジックが重複するため、#2 よりも #1 を使用する方が有効です。次に、次の代入演算子を検討してください。

A& operator=(A other) {
    swap(*this, other);
    return *this;
}

これは、コードが重複しないためのコピー代入演算子と移動代入演算子の両方であり、優れた形式です。

于 2012-03-23T23:49:26.330 に答える
3

swap()関連するオブジェクトがスローできない場合、DeadMG によって投稿された代入演算子はすべて正しいことを行っています。残念ながら、これは常に保証されるわけではありません! 特に、ステートフル アロケータがあり、これが機能しない場合。アロケータが異なる可能性がある場合は、個別のコピーと移動の割り当てが必要なようです。コピー コンストラクタは、無条件にアロケータを渡すコピーを作成します。

T& T::operator=(T const& other) {
    T(other, this->get_allocator()).swap(*this);
    return * this;
}

move 代入は、アロケータが同一で​​あるかどうかをテストし、同一である場合swap()は 2 つのオブジェクトのみをテストし、そうでない場合は単にコピー代入を呼び出します。

T& operator= (T&& other) {
    if (this->get_allocator() == other.get_allocator()) {
        this->swap(other);
    }
    else {
        *this = other;
    }
    return *this;
}

値を取るバージョンは、 が である場合に優先される、はるかに単純な代替手段noexcept(v.swap(*this))ですtrue

これは、暗黙的に元の質問にも答えます。スローswap()とムーブの代入が存在する場合、基本的な例外セーフではないため、両方の実装が間違っています。例外の唯一のソースswap()が一致しないアロケーターであると仮定すると、上記の実装は強力な例外セーフです。

于 2012-03-24T00:15:02.587 に答える