6
\$\begingroup\$

What do you think about this variant class?

Some hype: no RTTI needed, no heap allocation, supports copying.

#pragma once
#ifndef VARIANT_HPP
# define VARIANT_HPP

#include <cassert>

#include <ostream>

#include <type_traits>

#include <typeinfo>

#include <utility>

namespace detail
{

template <typename A, typename ...B>
struct max_align_type
{
  using type = typename ::std::conditional<
    (alignof(A) > alignof(typename max_align_type<B...>::type)),
    A,
    typename max_align_type<B...>::type
  >::type;
};

template <typename A, typename B>
struct max_align_type<A, B>
{
  using type = typename ::std::conditional<
    (alignof(A) > alignof(B)), A, B>::type;
};

template <typename A>
struct max_align_type<A>
{
  using type = A;
};

template <typename A, typename ...B>
struct max_size_type
{
  using type = typename ::std::conditional<
    (sizeof(A) > sizeof(typename max_size_type<B...>::type)),
    A,
    typename max_size_type<B...>::type
  >::type;
};

template <typename A, typename B>
struct max_size_type<A, B>
{
  using type = typename ::std::conditional<
    (sizeof(A) > sizeof(B)), A, B>::type;
};

template <typename A>
struct max_size_type<A>
{
  using type = A;
};

template <typename A, typename B, typename... C>
struct index_of :
  ::std::integral_constant<int,
    ::std::is_same<A, B>{} ?
    0 :
    (-1 == index_of<A, C...>{}) ? -1 : 1 + index_of<A, C...>{}
  >
{
};

template <typename A, typename B>
struct index_of<A, B> :
  ::std::integral_constant<int, ::std::is_same<A, B>{} - 1>
{
};

template <typename A, typename... B>
struct has_duplicates :
  ::std::integral_constant<bool,
    (-1 == index_of<A, B...>{} ? has_duplicates<B...>{} : true)
  >
{
};

template <typename A>
struct has_duplicates<A> :
  ::std::integral_constant<bool, false>
{
};

template <typename A, typename B, typename... C>
struct compatible_index_of :
  ::std::integral_constant<int,
    ::std::is_constructible<A, B>{} ?
      0 :
      (-1 == compatible_index_of<A, C...>{}) ?
        -1 :
        1 + compatible_index_of<A, C...>{}
  >
{
};

template <typename A, typename B>
struct compatible_index_of<A, B> :
  ::std::integral_constant<int, ::std::is_constructible<A, B>{} - 1>
{
};

template <typename A, typename B, typename... C>
struct compatible_type
{
  using type = typename ::std::conditional<
      ::std::is_constructible<A, B>{},
      B,
      typename compatible_type<A, C...
    >::type
  >::type;
};

template <typename A, typename B>
struct compatible_type<A, B>
{
  using type = typename ::std::conditional<
    ::std::is_constructible<A, B>{}, B, void>::type;
};

template <class S, class C, typename = void>
struct is_streamable : ::std::false_type { };

template <class S, class C>
struct is_streamable<S,
  C,
  decltype(void(sizeof(decltype(::std::declval<S&>()
    << ::std::declval<C const&>()))))
> : ::std::true_type
{
};

template < ::std::size_t I, typename A, typename ...B>
struct type_at : type_at<I - 1, B...>
{
};

template <typename A, typename ...B>
struct type_at<0, A, B...>
{
  using type = A;
};

template <bool B>
using bool_constant = ::std::integral_constant<bool, B>;

template <class A, class ...B>
struct all_of : bool_constant<A::value && all_of<B...>::value>
{
};

template <class A>
struct all_of<A> : bool_constant<A::value>
{
};

template <class A, class ...B>
struct any_of : bool_constant<A::value || any_of<B...>::value>
{
};

template <class A>
struct any_of<A> : bool_constant<A::value>
{
};

template <class A>
struct is_move_or_copy_constructible :
  bool_constant< ::std::is_copy_constructible<A>{} ||
    ::std::is_move_constructible<A>{}>
{
};

}

#ifdef __GNUC__
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wstrict-aliasing"
#endif // __GNUC__

