Skip to content

Instantly share code, notes, and snippets.

@tibordp
Last active November 14, 2023 16:04
Show Gist options
  • Star 59 You must be signed in to star a gist
  • Fork 16 You must be signed in to fork a gist
  • Save tibordp/6909880 to your computer and use it in GitHub Desktop.
Save tibordp/6909880 to your computer and use it in GitHub Desktop.
A simple variant type implementation in C++
#include <iostream>
#include <utility>
#include <typeinfo>
#include <type_traits>
#include <string>
template <size_t arg1, size_t ... others>
struct static_max;
template <size_t arg>
struct static_max<arg>
{
static const size_t value = arg;
};
template <size_t arg1, size_t arg2, size_t ... others>
struct static_max<arg1, arg2, others...>
{
static const size_t value = arg1 >= arg2 ? static_max<arg1, others...>::value :
static_max<arg2, others...>::value;
};
template<typename... Ts>
struct variant_helper;
template<typename F, typename... Ts>
struct variant_helper<F, Ts...> {
inline static void destroy(size_t id, void * data)
{
if (id == typeid(F).hash_code())
reinterpret_cast<F*>(data)->~F();
else
variant_helper<Ts...>::destroy(id, data);
}
inline static void move(size_t old_t, void * old_v, void * new_v)
{
if (old_t == typeid(F).hash_code())
new (new_v) F(std::move(*reinterpret_cast<F*>(old_v)));
else
variant_helper<Ts...>::move(old_t, old_v, new_v);
}
inline static void copy(size_t old_t, const void * old_v, void * new_v)
{
if (old_t == typeid(F).hash_code())
new (new_v) F(*reinterpret_cast<const F*>(old_v));
else
variant_helper<Ts...>::copy(old_t, old_v, new_v);
}
};
template<> struct variant_helper<> {
inline static void destroy(size_t id, void * data) { }
inline static void move(size_t old_t, void * old_v, void * new_v) { }
inline static void copy(size_t old_t, const void * old_v, void * new_v) { }
};
template<typename... Ts>
struct variant {
private:
static const size_t data_size = static_max<sizeof(Ts)...>::value;
static const size_t data_align = static_max<alignof(Ts)...>::value;
using data_t = typename std::aligned_storage<data_size, data_align>::type;
using helper_t = variant_helper<Ts...>;
static inline size_t invalid_type() {
return typeid(void).hash_code();
}
size_t type_id;
data_t data;
public:
variant() : type_id(invalid_type()) { }
variant(const variant<Ts...>& old) : type_id(old.type_id)
{
helper_t::copy(old.type_id, &old.data, &data);
}
variant(variant<Ts...>&& old) : type_id(old.type_id)
{
helper_t::move(old.type_id, &old.data, &data);
}
// Serves as both the move and the copy asignment operator.
variant<Ts...>& operator= (variant<Ts...> old)
{
std::swap(type_id, old.type_id);
std::swap(data, old.data);
return *this;
}
template<typename T>
void is() {
return (type_id == typeid(T).hash_code());
}
void valid() {
return (type_id != invalid_type());
}
template<typename T, typename... Args>
void set(Args&&... args)
{
// First we destroy the current contents
helper_t::destroy(type_id, &data);
new (&data) T(std::forward<Args>(args)...);
type_id = typeid(T).hash_code();
}
template<typename T>
T& get()
{
// It is a dynamic_cast-like behaviour
if (type_id == typeid(T).hash_code())
return *reinterpret_cast<T*>(&data);
else
throw std::bad_cast();
}
~variant() {
helper_t::destroy(type_id, &data);
}
};
struct test{
int * holder;
test() {
std::cout << "test()" << std::endl;
holder = new int();
}
test(test&& old) : holder(nullptr) {
std::cout << "test(test&&)" << std::endl;
std::swap(holder,old.holder);
}
test(const test& old) {
std::cout << "test(const test&)" << std::endl;
holder = new int(*old.holder);
}
~test()
{
std::cout << "~test()" << std::endl;
delete holder;
}
};
int main() {
using my_var = variant<std::string, test>;
my_var d;
d.set<std::string>("First string");
std::cout << d.get<std::string>() << std::endl;
d.set<test>();
*d.get<test>().holder = 42;
my_var e(std::move(d));
std::cout << *e.get<test>().holder << std::endl;
*e.get<test>().holder = 43;
d = e;
std::cout << *d.get<test>().holder << std::endl;
}
Copy link

ghost commented Apr 21, 2017

To restrict get/set to only valid datatypes:

template <typename...> struct is_one_of {
  static constexpr bool value = false;
};

template <typename T, typename S, typename... Ts> struct is_one_of<T, S, Ts...> {
  static constexpr bool value = std::is_same<T, S>::value || is_one_of<T, Ts...>::value;
};

And then in the class definition:

template <typename T, typename... Args, typename = typename std::enable_if<is_one_of<T, Ts...>::value, void>::type>
T& get() { ... }


template <typename T, typename... Args, typename = typename std::enable_if<is_one_of<T, Ts...>::value, void>::type>
void set(Args &&... args) { ... }

@Apjue
Copy link

Apjue commented Dec 31, 2017

If you use C++11 but not C++17, here is a non RTTI variant (made from tibordp's one): https://gist.github.com/S6066/f726a37b2b703efea7ee27103e5bec89

@calebh
Copy link

calebh commented Apr 4, 2021

I created a variant that doesn't depend on the standard library, and uses type level integer indices to access elements of the variant instead of type_id and hash_code. It is available here:
https://gist.github.com/calebh/fd00632d9c616d4b0c14e7c2865f3085

@xlxie
Copy link

xlxie commented Dec 24, 2021

Thanks for your codes, it really helps!

I just found that hash_code is pretty time-consuming and I will try calebh's solution to optimize the performance.

@CyberDNIWE
Copy link

CyberDNIWE commented Dec 1, 2022

I know it's an older thread but I've made a C++11 compatible version loosely based on @tibordp and @calebh versions as well as added functionality to:

  • visit variant, which will call visitor's appropriate operator() overload according to the type it happends to be storing at the time
  • compile-time check for set<T>() type to match at least one of variant<Ts...> types from a pack

I however wouldn't mind some help with making type_id() generation purely in compile time. I have a hunch it can be done, I just cant figure it out for the life of me :(

@daniel-junhui
Copy link

Hi. Just curious why you don't use union?

@calebh
Copy link

calebh commented Apr 28, 2023

Hi. Just curious why you don't use union?

Unions don't support move/copy semantics which means that you can't have a union with a shared_ptr inside. Since the C/C++ runtime doesn't know which element of the union it holds, it cannot know if an element uses a copy constructor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment