I have a Result<T>
template class that holds a union of some error_type
and T
. I would like to expose the common part (the error) in a base class without resorting to virtual functions.
Here is my attempt:
using error_type = std::exception_ptr;
struct ResultBase
{
error_type error() const
{
return *reinterpret_cast<const error_type*>(this);
}
protected:
ResultBase() { }
};
template <class T>
struct Result : ResultBase
{
Result() { new (&mError) error_type(); }
~Result() { mError.~error_type(); }
void setError(error_type error) { mError = error; }
private:
union { error_type mError; T mValue; };
};
static_assert(std::is_standard_layout<Result<int>>::value, "");
void check(bool condition) { if (!condition) std::terminate(); }
void f(const ResultBase& alias, Result<int>& r)
{
r.setError(std::make_exception_ptr(std::runtime_error("!")));
check(alias.error() != nullptr);
r.setError(std::exception_ptr());
check(alias.error() == nullptr);
}
int main()
{
Result<int> r;
f(r, r);
}
(This is stripped down, see extended version if unclear).
The base class takes advantage of standard-layout to find the address of the error field at offset zero. Then it casts the pointer to error_type
(assuming this really is the current dynamic type of the union).
Am I right to assume this is portable? Or is it breaking some pointer aliasing rule?
EDIT: My question was 'is this portable', but many commenters are puzzled by the use of inheritance here, so I will clarify.
First, this is a toy example. Please don't take it too literally or assume there is no use for the base class.
The design has three goals:
Result
types should be acessible via homogenous pointers or wrappers. For example: if instead of Result<T>
we were talking about Future<T>
, it should be possible to do whenAny(FutureBase& a, FutureBase& b)
regardless of a
/ b
concrete type.If willing to sacrifice (1), this becomes trivial. Something like:
struct ResultBase
{
error_type mError;
};
template <class T>
struct Result : ResultBase
{
std::aligned_storage_t<sizeof(T), alignof(T)> mValue;
};
If instead of goal (1) we sacrifice (2), it might look like this:
struct ResultBase
{
virtual error_type error() const = 0;
};
template <class T>
struct Result : ResultBase
{
error_type error() const override { ... }
union { error_type mError; T mValue; };
};
Again, the justification is not relevant. I just want to make sure original sample is conformant C++11 code.
Here is my own attempt at an answer focusing strictly on portability.
Standard-layout is defined in §9.1[class.name]/7:
A standard-layout class is a class that:
- has no non-static data members of type non-standard-layout class (or array of such types) or reference,
- has no virtual functions (10.3) and no virtual base classes (10.1),
- has the same access control (Clause 11) for all non-static data members,
- has no non-standard-layout base classes,
- either has no non-static data members in the most derived class and at most one base class with non-static data members, or has no base classes with non-static data members, and
- has no base classes of the same type as the first non-static data member.
By this definition Result<T>
is standard-layout provided that:
error_type
and T
are standard-layout. Note that this is not guaranteed for std::exception_ptr
, though likely in practice.T
is not ResultBase
.§9.2[class.mem]/20 states that:
A pointer to a standard-layout struct object, suitably converted using a reinterpret_cast, points to its initial member (or if that member is a bit-field, then to the unit in which it resides) and vice versa. [ Note: There might therefore be unnamed padding within a standard-layout struct object, but not at its beginning, as necessary to achieve appropriate alignment. —end note ]
This implies that empty base class optimization is mandatory for standard-layout types. Assuming Result<T>
does have standard-layout, this
in ResultBase
is guaranteed to point at the first field in Result<T>
.
9.5[class.union]/1 states:
In a union, at most one of the non-static data members can be active at any time, that is, the value of at most one of the non-static data members can be stored in a union at any time. [...] Each non-static data member is allocated as if it were the sole member of a struct.
And additionaly §3.10[basic.lval]/10:
If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined
- the dynamic type of the object,
- a cv-qualified version of the dynamic type of the object,
- a type similar (as defined in 4.4) to the dynamic type of the object,
- a type that is the signed or unsigned type corresponding to the dynamic type of the object,
- a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object,
- an aggregate or union type that includes one of the aforementioned types among its elements or nonstatic data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
- a type that is a (possibly cv-qualified) base class type of the dynamic type of the object,
- a char or unsigned char type.
This guarantees reinterpret_cast<const error_type*>(this)
will yield a valid pointer to the mError
field.
All controversy aside, this technique looks portable. Just keep formal limitations in mind: error_type
and T
must be standard-layout, and T
may not be type ResultBase
.
Side note: On most compilers (at least GCC, Clang and MSVC) non-standard-layout types will work as well. As long as Result<T>
has predictable layout, error and result types are irrelevant.