Reputation: 279
Lets say I have a generic class Container that contains any type of tuple, and has a function template<typename T> T& get<T>();
that returns a reference to the element in the tuple. My very simple implementation looks like this:
template<typename... Ts>
class Container
{
std::tuple<Ts...> contents;
public:
Container(const Ts&... ts) : contents(ts...) {}
template <typename T>
T& get()
{
//TypeIndex is some meta-programming struct to find index of T in Ts
return std::get<TypeIndex<T, Ts...>::value>(contents);
}
};
Are there any good type erasure techniques to turn Container into a regular class without altering the get function signature? As in calling get<T>()
without knowing the tuples full type list? Something like this:
Struct A { int x; }
Struct B { int y; }
Struct C { int z; }
int main()
{
Container container(A(), B()); //Underlying storage is a std::tuple<A, B>
A& a = container.get<A>(); //Doesn't know the tuples type list but assumes A is in there.
C& c = container.get<C>(); //C isn't in the tuples type list, crash program, which would be correct behavior.
}
boost::any
is the usual go-to solution for these types of problems, but doesn't solve this particular problem because I would have to know the actual type of the underlying tuple to cast. Like if I tried to use it in the example above I would do boost::any_cast<std::tuple<A, B>>
to get A, or B which isn't any use to me because I'm purposely trying to hide the tuple type list.
Edit: full definition of TypeIndex.
#include <type_traits>
template <typename T, typename... Ts>
struct TypeIndex;
template <typename T, typename... Ts>
struct TypeIndex<T, T, Ts...> : std::integral_constant<std::size_t, 0> {};
template <typename T, typename U, typename... Ts>
struct TypeIndex<T, U, Ts...> : std::integral_constant<std::size_t, 1 + TypeIndex<T, Ts...>::value> {};
Upvotes: 4
Views: 894
Reputation: 11191
A slightly more efficient solution than the ones proposed so far is to use std::tuple
as the actual underlying storage, thus avoiding use of any
or unordered_map
If we use the classic type-erasure pattern, we only need one dynamic allocation (plus whatever is required to copy the actual objects), or zero if you implement small buffer optimization.
We start by defining a base interface to access an element by type.
struct base
{
virtual ~base() {}
virtual void * get( std::type_info const & ) = 0;
};
We use void*
instead of any
to return a reference to the object, thus avoiding a copy and possibly a memory allocation.
The actual storage class is derived from base
, and templated on the arguments it can contain:
template<class ... Ts>
struct impl : base
{
template<class ... Us>
impl(Us && ... us) : data_(std::forward<Us>(us) ... )
{
//Maybe check for duplicated types and throw.
}
virtual void * get( std::type_info const & ti )
{
return get_helper( ti, std::index_sequence_for<Ts...>() );
}
template<std::size_t ... Indices>
void* get_helper( std::type_info const & ti, std::index_sequence<Indices...> )
{
//If you know that only one element of a certain type is available, you can refactor this to avoid comparing all the type_infos
const bool valid[] = { (ti == typeid(Ts)) ... };
const std::size_t c = std::count( std::begin(valid), std::end(valid), true );
if ( c != 1 )
{
throw std::runtime_error(""); // something here
}
// Pack the addresses of all the elements in an array
void * result[] = { static_cast<void*>(& std::get<Indices>(data_) ) ... };
// Get the index of the element we want
const int which = std::find( std::begin(valid), std::end(valid), true ) - std::begin(valid);
return result[which];
}
std::tuple<Ts ... > data_;
};
Now we only have to wrap this in a type-safe wrapper:
class any_tuple
{
public:
any_tuple() = default; // allow empty state
template<class ... Us>
any_tuple(Us && ... us) :
m_( new impl< std::remove_reference_t< std::remove_cv_t<Us> > ... >( std::forward<Us>(us) ... ) )
{}
template<class T>
T& get()
{
if ( !m_ )
{
throw std::runtime_error(""); // something
}
return *reinterpret_cast<T*>( m_->get( typeid(T) ) );
}
template<class T>
const T& get() const
{
return const_cast<any_tuple&>(*this).get<T>();
}
bool valid() const { return bool(m_); }
private:
std::unique_ptr< base > m_; //Possibly use small buffer optimization
};
Check it live.
This can be extended further in many ways, for instance you can add a constructor that takes an actual tuple, you can access by index and pack the value in a std::any
, etc.
Upvotes: 2
Reputation: 16431
If you're ok with using boost::any
, you could use a vector
or unordered_map
of them. Here's a version implemented with unordered_map
:
class Container
{
public:
template<typename... Ts>
Container(std::tuple<Ts...>&& t)
{
tuple_assign(std::move(t), data, std::index_sequence_for<Ts...>{});
}
template<typename T>
T get()
{
auto it = data.find(typeid(T));
if(it == data.cend()) {
throw boost::bad_any_cast{};
} else {
return boost::any_cast<T>(it->second);
}
}
private:
std::unordered_map<std::type_index, boost::any> data;
};
And then you could write almost as in your request. I changed the constructor to accept a tuple to avoid a host of sfinae code to prevent overridding copy/move constructors, but you can do this if you so wish.
Container c(std::make_tuple(1, 1.5, A{42}));
try {
std::cout << "int: " << c.get<int>() << '\n';
std::cout << "double: " << c.get<double>() << '\n';
std::cout << "A: " << c.get<A>().val << '\n';
c.get<A&>().val = 0;
std::cout << "A: " << c.get<A>().val << '\n';
std::cout << "B: " << c.get<B>().val << '\n'; // error
} catch (boost::bad_any_cast const& ex) {
std::cout << "exception: " << ex.what() << '\n';
}
You could also instruct your Container
to commit std::terminate()
instead of throwing an exception.
Upvotes: 0
Reputation: 5304
Instead of hand written TypeIndex<T, Ts...>::value
you can use typeid(T)::hash_code()
and store data in a std::unordered_map<size_t, boost::any>
.
std::tuple
does not store information about underlying types. That information is encoded in tuple's type. So if your get
method can't know the type of the tuple, then it can't get offset in it where the value is stored. So you have to revert to dynamic methods and having a map is the simpliest one.
Upvotes: 2
Reputation: 823
#include <iostream>
struct tuple_base {
virtual ~tuple_base() {}
};
template <typename T>
struct leaf : virtual tuple_base {
leaf(T const & t) : value(t) {}
virtual ~leaf() {}
T value;
};
template <typename ... T>
struct tuple : public leaf<T> ... {
template <typename ... U>
tuple(U && ... u) : leaf<T>{static_cast<U&&>(u)} ... {}
};
struct container {
tuple_base* erased_value;
template <typename T>
T & get() {
return dynamic_cast<leaf<T>*>(erased_value)->value;
}
};
int main() {
container c{new tuple<int, float, char>{1, 1.23f, 'c'}};
std::cout << c.get<float>() << std::endl;
}
The key is that you must know more information about the structure of the tuple type. It is not possible to extract information from a type erased arbitrary tuple implementation using only a single type which it contains. This is more of a proof of concept, and you would probably be better off using something else, though it is the solution to what you asked.
Upvotes: 0