Search code examples
c++c++11strict-aliasing

Accessing common part of an union from base class


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:

  1. Compactness. Error and result are mutually exclusive, so they should be in a union.
  2. No runtime overhead. Virtual functions are excluded (plus, holding vtable pointer conflicts with goal 1). RTTI also excluded.
  3. Uniformity. The common fields of different 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.


Solution

  • 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:

    • Both 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.