web-dev-qa-db-ja.com

自明ではないメンバーを持つC ++ 11匿名ユニオン

私は私の構造体を更新していて、それにstd :: stringメンバーを追加したいと思っていました。元の構造体は次のようになります。

struct Value {
  uint64_t lastUpdated;

  union {
    uint64_t ui;
    int64_t i;
    float f;
    bool b;
  };
};

もちろん、共用体にstd :: stringメンバーを追加するだけでは、通常、オブジェクトの自明でないコンストラクタを追加する必要があるため、コンパイルエラーが発生します。 std :: stringの場合(informit.comからのテキスト)

Std :: stringは6つの特別なメンバー関数のすべてを定義するため、Uには、暗黙的に削除されたデフォルトコンストラクター、コピーコンストラクター、コピー割り当て演算子、移動コンストラクター、移動割り当て演算子およびデストラクタがあります。事実上、これは、特別なメンバー関数の一部またはすべてを明示的に定義しない限り、Uのインスタンスを作成できないことを意味します。

次に、Webサイトは次のサンプルコードを提供します。

union U
{
int a;
int b;
string s;
U();
~U();
};

ただし、私は構造体内で匿名の共用体を使用しています。 freenodeで## C++を尋ねると、正しい方法はコンストラクタを構造体に置くことであり、次のコード例を私に教えてくれました:

#include <new>

struct Point  {
    Point() {}
    Point(int x, int y): x_(x), y_(y) {}
    int x_, y_;
};

struct Foo
{
  Foo() { new(&p) Point(); }
  union {
    int z;
    double w;
    Point p;
  };
};

int main(void)
{
}

しかし、そこからstd :: stringが定義する必要がある残りの特別な関数を作成する方法を理解できません。さらに、その例のctorがどのように機能しているかは完全にはわかりません。

これについて誰かにもう少し明確に説明してもらえますか?

28

ここに新しい配置をする必要はありません。

バリアントメンバーは、コンパイラーによって生成されたコンストラクターによって初期化されませんが、通常のctor-initializer-listを使用して、メンバーを選択して初期化するのに問題はありません。匿名ユニオン内で宣言されたメンバーは、実際には包含クラスのメンバーであり、包含クラスのコンストラクターで初期化できます。

この動作はセクション9.5で説明されています。 [class.union]

nion-likeクラスは、共用体または直接のメンバーとして匿名の共用体を持つクラスです。 unionに似たクラスXにはvariant membersのセットがあります。 Xが共用体の場合、そのバリアントメンバーは非静的データメンバーです。それ以外の場合、そのバリアントメンバーは、Xのメンバーであるすべての匿名ユニオンの非静的データメンバーです。

およびセクション12.6.2で[class.base.init]

ctor-initializerは、コンストラクターのクラスのバリアントメンバーを初期化する場合があります。 ctor-initializerが同じメンバーまたは同じ基本クラスに対して複数のmem-initializerを指定している場合、ctor-initializerは無効です-形成。

したがって、コードは次のようになります。

#include <new>

struct Point  {
    Point() {}
    Point(int x, int y): x_(x), y_(y) {}
    int x_, y_;
};

struct Foo
{
  Foo() : p() {} // usual everyday initialization in the ctor-initializer
  union {
    int z;
    double w;
    Point p;
  };
};

int main(void)
{
}

もちろん、コンストラクターで初期化された他のメンバー以外のバリアントメンバーを有効にする場合は、新しい配置を使用する必要があります。

22
Ben Voigt

そのnew (&p) Point()の例は、標準の配置new演算子の呼び出し(配置の新しい式による)です。そのため、<new>を含める必要があります。その特定の演算子は、それがnotメモリを割り当てるという点で特別です。渡されたものだけを返します(この場合は&pパラメータです)。式の最終結果は、オブジェクトが構築されたことです。

この構文を明示的なデストラクタ呼び出しと組み合わせると、オブジェクトの存続期間を完全に制御できます。

// Let's assume storage_type is a type
// that is appropriate for our purposes
storage_type storage;

std::string* p = new (&storage) std::string;
// p now points to an std::string that resides in our storage
// it was default constructed

// *p can now be used like any other string
*p = "foo";

// Needed to get around a quirk of the language
using string_type = std::string;

// We now explicitly destroy it:
p->~string_type();
// Not possible:
// p->~std::string();

// This did nothing to our storage however
// We can even reuse it
p = new (&storage) std::string("foo");

// Let's not forget to destroy our newest object
p->~string_type();

sクラスのstd::stringメンバー(Valueと呼びましょう)をいつ、どこで構築および破棄するかは、sの使用パターンによって異なります。この最小限の例では、特別なメンバーでそれを構築(したがって破棄)することはありません。

struct Value {
    Value() {}

    Value(Value const&) = delete;
    Value& operator=(Value const&) = delete;

    Value(Value&&) = delete;
    Value& operator=(Value&&) = delete;

    ~Value() {}

    uint64_t lastUpdated;

    union {
        uint64_t ui;
        int64_t i;
        float f;
        bool b;
        std::string s;
    };
};

したがって、以下はValueの有効な使用法です。

Value v;
new (&v.s) std::string("foo");
something_taking_a_string(v.s);
using string_type = std::string;
v.s.~string_type();

お気づきかもしれませんが、私はValueのコピーと移動を無効にしました。その理由は、もしあれば、どれがアクティブであるかを知らなければ、ユニオンの適切なアクティブメンバーをコピーまたは移動できないからです。

14
Luc Danton