template <typename... T>
struct variant
{
  static_assert(!::detail::any_of< ::std::is_reference<T>...>{},
    "reference types are unsupported");
  static_assert(!::detail::any_of< ::std::is_void<T>...>{},
    "void type is unsupported");
  static_assert(::detail::all_of<
    ::detail::is_move_or_copy_constructible<T>...>{},
    "unmovable and uncopyable types are unsupported");
  static_assert(!::detail::has_duplicates<T...>{},
    "duplicate types are unsupported");

  using max_align_type = typename ::detail::max_align_type<T...>::type;

  using max_size_type = typename ::detail::max_size_type<T...>::type;

  static constexpr auto const max_align = alignof(max_align_type);

  variant() = default;

  ~variant()
  {
    if (*this)
    {
      deleter_(store_);
    }
    // else do nothing
  }

  variant(variant const& other) { *this = other; }

  variant(variant&& other) { *this = ::std::move(other); }

  variant& operator=(variant const& rhs)
  {
    if (!rhs)
    {
      if (*this)
      {
        store_type_ = -1;

        deleter_(store_);
      }
      // else do nothing
    }
    else if (rhs.copier_)
    {
      rhs.copier_(*this, rhs);
    }
    else
    {
      throw ::std::bad_typeid();
    }

    return *this;
  }

  variant& operator=(variant&& rhs)
  {
    if (!rhs)
    {
      if (*this)
      {
        store_type_ = -1;

        deleter_(store_);
      }
      // else do nothing
    }
    else if (rhs.mover_)
    {
      rhs.mover_(*this, ::std::move(rhs));
    }
    else
    {
      throw ::std::bad_typeid();
    }

    return *this;
  }

  template <
    typename U,
    typename = typename ::std::enable_if< ::detail::any_of< ::std::is_same<
      typename ::std::remove_reference<U>::type, T>...>{} &&
      !::std::is_same<typename ::std::decay<U>::type, variant>{}
    >::type
  >
  variant(U&& f)
  {
    *this = ::std::forward<U>(f);
  }

  template <typename S = ::std::ostream, typename U>
  typename ::std::enable_if< ::detail::any_of< ::std::is_same<
    typename ::std::remove_reference<U>::type, T>...>{} &&
    !::std::is_rvalue_reference<U&&>{} &&
    ::std::is_copy_assignable<typename ::std::remove_reference<U>::type>{} &&
    !::std::is_same<typename ::std::decay<U>::type, variant>{},
    variant&
  >::type
  operator=(U&& f)
  {
    using user_type = typename ::std::remove_reference<U>::type;

    if (::detail::index_of<user_type, T...>{} == store_type_)
    {
      *static_cast<user_type*>(static_cast<void*>(store_)) = f;
    }
    else
    {
      if (*this)
      {
        deleter_(store_);
      }
      // else do nothing

      new (store_) user_type(::std::forward<U>(f));

      deleter_ = destructor_stub<user_type>;

      copier_ = get_copier<user_type>();

      mover_ = get_mover<user_type>();

      streamer_ = get_streamer<S, user_type>();

      store_type_ = ::detail::index_of<user_type, T...>{};
    }

    return *this;
  }

  template <typename S = ::std::ostream, typename U>
  typename ::std::enable_if<
    ::detail::any_of< ::std::is_same<
      typename ::std::remove_reference<U>::type, T>...>{} &&
    ::std::is_rvalue_reference<U&&>{} &&
    ::std::is_move_assignable<typename ::std::remove_reference<U>::type>{} &&
    !::std::is_same<typename ::std::decay<U>::type, variant>{},
    variant&
  >::type
  operator=(U&& f)
  {
    using user_type = typename ::std::remove_reference<U>::type;

    if (::detail::index_of<user_type, T...>{} == store_type_)
    {
      *static_cast<user_type*>(static_cast<void*>(store_)) = ::std::move(f);
    }
    else
    {
      if (*this)
      {
        deleter_(store_);
      }
      // else do nothing

      new (store_) user_type(::std::forward<U>(f));

      deleter_ = destructor_stub<user_type>;

      copier_ = get_copier<user_type>();

      mover_ = get_mover<user_type>();

      streamer_ = get_streamer<S, user_type>();

      store_type_ = ::detail::index_of<user_type, T...>{};
    }

    return *this;
  }

