Alexander
Alexander

Reputation: 1098

How to check if union contains type (using type_traits)?

I have a function template like the following:

template<class U, class T>
T* unsafeCast(U* theUnion) {
    reinterpret_cast<T*>(theUnion);
}

How can I make sure this only compiles if T is a type contained within the union U, so that the following holds?

union FooUnion {
    int a;
    double b;
} foo;

unsafeCast<FooUnion, int>(&foo); // compiles
unsafeCast<FooUnion, double>(&foo); // compiles
unsafeCast<FooUnion, char>(&foo); // does not compile

I understand that is_union from <type_traits> allows to check for a union, but how can I check for types within a union?

Upvotes: 1

Views: 486

Answers (2)

patstew
patstew

Reputation: 2001

If you can add constructors to your unions you can do it like this:

#include <type_traits>

// Use std::void_t to only allow types that the union can be constructed from
template<class T, class U, class = std::void_t<decltype(U{std::declval<T>()})>>
T* unsafeCast(U* theUnion) {
    return reinterpret_cast<T*>(theUnion);
}

union FooUnion {
    int a;
    double b;
    // Explictly add constructors for each desired type
    FooUnion(int a) : a{a} {}
    FooUnion(double b) : b{b} {}
    // Add a deleted catch all to prevent implicit conversions, e.g. from char
    template <class T>
    FooUnion(T) = delete;
} foo(0);

int main() {
    unsafeCast<int>(&foo); // compiles
    unsafeCast<double>(&foo); // compiles
    unsafeCast<char>(&foo); // does not compile
}

Godbolt

Upvotes: 0

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275820

You cannot.

boost::variant and std::variant are solutions to this problem such that the union carries with it the type information you need.

You could create a raw union like this:

template<class T>
struct data_holder {
  T data;
};
template<class...Ts>
struct union_data;
template<>
struct union_data<>{};
template<class T0>
struct union_data<T0>:data_holder<T0> {};
template<class T0, class...Ts>
struct union_data<T0, Ts...> {
  union {
    union_data<T0> lhs;
    union_data<Ts...> rhs;
  };
};
template<class...Ts>
struct raw_union:union_data<Ts...>{
  template<class T>
  constexpr static bool valid_type() {
    return (std::is_same<T, Ts>{}||...); // rewrite in C++14/11
  }
  template<class T>
  union_data<T>* get_data_ptr() {
    static_assert( valid_type<T>() );
    return reinterpret_cast<union_data<T>*>(this);
  }
  template<class T>
  union_data<T> const* get_data_ptr() const{
    static_assert( valid_type<T>() );
    return reinterpret_cast<union_data<T> const*>(this);
  }
  template<class T>
  T& get_unsafe() {
    return get_data_ptr<T>()->data;
  }
  template<class T>
  T const& get_unsafe() const {
    return get_data_ptr<T>()->data;
  }
  template<class T, class...Us>
  T& emplace( Us&&... us ) {
    auto* ptr = ::new( (void*)get_data_ptr<T>() ) union_data<T>{ T(std::forward<Us>(us)...) };
    return ptr->data;
  }
  template<class T>
  void dtor() {
    get_data_ptr<T>()->~T();
  }
};

which is unsafe and undiscriminated, but does check if foo.get_unsafe<int>() actually contains an int.

live example.

Use:

raw_union<int, double> un;
un.emplace<int>(7);
std::cout << un.get_unsafe<int>() << "\n";

it does not support multiple union members of the same type. You are in charge of calling .emplace<T>(x) before using T, and if a non-trivial destructor .dtor<T>().

Accessing members that are not active is just as perilous as doing so with raw C/C++ unions.

Upvotes: 1

Related Questions