【问题标题】:C++11 anonymous union with non-trivial members具有重要成员的 C++11 匿名联合
【发布时间】:2012-05-28 11:09:24
【问题描述】:

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

struct Value {
  uint64_t lastUpdated;

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

仅仅向联合中添加一个 std::string 成员当然会导致编译错误,因为通常需要添加对象的非平凡构造函数。 In the case of std::string (text from informit.com)

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

然后网站继续给出如下示例代码:

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 是如何工作的。

我可以请人向我解释清楚一点吗?

【问题讨论】:

  • 在我看来你真正需要的是一个合适的variant...
  • 我已经有一个在别处使用的变体类。在这种情况下我没有使用它,因为这是用于网络上的序列化数据,我想保持数据很小,所以 Variant 保持内部的所有内容(如键入信息)到我想保持外部并决定的类什么是基于数据包模式的。

标签: c++ constructor c++11 anonymous unions


【解决方案1】:

这里不需要放置新的。

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

此行为在第 9.5 节中描述。 [class.union]:

union-like 类是一个联合或具有匿名联合作为直接成员的类。类似联合的类X 有一组变体成员。如果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)
{
}

当然,当激活一个变量成员而不是在构造函数中初始化的另一个成员时,仍然应该使用placement new。

【讨论】:

  • 如果定义了Foo 构造函数但没有在这些成员变量中选择一个(即Foo() { } 而不是Foo() : p() { }),会发生什么? GCC 5.1 和 Clang 3.6 编译构造函数时没有任何警告或错误:melpon.org/wandbox/permlink/gLkD49UOrrGhFUJc 但是,尚不清楚标准对这种情况的描述。
  • @dkim:我很确定所有变体成员都处于相同状态,特别是获得了存储但未执行初始化。第 3.8 节(对象生命周期)规定了在这种状态下允许对成员进行的操作。
【解决方案2】:

new (&amp;p) Point() 示例是对标准放置 new 运算符的调用(通过放置新表达式),因此您需要包含 &lt;new&gt;。该特定运算符的特殊之处在于它分配内存,它只返回您传递给它的内容(在本例中为&amp;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();

您应该在何时何地在Value 类中构造和销毁std::string 成员(我们称之为s)取决于您对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。这样做的原因是,如果不知道哪个是活跃的(如果有的话),我们就无法复制或移动工会的适当活跃成员。

【讨论】:

  • "// 需要绕过语言的一个怪癖" - 实际上,这是错误的 - 你可以直接调用析构函数,如果你做对了(需要正确解析范围):p-&gt;std::string::~string();。不过,更具可读性?好吧,当然看起来更复杂,但使用了众所周知的数据类型,而上面的解决方案更紧凑(除了using 的附加代码行),但引入了一个鲜为人知的别名。当然是个人喜好问题(至于我自己,我会投票给众所周知的数据类型......)。
猜你喜欢
  • 2019-10-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多