  template <typename S = ::std::ostream, typename U>
  typename ::std::enable_if<
    ::detail::any_of< ::std::is_same<
      typename ::std::remove_reference<U>::type, T>...>{} &&
    !::std::is_copy_assignable<
      typename ::std::remove_reference<U>::type>{} &&
    !::std::is_move_assignable<
      typename ::std::remove_reference<U>::type>{} &&
    !::std::is_same<typename ::std::decay<U>::type, variant>{},
    variant&
  >::type
  operator=(U&& f)
  {
    using user_type = typename ::std::remove_reference<U>::type;

    if (*this)
    {
      deleter_(store_);
    }
    // else do nothing

    new (store_) user_type(::std::forward<U>(f));

    deleter_ = destructor_stub<user_type>;

    copier_ = get_copier<user_type>();

    mover_ = get_mover<user_type>();

    streamer_ = get_streamer<S, user_type>();

    store_type_ = ::detail::index_of<user_type, T...>{};

    return *this;
  }

  explicit operator bool() const noexcept { return -1 != store_type_; }

  template <typename S = ::std::ostream, typename U>
  variant& assign(U&& f)
  {
    return operator=<S>(::std::forward<U>(f));
  }

  template <typename U>
  bool contains() const noexcept
  {
    return ::detail::index_of<U, T...>{} == store_type_;
  }

  bool empty() const noexcept { return !*this; }

  template <typename U>
  typename ::std::enable_if<
    (-1 != ::detail::index_of<U, T...>{}) &&
    (::std::is_enum<U>{} || ::std::is_fundamental<U>{}),
    U
  >::type
  cget() const
  {
    if (::detail::index_of<U, T...>{} == store_type_)
    {
      return *static_cast<U const*>(static_cast<void const*>(store_));
    }
    else
    {
      throw ::std::bad_typeid();
    }
  }

  template <typename U>
  typename ::std::enable_if<
    (-1 != ::detail::index_of<U, T...>{}) &&
    !(::std::is_enum<U>{} || ::std::is_fundamental<U>{}),
    U const&
  >::type
  cget() const
  {
    if (::detail::index_of<U, T...>{} == store_type_)
    {
      return *static_cast<U const*>(static_cast<void const*>(store_));
    }
    else
    {
      throw ::std::bad_typeid();
    }
  }

  template <typename U>
  typename ::std::enable_if<
    (-1 != ::detail::index_of<U, T...>{}),
    U&
  >::type
  get()
  {
    if (::detail::index_of<U, T...>{} == store_type_)
    {
      return *static_cast<U*>(static_cast<void*>(store_));
    }
    else
    {
      throw ::std::bad_typeid();
    }
  }

  template <typename U>
  typename ::std::enable_if<
    (-1 != ::detail::index_of<U, T...>{}),
    U const&
  >::type
  get() const
  {
    if (::detail::index_of<U, T...>{} == store_type_)
    {
      return *static_cast<U const*>(static_cast<void const*>(store_));
    }
    else
    {
      throw ::std::bad_typeid();
    }
  }

  template <typename U>
  typename ::std::enable_if<
    (-1 == ::detail::index_of<U, T...>{}) &&
    (-1 != ::detail::compatible_index_of<U, T...>{}) &&
    (::std::is_enum<U>{} || ::std::is_fundamental<U>{}),
    U
  >::type
  get() const
  {
    static_assert(::std::is_same<
      typename ::detail::type_at<
        ::detail::compatible_index_of<U, T...>{}, T...>::type,
      typename ::detail::compatible_type<U, T...>::type>{},
      "internal error");
    if (::detail::compatible_index_of<U, T...>{} == store_type_)
    {
      return U(*static_cast<
        typename ::detail::compatible_type<U, T...>::type const*>(
          static_cast<void const*>(store_)));
    }
    else
    {
      throw ::std::bad_typeid();
    }
  }

  template <typename U>
  static constexpr int type_index() noexcept
  {
    return ::detail::index_of<U, T...>{};
  }

