web-dev-qa-db-ja.com

(なぜ)ムーブコンストラクターまたはムーブ代入演算子はその引数をクリアする必要がありますか?

私が受講しているC++コースからの移動コンストラクターの実装例は次のようになります。

/// Move constructor
Motorcycle::Motorcycle(Motorcycle&& ori)
    : m_wheels(std::move(ori.m_wheels)),
      m_speed(ori.m_speed),
      m_direction(ori.m_direction)
{
    ori.m_wheels = array<Wheel, 2>();
    ori.m_speed      = 0.0;
    ori.m_direction  = 0.0;
}

m_wheelsはタイプstd::array<Wheel, 2>のメンバーであり、Wheelにはdouble m_speedbool m_rotatingのみが含まれます。Motorcycleクラス、m_speedおよびm_directiondoublesです。)

oriの値をクリアする必要がある理由がよくわかりません。

Motorcycleに「盗む」必要のあるポインタメンバーがある場合は、確かに、たとえばori.m_thingy = nullptrを2回使用しないように、delete m_thingyを設定する必要があります。しかし、フィールドにオブジェクト自体が含まれている場合は重要ですか?

私はこれについて友人に尋ねました、そして彼らは私を このページ に向けました、それは言います:

移動コンストラクターは通常、引数によって保持されているリソース(動的に割り当てられたオブジェクトへのポインター、ファイル記述子、TCPソケット、I/Oストリーム、実行中のスレッドなど))を作成するのではなく、「盗みます」。それらのコピーを作成し、引数を有効であるが不確定な状態のままにします。たとえば、std::stringまたはstd::vectorから移動すると、引数が生成される場合があります。空のままにします。ただし、この動作は信頼しないでください。

不確定状態の意味を誰が定義しますか?速度を0.0に設定することが、そのままにしておくよりも不確定であることがわかりません。そして、引用の最後の文が言うように—コードはとにかくmoved-fromMotorcycleの状態に依存すべきではないので、なぜわざわざクリーンアップするのですか? ?

35
Lynn

彼らは必要掃除する必要はありません。クラスの設計者は、移動元のオブジェクトゼロを初期化したままにしておくことをお勧めします。

が理にかなっている状況は、デストラクタで解放されるリソースを管理するオブジェクトの場合であることに注意してください。たとえば、動的に割り当てられたメモリへのポインタ。移動元オブジェクトのポインタを変更せずに残すと、2つのオブジェクトが同じリソースを管理することになります。彼らの両方のデストラクタは解放しようとしました。

30
juanchopanza

不確定状態の意味を誰が定義しますか?

クラスの作者。

_std::unique_ptr_ のドキュメントを見ると、次の行の後にそれがわかります。

_std::unique_ptr<int> pi = std::make_unique<int>(5);
auto pi2 = std::move(pi);
_

piは実際には非常に明確な状態にあります。それwillreset()およびpi.get()willnullptrと等しい。

14
Richard Hodges

このクラスの作成者がコンストラクターに不要な操作を入れているのはおそらく正しいでしょう。

m_wheelsstd::vectorのようにヒープに割り当てられた型であったとしても、それはすでにであるため、「クリア」する理由はありません。そのownmove-constructorに渡されます:

: m_wheels(std::move(ori.m_wheels)),   // invokes move-constructor of appropriate type

ただし、この特定の場合に明示的な「クリア」操作が必要かどうかを知ることができるほど十分なクラスを示していません。 Deduplicatorの回答によると、次の関数は「移動元」状態で正しく動作することが期待されています。

// Destruction
~Motorcycle(); // This is the REALLY important one, of course
// Assignment
operator=(const Motorcycle&);
operator=(Motorcycle&&);

したがって、move-constructorが正しいかどうかを判断するには、これらの関数のeachの実装を調べる必要があります。

3つすべてがデフォルトの実装を使用している場合(これは、あなたが示したものからすると合理的と思われます)、移動元のオブジェクトを手動でクリアする理由はありません。ただし、これらの関数のいずれかがsem_wheelsm_speed、またはm_directionの値を使用して動作方法を決定する場合、move-constructorは- 必要正しい動作を保証するためにそれらをクリアします。

プリミティブがクリーンアップを実行する必要があるかどうかを示すフラグとして明示的に使用されない限り、通常、プリミティブの値が「移動元」状態で重要になるとは思われないため、このようなクラス設計はエレガントでエラーが発生しやすくなりますデストラクタ。

最終的に、C++クラスで使用するためのexampleとして、示されているmove-constructorは、望ましくない動作を引き起こすという意味で技術的に「間違っている」わけではありませんが、この質問を投稿することになったまさに混乱を引き起こします。

10
Kyle Strand

移動元のオブジェクトで安全に実行できる必要があることがいくつかあります。

  1. それを破壊します。
  2. 割り当て/移動します。
  3. 明示的にそう言う他の操作。

したがって、これら2つの保証を与えながら、できるだけ早くそれらから移動する必要があります。さらに、それらが有用であり、無料で実行できる場合に限ります。
これは多くの場合、ソースをクリアしないことを意味し、他の場合はそれを行うことを意味します。

さらに、簡潔さと自明性を維持するために、ユーザー定義関数よりも明示的にデフォルト設定され、両方よりも暗黙的に定義された関数を優先します。

3
Deduplicator

不確定状態の意味を誰が定義しますか?

英語だと思います。ここでの「不確定」は、通常の英語の意味の1つであり、 「確定していない、範囲が正確に固定されていない、不定である、不確実である。確立されていない。解決または決定されていない」 。移動元オブジェクトの状態は、そのオブジェクトタイプに対して「有効」である必要があることを除いて、標準によって制約されません。オブジェクトが移動するたびに同じである必要はありません。

