C++11 anonymous union with non-trivial members

感情迁移 提交于 2019-11-27 07:06:30
Ben Voigt

There is no need for placement new here.

Variant members won't be initialized by the compiler-generated constructor, but there should be no trouble picking one and initializing it using the normal ctor-initializer-list. Members declared inside anonymous unions are actually members of the containing class, and can be initialized in the containing class's constructor.

This behavior is described in section 9.5. [class.union]:

A union-like class is a union or a class that has an anonymous union as a direct member. A union-like class X has a set of variant members. If X is a union its variant members are the non-static data members; otherwise, its variant members are the non-static data members of all anonymous unions that are members of X.

and in section 12.6.2 [class.base.init]:

A ctor-initializer may initialize a variant member of the constructor’s class. If a ctor-initializer specifies more than one mem-initializer for the same member or for the same base class, the ctor-initializer is ill-formed.

So the code can be simply:

#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)
{
}

Of course, placement new should still be used when vivifying a variant member other than the other initialized in the constructor.

Luc Danton

That new (&p) Point() example is a call to the Standard placement new operator (via a placement new expression), hence why you need to include <new>. That particular operator is special in that it does not allocate memory, it only returns what you passed to it (in this case it's the &p parameter). The net result of the expression is that an object has been constructed.

If you combine this syntax with explicit destructor calls then you can achieve complete control over the lifetime of an object:

// 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();

When and where you should construct and destroy the std::string member (let's call it s) in your Value class depends on your usage pattern for s. In this minimal example you never construct (and hence destruct) it in the special members:

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;
    };
};

The following is thus a valid use of Value:

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

As you may have noticed, I disabled copying and moving Value. The reason for that is that we can't copy or move the appropriate active member of the union without knowing which one it is that is active, if any.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!