It is often useful to define classes or structures that have a variable number and type of data members which are defined at compile time. The canonical example is std::tuple, but sometimes is it is necessary to define your own custom structures. Here is an example that defines the structure using compounding (rather than inheritance as with std::tuple
.
Start with the general (empty) definition, which also serves as the base-case for recrusion termination in the later specialisation:
template<typename ... T>
struct tuple {};
This already allows us to define an empty structure, tuple<>
data, albeit that isn't very useful yet.
Next comes the recursive case specialisation:
template<typename T, typename ... Rest>
struct tuple<T, Rest ...>
{
tuple(const T& first, const Rest& ... rest)
: first(first)
, rest(rest...)
{}
T first;
tuple<Rest ... > rest;
};
This is now sufficient for us to create arbitrary data structures, like tuple<int, float, std::string> data(1, 2.1, "hello").
So what's going on? First, note that this is a specialisation whose requirement is that at least one variadic template parameter (namely T
above) exists, whilst not caring about the specific makeup of the pack Rest
. Knowing that T exists allows the definition of its data member, first
. The rest of the data is recursively packaged as tuple<Rest ... >
rest. The constructor initiates both of those members, including a recursive constructor call to the rest member.
You can visualise this as follows:
tuple <int, float>
-> int first
-> tuple <float> rest
-> float first
-> tuple <> rest
-> (empty)
So on to the helper class. This time we will need an empty forward declaration and two specialisations. First the declaration:
template<size_t idx, typename T>
struct helper;
Now the base-case (when idx==0
). In this case we just return the first member:
template<typename T, typename ... Rest>
struct helper<0, tuple<T, Rest ... >>
{
static T get(tuple<T, Rest...>& data)
{
return data.first;
}
};
In the recursive case, we decrement idx and invoke the helper for the rest member:
template<size_t idx, typename T, typename ... Rest>
struct helper<idx, tuple<T, Rest ... >>
{
static auto get(tuple<T, Rest...>& data)
{
return helper<idx-1, tuple<Rest ...>>::get(data.rest);
}
};
To work through an example, suppose we have tuple<int, float>
data and we need data.get<1>()
. This invokes helper<1, tuple<int, float>>::get(data)
(the 2nd specialisation), which in turn invokes helper<0, tuple>::get(data.rest)
, which finally returns (by the 1st specialisation as now idx is 0) data.rest.first.
So that's it! Here is the whole functioning code, with some example use in the main function:
Full code
#include <type_traits>
#include <iostream>
using namespace std;
namespace my {
template <typename ...Ts>
struct tuple {};
template <typename T, typename ...Ts>
struct tuple <T, Ts...> {
tuple(T first, Ts... rest) :
first(first), rest(rest...){}
T first;
tuple<Ts...> rest;
};
namespace detail {
template <int N, typename ...Ts>
struct helper;
template <typename T, typename ...Ts>
struct helper <0, tuple<T, Ts...>> {
static auto get(tuple<T, Ts...> ds){
return ds.first;
}
};
template <int N, typename T, typename ...Ts>
struct helper <N, tuple<T, Ts...>> {
static auto get(tuple<T, Ts...> ds){
return helper<N-1, tuple<Ts...>>::get(ds.rest);
}
};
}
template <int N, typename ...Ts>
auto get(tuple<Ts...> ds){
return detail::helper<N, decltype(ds)>::get(ds);
}
}
int main(){
my::tuple <int, bool, float> test = {5, false, 10.5};
std::cout << my::get<0>(test) << endl;
std::cout << my::get<1>(test) << endl;
std::cout << my::get<2>(test) << endl;
}
Reference
boost::get
andboost::tuple
? How about the header file yourstd::get
is implemented in? It isn't easy to read, but usually these are implemented as in C++. (the standard does not require that the standard library is implemented in C++) – Mini