My goal is to learn a bit more about generic programming in C++. So, one of the trickiest things I've heard you can do is creating a Variant class. This took me a while and I needed to read and study a few implementations:
#ifndef VARIANT_HPP_INCLUDED
#define VARIANT_HPP_INCLUDED
#include <cstddef>
#include <typeindex>
#include <type_traits>
#include <iostream>
namespace spaceengine
{
namespace utils
{
template<typename... Ts>
struct VariantHelper;
template<typename F, typename... Ts>
struct VariantHelper<F, Ts...>
{
inline static void Destroy(const std::type_index typeIndex, void* data)
{
if(typeIndex == std::type_index(typeid(F)))
reinterpret_cast<F*>(data)->~F();
else
VariantHelper<Ts...>::Destroy(typeIndex,data);
}
inline static void Move(const std::type_index oldTypeIndex, void* oldV, void* newV)
{
if(oldTypeIndex == std::type_index(typeid(F)))
new (newV) F(std::move(*reinterpret_cast<F*>(oldV)));
else
VariantHelper<Ts...>::Move(oldTypeIndex,oldV,newV);
}
inline static void Copy(const std::type_index oldTypeIndex, const void* oldV, void* newV)
{
if(oldTypeIndex == std::type_index(typeid(F)))
new (newV) F(*reinterpret_cast<const F*>(oldV));
else
VariantHelper<Ts...>::Copy(oldTypeIndex,oldV,newV);
}
};
template<>
struct VariantHelper<>
{
inline static void Destroy(const std::type_index typeIndex, void* data){}
inline static void Move(const std::type_index oldTypeIndex, void* oldV, void* newV) {}
inline static void Copy(const std::type_index oldTypeIndex, const void* oldV, void* newV) {}
};
template<typename... Ts>
class Variant
{
public:
Variant() : m_typeIndex(InvalidType()){}
Variant(const Variant<Ts...>& old) : m_typeIndex(old.m_typeIndex)
{
HelperType::Copy(old.m_typeIndex, &old.m_data, &m_data);
}
Variant(Variant<Ts...>&& old) : m_typeIndex(old.m_typeIndex)
{
HelperType::Move(old.m_typeIndex, &old.m_data, &m_data);
}
Variant<Ts...>& operator=(Variant<Ts...> old)
{
m_typeIndex = old.m_typeIndex;
Variant<Ts...> temp(*this);
HelperType::Copy(old.m_typeIndex,&old.m_data,&m_data);
HelperType::Copy(temp.m_typeIndex,&temp.m_data,&old.m_data);
return *this;
}
template<typename T>
bool Is() const
{
return (m_typeIndex == std::type_index(typeid(T)));
}
bool IsValid() const
{
return (m_typeIndex != std::type_index(typeid(InvalidType())));
}
std::type_index GetTypeIndex() const { return m_typeIndex; }
template<typename T, typename... Args>
void Set(Args&&... args)
{
HelperType::Destroy(m_typeIndex,&m_data);
new (&m_data) T(std::forward<Args>(args)...);
m_typeIndex = std::type_index(typeid(T));
}
template<typename T>
const T& Get() const
{
if(m_typeIndex == std::type_index(typeid(T)))
return *reinterpret_cast<const T*>(&m_data);
else
throw std::bad_cast();
}
template<typename T>
T& Get()
{
if(m_typeIndex == std::type_index(typeid(T)))
return *reinterpret_cast<T*>(&m_data);
else
throw std::bad_cast();
}
~Variant()
{
HelperType::Destroy(m_typeIndex,&m_data);
}
private:
using DataType = typename std::aligned_union<1,Ts...>::type;
using HelperType = VariantHelper<Ts...>;
static inline std::type_index InvalidType()
{
return std::type_index(typeid(void));
}
std::type_index m_typeIndex;
DataType m_data;
};
}
}
#endif // VARIANT_HPP_INCLUDED
What I'm really looking for is:
- Is there anything that's usually required by a
Variant
that this class doesn't provide? - Have I missed anything else that's obvious?
- Does my use of placement new mean that my
Variant
class is stack allocated, or have I misunderstood what placement new does?
One of the things that irks me is that the boost::variant
class is very large; and it seems like my much smaller code does 100% of what I need. But I can't decipher boost::variant
well enough to figure out what it does that mine doesn't do.
1 Answer 1
Does my use of placement new mean that my Variant class is stack allocated, or have I misunderstood what placement new does?
It does mean that, and your implementation using the std::union
to provide sufficient allocated space appears to be valid.
Almost at least, because your implementation of Variant::Set()
is not checking types. So if a type is accidentally passed which wasn't covered within Ts
, respectively requires more allocated space, it is going to corrupt the memory.
But I can't decipher boost::variant well enough to figure out what it does that mine doesn't do.
For instance, the boost implementation gives an never empty guarantee which your implementation does not. You are explicitly handling the case in which the Variant is in an invalid state.
In Variant::Set()
, you are explicitly destroying old m_data
, but you are not reverting m_typeIndex
to a safe state prior to invoking the new constructor. If the new constructor would throw, it would leave m_typeIndex
on the old value which is plain wrong.
Consider adding an Variant::Unset()
method which handles save destruction instead and is guaranteed to set m_typeIndex
explicitly to the invalid type.
This also leads to invoking the destructor on invalid states.
That recursion in the template of VariantHelper
. Not going to lie - that's rather difficult to trace. A comment would have been in place, explaining what you are going to do.
-
\$\begingroup\$ Thanks for your review. Some questions, regarding Set, is there a way to check types at compile time? I've been scratching my head trying to see a way to do it (it's simple to add check for VariantHelper to return true, else loop). When you say it doesn't support primitive types, do you mean
int
,bool
etc? Because it certainly does; I'm missing something here. The VarientHelper just loops through itself until the first parameter in the parameter pack is the one desired. \$\endgroup\$NeomerArcana– NeomerArcana2016年05月08日 01:09:54 +00:00Commented May 8, 2016 at 1:09 -
\$\begingroup\$ About the built-in types, my fault. I wasn't aware that calling the destructor is explicitly allowed even for built-in types, as long as it is in the scope of a template. As for the type checking, you can do something like this for minimum effort: stackoverflow.com/a/16924234/2879325 , recursing through the list, and finally wrapped in a
static_assert
. \$\endgroup\$Ext3h– Ext3h2016年05月08日 10:37:05 +00:00Commented May 8, 2016 at 10:37