  int type_index() const noexcept { return store_type_; }

private:
  using copier_type = void (*)(variant&, variant const&);
  using mover_type = void (*)(variant&, variant&&);
  using streamer_type = void (*)(void*, variant const&);

  template <typename charT, typename traits>
  friend ::std::basic_ostream<charT, traits>& operator<<(
    ::std::basic_ostream<charT, traits>& os, variant const& v)
  {
    v.streamer_(&os, v);

    return os;
  }

  template <class U>
  constexpr typename ::std::enable_if<
    ::std::is_copy_constructible<U>{}, copier_type
  >::type
  get_copier() const
  {
    return copier_stub<U>;
  }

  template <class U>
  constexpr typename ::std::enable_if<
    !::std::is_copy_constructible<U>{}, copier_type
  >::type
  get_copier() const
  {
    return nullptr;
  }

  template <class U>
  constexpr typename ::std::enable_if<
    ::std::is_move_constructible<U>{}, mover_type
  >::type
  get_mover() const
  {
    return mover_stub<U>;
  }

  template <class U>
  constexpr typename ::std::enable_if<
    !::std::is_move_constructible<U>{}, mover_type
  >::type
  get_mover() const
  {
    return nullptr;
  }

  template <class S, class U>
  constexpr typename ::std::enable_if<
    ::detail::is_streamable<S, U>{},
    streamer_type
  >::type
  get_streamer() const
  {
    return streamer_stub<S, U>;
  }

  template <class S, class U>
  constexpr typename ::std::enable_if<
    !::detail::is_streamable<S, U>{},
    streamer_type
  >::type
  get_streamer() const
  {
    return nullptr;
  }

  template <typename U>
  static void destructor_stub(void* const p)
  {
    static_cast<U*>(p)->~U();
  }

  template <typename U>
  static typename ::std::enable_if<
    ::std::is_copy_constructible<U>{} &&
    ::std::is_copy_assignable<U>{}
  >::type
  copier_stub(variant& dst, variant const& src)
  {
    if (src.store_type_ == dst.store_type_)
    {
      *static_cast<U*>(static_cast<void*>(dst.store_)) =
        *static_cast<U const*>(static_cast<void const*>(src.store_));
    }
    else
    {
      if (dst)
      {
        dst.deleter_(dst.store_);
      }
      // else do nothing

      new (dst.store_) U(*static_cast<U const*>(
        static_cast<void const*>(src.store_)));

      dst.deleter_ = src.deleter_;

      dst.copier_ = src.copier_;

      dst.mover_ = src.mover_;

      dst.streamer_ = src.streamer_;

      dst.store_type_ = src.store_type_;
    }
  }

  template <typename U>
  static typename ::std::enable_if<
    ::std::is_copy_constructible<U>{} &&
    !::std::is_copy_assignable<U>{}
  >::type
  copier_stub(variant& dst, variant const& src)
  {
    if (dst)
    {
      dst.deleter_(dst.store_);
    }
    // else do nothing

    new (dst.store_) U(*static_cast<U const*>(
      static_cast<void const*>(src.store_)));

    dst.deleter_ = src.deleter_;

    dst.copier_ = src.copier_;

    dst.mover_ = src.mover_;

    dst.streamer_ = src.streamer_;

    dst.store_type_ = src.store_type_;
  }

  template <typename U>
  static typename ::std::enable_if<
    ::std::is_move_constructible<U>{} &&
    ::std::is_move_assignable<U>{}
  >::type
  mover_stub(variant& dst, variant&& src)
  {
    if (src.store_type_ == dst.store_type_)
    {
      *static_cast<U*>(static_cast<void*>(dst.store_)) =
        ::std::move(*static_cast<U*>(static_cast<void*>(src.store_)));
    }
    else
    {
      if (dst)
      {
        dst.deleter_(dst.store_);
      }
      // else do nothing

      new (dst.store_) U(::std::move(*static_cast<U*>(
        static_cast<void*>(src.store_))));

      dst.deleter_ = src.deleter_;

      dst.copier_ = src.copier_;

      dst.mover_ = src.mover_;

      dst.streamer_ = src.streamer_;

      dst.store_type_ = src.store_type_;
    }
  }

