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:
- Compactness. Error and result are mutually exclusive, so they should be in a union.
- No runtime overhead. Virtual functions are excluded (plus, holding vtable pointer conflicts with goal 1). RTTI also excluded.
- Uniformity. The common fields of different
Result
types should be acessible via homogenous pointers or wrappers. For example: if instead ofResult<T>
we were talking aboutFuture<T>
, it should be possible to dowhenAny(FutureBase& a, FutureBase& b)
regardless ofa
/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.