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.
Either Error Int
, just with more than two possibilities, right? Like "a value is either an error, an int, a string or a MyObject"? – Blondytemplate<class T> struct Errorneous { union { error_type error; T value;} data; bool is_error; };
? Why use inheritance here? – BlondyFuture<R>
and I would like to supportwhenAny(Future<T1>&, Future<T2>&, ...)
. – GregwhenAny
example makes more sense. When the actual result needs to be extracted,T
is known so we just read it via derived class. Regardingboost::any
- I can't use type erasure that relies on virtual methods. – GregT
value? – FishermanwhenAny(FutureBase&, FutureBase& ...)
, and have already detailed the implementation goals. This is not productive. – Gregtemplate <class ... Ts> whenAny(Ts ... ts)
. You will be able to do whatever you need to do faster if you keep the types around. You started off with a whenAny more like that (earlier in comments), and changed to type erased version. It seems like you are insisting on an implementation independent of whether an alternative meets your goals. – OrecticwhenAny
would work only if the arg count is known at compile time. I'm not sure about the performance benefit (strict aliasing rules might help), though it will affect compile time and binary size. More importantly, what to do when the item count is determined at run-time?whenAny(std::vector<FutureBase*>)
works for me. Without a base class, you'd need a wrapper for type erasure and this normally relies on dynamic dispatch. I'm not insisting on a particular implementation, just pointing out that none of the alternatives meet all 3 goals. – Gregstd::exception_ptr
is already using type erasure so you ends up using type erasure twice, while using your own type erasure allows you yo use type erasure just once. See my answer, also the answer from PiotrNycz is very good. @ValentinMilea – LollopmHasError
in your complete version, it sounds like you should move the discriminator (mHasError
) to the base class, and use something similar toResult<error_type>
when you have an error. – Deakinstd::vector<FutureBase*>
this type, like any C++ type, hassizeof
determined at compile time. However it is not guaranteed to work as a part of a union. – EntwistleResult<Thing>
holds eitherThing
orerror_type
depending on run-time conditions. So it doesn't make sense in my case to haveResult<error_type>
. – Gregerror()
(and has_error()` in the "real" code, both incorrectly claim there's an error when there actually isn't, for exactly this issue. You must use virtual functions, or additional space, in order to tell if there's an error or not. Or rely on undefined behavior and make assumptions about the byte patterns ofT
vsstd::exception_ptr
. Those are the only solutions. – Impi