C++ 11与非平凡成员的匿名联合

Omn*_*ity 27 c++ constructor anonymous unions c++11

我正在更新我的结构,我想要添加一个std :: string成员.原始结构如下所示:

struct Value {
  uint64_t lastUpdated;

  union {
    uint64_t ui;
    int64_t i;
    float f;
    bool b;
  };
};
Run Code Online (Sandbox Code Playgroud)

当然,只是将一个std :: string成员添加到union会导致编译错误,因为通常需要添加该对象的非平凡构造函数. 在std :: string的情况下(来自informit.com的文本)

由于std :: string定义了所有六个特殊成员函数,因此U将具有隐式删除的默认构造函数,复制构造函数,复制赋值运算符,移动构造函数,移动赋值运算符和析构函数.实际上,这意味着除非您明确定义某些或所有特殊成员函数,否则无法创建U的实例.

然后该网站继续提供以下示例代码:

union U
{
int a;
int b;
string s;
U();
~U();
};
Run Code Online (Sandbox Code Playgroud)

但是,我在结构中使用匿名联合.我在freenode上问了## C++,他们告诉我这样做的正确方法是将构造函数放在struct中,并给我这个示例代码:

#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)
{
}
Run Code Online (Sandbox Code Playgroud)

但是从那里我无法想象如何制作std :: string需要定义的其他特殊函数,而且,我不完全清楚该示例中的ctor是如何工作的.

我可以让某人向我解释一下这一点吗?

Ben*_*igt 23

这里没有必要放置新的.

变量成员不会被编译器生成的构造函数初始化,但是选择一个并使用普通的ctor-initializer-list初始化它应该没有问题.在匿名联合内部声明的成员实际上是包含类的成员,并且可以在包含类的构造函数中初始化.

9.5节描述了这种行为.[class.union]:

工会状类是联合或具有匿名联合作为直接成员的类.类似联合的类X具有一组变体成员.如果X是联合,其变体成员是非静态数据成员; 否则,其变体成员是作为其成员的所有匿名联合的非静态数据成员X.

在第12.6.2节中[class.base.init]:

一个构造函数初始化值可以初始化构造函数的类的变体成员.如果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)
{
}
Run Code Online (Sandbox Code Playgroud)

当然,在构建器中初始化的另一个变体成员之外,仍然可以使用placement new.


Luc*_*ton 14

new (&p) Point()示例是对标准放置new运算符的调用(通过放置新表达式),因此您需要包含<new>.这个特定的运营商是特殊的,它并没有分配内存,它只返回你传递什么给它(在这种情况下,它的&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();
Run Code Online (Sandbox Code Playgroud)

您应该在何时何地构建和销毁您的类中的std::string成员(让我们称之为s)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;
    };
};
Run Code Online (Sandbox Code Playgroud)

因此以下是有效使用Value:

Value v;
new (&v.s) std::string("foo");
something_taking_a_string(v.s);
using string_type = std::string;
v.s.~string_type();
Run Code Online (Sandbox Code Playgroud)

您可能已经注意到,我禁用了复制和移动Value.这样做的原因是我们不能复制或移动联盟的相应活动成员,而不知道它是哪一个是活动的,如果有的话.

  • *"//需要绕过语言的怪癖"* - 实际上,这是错误的 - 你*可以*直接调用析构函数,如果你做得正确(需要正确解析范围):`p-> std: :串::〜串();`.但是更具可读性?嗯,当然看起来更复杂,但使用一个众所周知的数据类型,而上面的解决方案更紧凑(除了'using`的附加代码行),但引入了一个鲜为人知的别名.当然是个人品味的问题(关于我自己,我会投票选出众所周知的数据类型......). (6认同)