26

私は、C++11 のムーブ セマンティクスがどのように機能するかについて頭を悩ませようとしてきましたが、ムーブ元オブジェクトが満たす必要がある条件を理解するのにかなりの苦労をしています。セマンティクスを移動するという議論はpimplsに最適です

私の問題の最も簡単な例は、次のような pimpl イディオムに関係しています。

class Foo {
    std::unique_ptr<FooImpl> impl_;
public:
    // Inlining FooImpl's constructors for brevity's sake; otherwise it 
    // defeats the point.
    Foo() : impl_(new FooImpl()) {}

    Foo(const Foo & rhs) : impl_(new FooImpl(*rhs.impl_)) {}

    Foo(Foo && rhs) : impl_(std::move(rhs.impl_)) {}

    Foo & operator=(Foo rhs) 
    {
        std::swap(impl_, rhs.impl_);

        return *this;
    }

    void do_stuff () 
    {
        impl_->do_stuff;
    }
};

では、引っ越してきた後はどうすればよいのFooでしょうか。移動元のオブジェクトを安全に破棄し、割り当てることができます。どちらも非常に重要です。ただし、 でやろうとするdo_stuffFoo爆発します。の定義にムーブ セマンティクスを追加する前はFoo、everyFooは できるという不変条件を満たしていましたがdo_stuff、それはもはや当てはまりません。(たとえば)moved-fromFooを配置すると、新しい動的割り当てが必要になり、移動セマンティクスの目的が部分的に無効になるため、優れた代替手段はあまりないようです。impl_入っているかどうかを確認do_stuffして、デフォルトに初期化できましたFooImplそうであれば、それは(通常は偽の)チェックを追加します。メソッドがたくさんある場合は、すべてのメソッドでチェックを行うことを忘れないでください。

do_stuffできることは合理的な不変条件であるという考えをあきらめるべきですか?

4

2 に答える 2

28

「有効な」状態とは何か、および型の移動元オブジェクトに対して実行できる操作は型に対して定義し、文書化します。

標準ライブラリ タイプのオブジェクトを移動すると、オブジェクトは未指定の状態になり、有効な操作を判断するために通常どおり照会できます。

17.6.5.15 ライブラリ型の移動元状態 [lib.types.movedfrom]

C++ 標準ライブラリで定義されている型のオブジェクトは、(12.8) から移動できます。移動操作は、明示的に指定することも、暗黙的に生成することもできます。特に指定がない限り、そのような移動元オブジェクトは、有効であるが指定されていない状態に置かれます。

オブジェクトが「有効」な状態にあるということは、そのタイプに対して標準で指定されているすべての要件が引き続き当てはまることを意味します。つまり、前提条件が満たされている移動元の標準ライブラリ型に対して任意の操作を使用できます。

通常、オブジェクトの状態は既知であるため、実行する各操作の前提条件を満たしているかどうかを確認する必要はありません。移動元オブジェクトとの唯一の違いは、状態がわからないため、確認する必要があることです。たとえば、文字列の状態を照会して pop_back() の前提条件が満たされていることを確認するまでは、move-from 文字列に対して pop_back() を実行しないでください。

std::string s = "foo";
std::string t(std::move(s));
if (!s.empty()) // empty has no preconditions, so it's safe to call on moved-from objects
    s.pop_back(); // after verifying that the preconditions are met, pop_back is safe to call on moved-from objects

標準ライブラリのすべての異なる実装に対して単一の有用な要件セットを作成するのは面倒なので、状態はおそらく指定されていません。


仕様だけでなく、型の実装も担当するため、状態を指定するだけで、クエリを実行する必要がなくなります。たとえば、pimpl 型オブジェクトから移動すると、do_stuff が未定義の動作を伴う無効な操作になることを指定することは完全に合理的です (null ポインターの逆参照を介して)。この言語は、移動元のオブジェクトに対して何もできない場合、またはユーザーが非常に明確かつ非常に明示的に移動操作を示した場合にのみ移動が発生するように設計されているため、ユーザーは移動されたオブジェクトに決して驚くべきではありません。オブジェクトから。


また、標準ライブラリによって定義された「概念」では、移動元のオブジェクトが考慮されていないことに注意してください。つまり、標準ライブラリで定義された概念の要件を満たすためには、型の移動元オブジェクトが概念の要件を満たしている必要があります。これは、タイプのオブジェクトが (関連する概念で定義されているように) 有効な状態にとどまらない場合、標準ライブラリでそれを使用できない (または結果として未定義の動作になる) ことを意味します。

于 2012-08-23T15:46:17.487 に答える
7

ただし、Foo で do_stuff しようとすると、爆発します。

はい。これもそうです:

vector<int> first = {3, 5, 6};
vector<int> second = std::move(first);
first.size();  //Value returned is undefined. May be 0, may not

標準が使用する規則は、オブジェクトを有効(オブジェクトが機能することを意味する) のままにすることですが、未指定の状態にします。これは、呼び出すことができる関数は、オブジェクトの現在の状態に条件がない関数だけであることを意味します。の場合vector、そのコピー/移動代入演算子、clearおよびempty、およびその他のいくつかの操作を使用できます。だからあなたはこれを行うことができます:

vector<int> first = {3, 5, 6};
vector<int> second = std::move(first);
first.clear();  //Cause the vector to become empty.
first.size(); //Now the value is guaranteed to be 0.

あなたの場合、デストラクタと同様に、(どちらかの側からの) コピー/移動割り当ても引き続き機能するはずです。しかし、あなたの他のすべての機能には、移動されていないという状態に基づく前提条件があります。

だから私はあなたの問題を見ていません。

Pimpl 化されたクラスのインスタンスが空にならないようにしたい場合は、適切なコピー セマンティクスを実装し、移動を禁止します。移動には、オブジェクトが空の状態である可能性が必要です。

于 2012-08-23T15:38:18.333 に答える