実装によって提供される型についてのみ話している場合、適切な言語は「有効であるが、それ以外は指定されていない状態」になります。ただし、ユーザーコードで実行できることの詳細ではなく、C++実装の詳細について説明するために「未指定」という単語を予約します。

「有効」は、タイプごとに個別に定義されます。整数型を例にとると、トラップ表現は無効であり、値を表すものはすべて有効です。

ここでのその標準は、moveコンストラクターがオブジェクトを不確定にする必要があると言っているのではなく、単にオブジェクトを決定された状態にする必要がないということです。したがって、0は古い値よりも「不確定」ではないというのは正しいですが、moveコンストラクターが古いオブジェクトを「可能な限り不確定」にする必要がないため、いずれにしても意味がありません。

この場合、クラスの作成者はchosenを使用して、古いオブジェクトを1つの特定の状態にします。その場合、その状態が何であるかを文書化するかどうかは完全に彼ら次第であり、文書化する場合は、コードのユーザーがそれに依存するかどうかは完全に責任があります。

私は通常、それに依存しないnotをお勧めします。特定の状況下では、意味的に移動であると考えて記述したコードが実際にコピーを実行するためです。たとえば、std::move割り当ての右側で、オブジェクトがconstであるかどうかを気にしないのは、どちらの方法でも機能するためです。その後、他の誰かがやって来て、「ああ、移動されました。ゼロにクリアされました」。いいえ。彼らは、移動時にMotorcycleがクリアされることを見落としていましたが、const Motorcycleもちろん、ドキュメントが彼らに何を示唆していても、そうではありません:-)

状態を設定する場合、それは実際にはどの状態をコイントスするかです。これが最もニュートラルな値を表すことに基づいて、引数なしのコンストラクターが実行すること(存在する場合)と一致する「クリア」状態に設定できます。そして、多くのアーキテクチャでは、0が何かを設定するための(おそらく共同で)最も安い値だと思います。あるいは、誰かが誤ってオブジェクトから移動してその値を使用するバグを書いたときに、「何?光の速度?住宅街?そうそう、覚えている、それはこのクラスが引っ越したときに設定する値だ-から、私はおそらくそれをした」。

2
Steve Jessop

これには標準的な動作はありません。ポインタと同様に、削除した後も引き続き使用できます。一部の人々はあなたが気にしないでオブジェクトを再利用するべきではないと見ていますが、コンパイラはそれを禁止しません。

これについてのブログ投稿(私ではない)が1つあり、コメントに興味深い議論があります。

https://foonathan.github.io/blog/2016/07/23/move-safety.html

およびフォローアップ:

https://foonathan.github.io/blog/2016/08/24/move-default-ctor.html

そして、これについて議論している議論を伴うこのトピックについての最近のビデオチャットもここにあります:

https://www.youtube.com/watch?v=g5RnXRGar1w

基本的には、移動元のオブジェクトを削除されたポインタのように扱うか、安全に「移動元の状態」にすることです。

2
Hayt

ソースオブジェクトは右辺値であり、場合によってはx値です。したがって、注意する必要があるのは、そのオブジェクトの差し迫った破壊です。

リソースハンドルまたはポインタは、移動とコピーを区別する最も重要な項目です。操作後、そのハンドルの所有権が譲渡されたと見なされます。

明らかに、質問で述べたように、移動中にソースオブジェクトに影響を与えて、転送されたオブジェクトの所有者として自分自身を識別しなくなるようにする必要があります。

Thing::Thing(Thing&& rhs)
     : unqptr_(move(rhs.unqptr_))
     , rawptr_(rhs.rawptr_)
     , ownsraw_(rhs.ownsraw_)
{
    the.ownsraw_ = false;
}

rawptr_をクリアしないことに注意してください。

ここでは、所有権フラグが1つのオブジェクトに対してのみ真である限り、ダングリングポインタが存在するかどうかは関係ないという設計上の決定が行われます。

ただし、別のエンジニアが、ランダムなubの代わりに、次の間違いがnullptrアクセス​​になるように、ポインターをクリアする必要があると判断する場合があります。

void MyClass::f(Thing&& t) {
    t_ = move(t);
    // ...
    cout << t;
}

質問に示されている変数のように無害なものの場合、それらをクリアする必要はないかもしれませんが、それはクラスの設計に依存します。

考えてみましょう:

class Motorcycle
{
    float speed_ = 0.;
    static size_t s_inMotion = 0;
public:
    Motorcycle() = default;
    Motorcycle(Motorcycle&& rhs);
    Motorcycle& operator=(Motorcycle&& rhs);

    void setSpeed(float speed)
    {
        if (speed && !speed_)
            s_inMotion++;
        else if (!speed && speed_)
            s_inMotion--;
        speed_ = speed;
    }

    ~Motorcycle()
    {
        setSpeed(0);
    }
};

これは、所有権が必ずしも単純なポインタやブール値ではないことを示すかなり人工的なデモンストレーションですが、内部の一貫性の問題になる可能性があります。

ムーブ演算子は、setSpeedを使用して自分自身にデータを入力することも、単に実行することもできます。

Motorcycle::Motorcycle(Motorcycle&& rhs)
    : speed_(rhs.speed_)
{
    rhs.speed_ = 0;  // without this, s_inMotion gets confused
}

(タイプミスやオートコレクトについてお詫びします。電話で入力しました)

1
kfsone