I'd like to use templates to simplify the construction of unions with non-trivial types. The following seems to "work" in practice, but is technically not legal by the spec:
template<typename T> struct union_entry {
void (*destructor_)(void *); // how to destroy type T when active
T value_;
};
union U {
union_entry<A> a;
union_entry<B> b;
// ... some constructor and destructor...
};
The problem is that (according to N4141) you can access a common initial sequence of two structures in a union (i.e., the destructor_
field) only if both structures are standard-layout types--at least according to a non-normative note in 9.5.1. According to 9.0.7, a standard-layout type can't have any non-static data members with non-standard layout. So if either A or B is not standard-layout, then it becomes illegal to access destructor_
in the wrong union.
A loophole would seem to be to make union_entry
standard layout by turning value_
in to an alignas(T) char[sizeof(T)]
. Nothing in 9.0.7 appears to rule out the use of the alignas
. Thus, my question: Is the following a standard-layout type for any type T
? And hence can value_
to be cast to T&
to emulate the previous example, while still allowing destructor_
to be used in a non-active union_entry
?
template<typename T> struct union_entry {
void (*destructor_)(void *);
alignas(T) char value_[sizeof(T)];
}
In both clang-3.8.1 and g++-6.2.1, std::is_standard_layout
suggests union_entry<T>
is standard layout even when T
is not. Here's a complete working example of how I would like to use this technique:
#include <cassert>
#include <iostream>
#include <new>
#include <string>
using namespace std;
template<typename T> struct union_entry {
void (*destructor_)(void *);
alignas(T) char value_[sizeof(T)];
union_entry() : destructor_(nullptr) {}
~union_entry() {} // Just to cause error in unions w/o destructors
void select() {
if (destructor_)
destructor_(this);
destructor_ = destroy_helper;
new (static_cast<void *>(value_)) T{};
}
T &get() {
assert(destructor_ == destroy_helper);
return *reinterpret_cast<T *>(value_);
}
private:
static void destroy_helper(void *_p) {
union_entry *p = static_cast<union_entry *>(_p);
p->get().~T();
p->destructor_ = nullptr;
}
};
union U {
union_entry<int> i;
union_entry<string> s;
U() : i() {}
~U() { if (i.destructor_) i.destructor_(this); }
};
int
main()
{
U u;
u.i.select();
u.i.get() = 5;
cout << u.i.get() << endl;
u.s.select();
u.s.get() = "hello";
cout << u.s.get() << endl;
// Notice that the string in u.s is destroyed by calling
// u.i.destructor_, not u.s.destructor_
}
Thanks to @Arvid, who pointed me to std::aligned_storage
, I believe there is a definitive (albeit non-normative) answer in section 20.10.7.6 of the standard (which I assume is the same as N4141).
First, Table 57 says of aligned_storage
"The member typedef type
shall be a POD type...", where 9.0.10 makes clear "A POD struct is a non-union class that is both a trivial class and a standard-layout class."
Next, 20.10.7.6.1 gives a non-normative example implementation:
template <std::size_t Len, std::size_t Alignment>
struct aligned_storage {
typedef struct {
alignas(Alignment) unsigned char __data[Len];
} type;
};
So clearly the use of alignas
does not prevent a type from being standard-layout.