Boiethios Boiethios - 1 month ago 8
C++ Question

Destruction error while implementing variant

I am trying to rewrite the basis of

std::variant
because I must use a c++14 compiler (without std::variant) and I need a typesafe enum.

My code only works with fundamental types (ie
int
,
float
,
char *
, etc.) But when using
std::string
for example, I have issues with destruction.

Here is my code:

namespace utils
{
namespace // private implementation
{
template<std::size_t Index, typename T, typename... Rest>
struct can_construct_type : std::false_type {}; // End of recursion, T is not in Rest...

template<std::size_t Index, typename T, typename First>
struct can_construct_type<Index, T, First> : std::is_constructible<First, T>
{ // There is only one type remaining to check
static constexpr std::size_t index = Index;
};

template<std::size_t Index, typename T, typename First, typename... Rest>
struct can_construct_type<Index, T, First, Rest...> : std::integral_constant<bool, std::is_constructible<First, T>::value || can_construct_type<Index + 1, T, Rest...>::value>
{ // Main recursive loop. Check for First, then recursively for Rest...
static constexpr std::size_t index = (std::is_constructible<First, T>::value ? Index : can_construct_type<Index + 1, T, Rest...>::index);
};
}

template<typename T, typename... Types>
using can_construct_type = can_construct_type<0, T, Types...>;
}


template<typename... Types>
class variant
{
template<typename _Vp> friend void * __get_storage(_Vp &);

using storage_t = std::aligned_union_t<0u, Types...>;
storage_t m_storage;
int m_type_index;

/* creation of content */
template<typename T> inline void set_value(T const & value)
{
new ((T *)std::addressof(m_storage)) T(value);
m_type_index = utils::can_construct_type<T, Types...>::index;
}

/* destruction of content */
inline void destroy_data(void) { invoke_destructor(m_type_index, std::addressof(m_storage)); }
static void invoke_destructor(int type, storage_t * storage_address)
{
static const std::array<void(*)(storage_t *), sizeof...(Types)> destructors
{
std::addressof(invoke_destructor_impl<Types>)...
};
destructors[type](storage_address);
}
template<class T> static void invoke_destructor_impl(storage_t * storage_address)
{
T * pt = reinterpret_cast<T *>(storage_address);
delete pt;
//pt->~T(); // I tried delete or ->~T() but both crash
}

public:
variant() = delete;
template<typename T> variant(T value)
{
set_value(value);
}

template<typename T> variant & operator=(T & value)
{
destroy_data();
set_value(value);
return *this;
}

~variant()
{
destroy_data();
}

std::size_t index(void) const
{
return m_type_index;
}
};

/* getter */
template<typename Variant> static void * __get_storage(Variant & var)
{
return reinterpret_cast<void *>(std::addressof(var.m_storage));
}

template<typename T, typename... Types> T * get_if(variant<Types...> & var)
{
return (var.index() == utils::contains_type<T, Types...>::index
? reinterpret_cast<T *>(__get_storage(var))
: nullptr);
}

/* tests */
int main()
{
variant<int, char *> var_test(42);
int * value1 = get_if<int>(var_test);
if (value1)
{
std::cout << *value1 << "\n"; // OK
}

var_test = _strdup("Hello !");
char ** value2 = get_if<char *>(var_test);
if (value2)
{
std::cout << *value2 << "\n"; // OK
}

variant<int, std::string> var_test_2(42);
//var_test_2 = _strdup("Hello again !");
var_test_2 = std::string("Hello again !");
std::string * value3 = get_if<std::string>(var_test_2);
if (value3)
{
std::cout << *value3 << "\n";
}
return 0; // it crashes when the destructor of var_test_2 is called
}


I put a full code example, but only the implementation of
variant
is important.

I suppose I badly used the
std::aligned_union
but I cannot figure where and how.

I get the error:


Exception thrown at 0x00096370 in Test.exe: 0xC0000005: Access violation writing location 0xDDDDDDDD.


Update: it seems the destructor is called twice but I do not find why.

Answer

Your assignment operator is wrong:

template<typename T> variant & operator=(T & value)

This only accepts lvalues, but you try to assign an rvalue:

var_test_2 = std::string("Hello again !");

Instead of calling your custom assignment operator, it calls the implicitly defined copy assignment operator by constructing a temporary variant object through your non-explicit constructor:

template<typename T> variant(T value)

You can see this happening if you delete the copy constructor or mark your constructor as explicit:

variant(const variant&) = delete;
template<typename T> explicit variant(T value)

GCC says:

test.cpp:100:14: error: invalid initialization of non-const reference of type ‘std::__cxx11::basic_string<char>&’ from an rvalue of type ‘std::__cxx11::string {aka std::__cxx11::basic_string<char>}’
   var_test_2 = std::string("Hello again !");

If you change your assignment operator to use const references then your code works as desired. I'd additionally suggest marking your constructor as explicit, otherwise you run into really subtle problems like this one.

Live demo


A small aside: __get_storage is a reserved name since it starts with two underscores. You should not use it in your code.