3
\$\begingroup\$

Here is a simple variant type I have made. For simplicity, it only can handle two different values. I am planning on using this to create a variadic variant template, using inheritance and recursion.

The requirements for my variant type:

  • It must have performance similar to std::variant
  • It must not dynamically allocate memory
  • There must be no undefined behavior
  • It should check as much as possible at compile-time, and throw an exception if access is attempted to the wrong type
  • It must support nontrivial types

Here is the source:

#include <iostream>
#include <cstddef>
#include <typeindex>
#include <optional>
#include <algorithm>
#include <stdexcept>
#include <functional>

/** A variant (sum type) that can contain one of two different types at any one time**/
template<typename T1, typename T2>
class Either {

    using Bigest = std::conditional_t<sizeof(T1) <= sizeof(T2), T1, T2>;
    alignas(sizeof(Bigest)) std::byte storage[sizeof(Bigest)];
    std::optional<std::type_index> conatinedType;
    std::function<void(std::byte*)> destructor;

public:
    Either() : conatinedType(std::nullopt) {}

    template<typename T>
    Either(const T& value) : conatinedType(typeid(T)) {
        static_assert(std::is_same<T1,T>::value || std::is_same<T2,T>::value, "Either cannot contain type T");
        const auto src = reinterpret_cast<const std::byte*>(&value);
        for(size_t i = 0; i < sizeof(T); i++) {
            storage[i] = src[i];
        }
        destructor = [](std::byte* data){ (*reinterpret_cast<T*>(data)).~T(); };
    }

    class BadVariantAccess : public std::exception {};

    template<typename T>
    const T& getAs() const {
        static_assert(std::is_same<T1,T>::value || std::is_same<T2,T>::value, "Either cannot contain type T");
        if(conatinedType == typeid(T)) {
            const auto ptr = reinterpret_cast<const T*>(&storage);
            return *ptr;
        }
        else throw BadVariantAccess();
    };
};

And a test:

struct MyInt {
    int val;
    MyInt(const int& val) : val(val) {
        std::cout << "making int!" << std::endl;
    }
    ~MyInt() {
        std::cout << "destructing int!" << std::endl;
    }
};

struct MyDouble {
    double val;
    MyDouble(const double& val) : val(val) {
        std::cout << "making double!" << std::endl;
    }
    ~MyDouble() {
        std::cout << "destructing double!" << std::endl;
    }
};

int main()
{
    Either<MyInt, MyDouble> e = MyDouble(5.0987);

    try{
        std::cout << e.getAs<MyDouble>().val << std::endl;
    }
    catch(...) {
        std::cerr << "Bad Variant Access" << std::endl;
    }

    return 0;
}
\$\endgroup\$
2
  • 1
    \$\begingroup\$ You can of course name your variables as you like, but personally, it would annoy me to see misspellings all the time: conatinedType -> containedType, Bigest -> Biggest. \$\endgroup\$
    – Aganju
    Mar 13, 2020 at 12:51
  • \$\begingroup\$ Welcome to Code Review! Can you tag the question will the revision of the C++ standard you are using (c++17, etc.)? I'll write a review for you when I have time. You can take the tour and visit our FAQs to further familiarize yourself our community. \$\endgroup\$
    – L. F.
    Mar 14, 2020 at 7:42

1 Answer 1

2
\$\begingroup\$

Construction and destruction of non-trivial types

Unfortunately, your class does not really support non-trivial types, because your class copies the representation of the supplied values and instead of invoking the proper copy constructor. In other words, you create no object in the Either class, so the behavior of reinterpret_casting the storage and calling the destructor is undefined.

Interestingly, you store the destructor function object but never actually call it, so the class ends up working for trivial types, which do not require invocations to the corresponding constructor or destructor. However, making your code work by letting two errors cancel out each other is not really a good idea.

The copy and move semantics of your class is also broken. We'll try to fix this problem later.

Use of runtime type information (typeid)

The usage of typeid is a bit overkill here and not optimal, because it forces the user to pass exactly the same type as one of the stored types, disallowing conversions. In fact, simply storing an index seems to suffice.

std::variant uses overload resolution to deduce the type, which is hard to implement. You may consider supporting in_place construction. (On the other hand, std::variant is typically implemented with unions because of constexpr requirements.)

The storage

using Bigest = std::conditional_t<sizeof(T1) <= sizeof(T2), T1, T2>;
alignas(sizeof(Bigest)) std::byte storage[sizeof(Bigest)];
std::optional<std::type_index> conatinedType;
std::function<void(std::byte*)> destructor;

Consider using std::aligned_storage. As I said above, we can simply store an index.

using storage_type = std::aligned_storage_t<std::max( sizeof(T1),  sizeof(T2)),
                                            std::max(alignof(T1), alignof(T2))>;
storage_type storage;
std::size_t index;

For valueless variants, we can provide a special index:

static constexpr std::size_t npos = std::numeric_limits<std::size_t>::max();

The constructors

The default constructor of std::variant default-constructs the first alternative type. You can go into the valueless state instead:

Either() noexcept
    : index{npos}
{
}

Here's how you'd support in place constructors:

template <std::size_t I, typename... Args>
explicit Either(std::in_place_t<I>, Args&&... args)
{
    construct<I>(std::forward<Args>(args)...);
}

template <std::size_t I, typename U, typename... Args>
explicit Either(std::in_place_t<I>, std::initializer_list<U> ilist, Args&&... args)
{
    construct<I>(ilist, std::forward<Args>(args)...);
}

where the private member function template construct is defined like

template <std::size_t I>
struct alternative_type {
    static_assert(I < 2);
    using type = std::conditional_t<I == 0, T1, T2>;
};
template <std::size_t I>
using alternative_type_t = typename alternative_type<I>::type;

// not exception safe
template <std::size_t I, typename... Args>
void construct(Args&&... args)
{
    index = I;
    ::new (static_cast<void*>(&storage))
        alternative_type_t<I>(std::forward<Args>(args));
}

Special member functions

As I said before, you need to implement the copy/move/destruction operations. Here's the copy constructor for example: (SFINAE and explicit issues are omitted for simplicity.)

Either(const Either& other)
{
    if (other.index == 0) {
        construct<T1>(other.get<0>());
    } else if (other.index == 1) {
        construct<T2>(other.get<1>());
    } else {
        index = npos;
    }
}
\$\endgroup\$

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.