  template <typename U>
  static typename ::std::enable_if<
    ::std::is_move_constructible<U>{} &&
    !::std::is_move_assignable<U>{}
  >::type
  mover_stub(variant& dst, variant&& src)
  {
    if (dst)
    {
      dst.deleter_(dst.store_);
    }
    // else do nothing

    new (dst.store_) U(::std::move(*static_cast<U*>(
      static_cast<void*>(src.store_))));

    dst.deleter_ = src.deleter_;

    dst.copier_ = src.copier_;

    dst.mover_ = src.mover_;

    dst.streamer_ = src.streamer_;

    dst.store_type_ = src.store_type_;
  }

  template <class S, typename U>
  static typename ::std::enable_if<
    ::detail::is_streamable<S, U>{}
  >::type
  streamer_stub(void* const os, variant const& v)
  {
    *static_cast<S*>(os) << v.cget<U>();
  }

  using deleter_type = void (*)(void*);
  deleter_type deleter_;

  copier_type copier_;
  mover_type mover_;

  streamer_type streamer_;

  int store_type_{-1};

  alignas(max_align_type) char store_[sizeof(max_size_type)];
};

#ifdef __GNUC__
# pragma GCC diagnostic pop
#endif // __GNUC__

#endif // VARIANT_HPP

Usage:

#include <iostream>

#include "variant.hpp"

int main()
{
  variant<std::string, int> v;

  v = std::string("A");

  std::cout << v.get<std::string>() << std::endl;

  auto s(v);

  std::cout << s.get<std::string>() << std::endl;

  s = 999;

  std::cout << s.get<float>() << std::endl;

  s = v;

  std::cout << s.get<std::string>() << std::endl;

  return 0;
}
\$\endgroup\$
  • \$\begingroup\$ Quick comment: In your usage example, you should be able to write v = std::string("AAAAAA"); (without the std::move). \$\endgroup\$ – ruds Jul 25 '13 at 4:41
  • \$\begingroup\$ Now works with g++ yay! \$\endgroup\$ – user1095108 Aug 9 '13 at 13:08
4
\$\begingroup\$

I can think of one small improvement you can make.

In the assignment operator (for both variant and U arguments), if the typeid of the argument is the same as store_type_, you can move the argument directly into store instead of deleting and re-newing.

Otherwise, this is a pretty cool class -- always interesting to see what's possible with C++, though I'd have to see a fairly deep series of unit tests to be fully confident this code is correct. Some ideas for test cases:

  • std::unique_ptr with custom deleters moved into a variant
  • types with different alignments stored in the same variant
  • variants stored in a variant
  • trying to store a non-movable type fails to compile
  • Simple things, like
    • get<U> fails to compile if U isn't one of the types (e.g. if it's a subtype of one of the types)
    • get<U> throws when U isn't currently stored
    • contains<U> works correctly
    • variant<Foo, Bar> v(Foo()); Foo foo = std::move(v.get<Foo>()); works correctly.

Some of these are a bit paranoid, but this is certainly the type of class where I'd like to push into the corner cases to ensure that they work as I expect.

\$\endgroup\$
  • \$\begingroup\$ To move both destination and source objects must exist, so I cannot implement the improvement you propose. I could, however, implement support for CopyConstructible types. \$\endgroup\$ – user1095108 Jul 25 '13 at 14:17
  • \$\begingroup\$ *static_cast<U*>(static_cast<void*>(store_)) = std::move(f); doesn't work? \$\endgroup\$ – ruds Jul 25 '13 at 14:35
  • \$\begingroup\$ This is the situation in which you're already storing an object of the appropriate type. \$\endgroup\$ – ruds Jul 25 '13 at 21:05
  • \$\begingroup\$ Sure, but the stored object needs to be either MoveAssignable or CopyAssignable, those are additional requirements to MoveConstructible and CopyConstructible. \$\endgroup\$ – user1095108 Jul 25 '13 at 22:08
  • \$\begingroup\$ Fair enough, but my sense is that most objects that are MoveConstructible are also MoveAssignable; in any case you can enable_if on is_move_assignable. \$\endgroup\$ – ruds Jul 26 '13 at 0:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.