How to Use Typelists

How to use typelists

The typelists are generic compile-time collections of types. If you use dynamic_cast, you are missing the point, because it should not be needed, because it is a static, compile time concept.

This works but I don't see the advantage over inheritance which is capable to do the same.

You cannot make any existing type inherit from anything you want. This is simply not feasible, because this existing type may be a built in type or a type from a library.
Think of the typelists as extensions of lists of types (e.g. in std::pair) for any reasonable number of types (instead of just 2).

The typelists can be used to create a facility to pass around a set of arguments to a function. This is a piece of code that calls generalized functors of 5 parameters (another concept from Modern C++ design) with the arguments supplied in a tupe (yet another one) with the typelist that defines types of objects held in the tuple:

//functor is just a holder of a pointer to method and a pointer to object to call this 
//method on; (in case you are unfamiliar with a concept)
template<class R, class t0, class t1, class t2, class t3, class t4>
R call(Loki::Functor<R,LOKI_TYPELIST_5(t0, t1, t2, t3, t4
)> func,
Loki::Tuple<LOKI_TYPELIST_5(t0, t1, t2, t3, t4)> tuple)
{
///note how you access fields
return func(Loki::Field<0>(tuple), Loki::Field<1>(tuple),
Loki::Field<2>(tuple), Loki::Field<3>(tuple),
Loki::Field<4>(tuple));
}

//this uses the example code
#include<iostream>
using namespace std;

int foo(ostream* c,int h,float z, string s,int g)
{
(*c)<<h<<z<<s<<g<<endl;
return h+1
}

int main(int argc,char**argv)
{
Loki::Functor<int,LOKI_TYPELIST_5(ostream*, int, float, string, int)> f=foo;
//(...)
//pass functor f around
//(...)
//create a set of arguments
Loki::Tuple<LOKI_TYPELIST_5(ostream*, int, float, string, int)> tu;
Field<0>(tu)=&cout;
Field<1>(tu)=5;
Field<2>(tu)=0.9;
Field<3>(tu)=string("blahblah");
Field<4>(tu)=77;
//(...)
//pass tuple tu around, possibly save it in a data structure or make many
//specialized copies of it, or just create a memento of a call, such that
//you can make "undo" in your application; note that without the typelist
//you would need to create a struct type to store any set of arguments;
//(...)
//call functor f with the tuple tu
call(f,tu);
}

Note that only with other concepts like tuples or functors the typelists start to be useful.
Also, I have been experiencing Loki for about 2 years in a project and because of the template code (a lot of it) the sizes of executables in DEBUG versions tend to be BIG (my record was 35 MB or so). Also there was a bit of hit on the speed of compilation. Also recall that C++0x is probably going to include some equivalent mechanism. Conclusion: try not to use typelists if you don't have to.

Usage of typelist

Its also used in Mixin-Based Programming in C++ described by Ulrich W. Eisenecker, Frank Blinn, and Krzysztof Czarneck.

implementing a typelist in C++

With clang++ (Apple clang version 11.0.3 (clang-1103.0.32.59)) I get the following message:

t.cpp:51:8: error: class template partial specialization contains template parameters that cannot be deduced; this partial specialization will never be used [-Wunusable-partial-specialization]
struct Back<Typelist<Head...,Tail>>
^~~~~~~~~~~~~~~~~~~~~~~~~~~~
t.cpp:50:23: note: non-deducible template parameter 'Head'
template <typename... Head, typename Tail>
^
t.cpp:50:38: note: non-deducible template parameter 'Tail'
template <typename... Head, typename Tail>
^

From: https://en.cppreference.com/w/cpp/language/parameter_pack (sorry i don't have a primary ref)

In a primary class template, the template parameter pack must be the final parameter in the template parameter list.

In a function template, the template parameter pack may appear earlier in the list provided that all following parameters can be deduced from the function arguments, or have default arguments:

template<typename... Ts, typename U> struct Invalid; // Error: Ts.. not at the end

template<typename ...Ts, typename U, typename=void>
void valid(U, Ts...); // OK: can deduce U
// void valid(Ts..., U); // Can't be used: Ts... is a non-deduced context in this position

valid(1.0, 1, 2, 3); // OK: deduces U as double, Ts as {int,int,int}

But we can solve the issue by defining Back in terms of Element.

/**** get last element ****/
template <typename List>
struct Back;

template <typename... Args>
struct Back<Typelist<Args...>>
{
using type = typename Element<Typelist<Args...>, sizeof...(Args) - 1>::type;
};

Question:

Why do you have template using statements for all your types except Back and Element?

Basic Typelist functionality

We first need some helper templates. The first one checks if two types are the same:

template <typename T, typename U>
struct is_same
{
// Default case: T and U are not the same type
static const bool value = false;
};

template <typename T>
struct is_same<T, T>
{
// Specialization: both template arguments are of the same type
static const bool value = true;
};

We can now use the boolean is_same<T, U>::value to determine if the types T and U are equivalent.

Using is_same we can now write a template which checks if a specific type occurs in a type list:

template <typename TList, typename T>
struct contains
{
static const bool value =
is_same<typename TList::head, T>::value // Base case
|| contains<typename TList::tail, T>::value; // Recursion
};

template <typename T>
struct contains<nulltype, T>
{
// Termination condition
static const bool value = false;
};

This is a recursive template, using a specialization on nulltype to terminate the recursion.

One final helper template we need is enable_if:

template <bool Cond, typename T=void>
struct enable_if
{
// Default case: Cond assumed to be false, no typedef
};

template <typename T>
struct enable_if<true, T>
{
// Specialization: Cond is true, so typedef
typedef T type;
};

enable_if<true, T>::type yields T, whereas enable_if<false, T>::type is undefined. We exploit the SFINAE rule the enable or disable a function depending on its template argument, like so:

template <typename TList>
class OutputClass
{
public:
// Only enable the function if T is in TList (by SFINAE)
template <typename T>
typename enable_if<contains<TList, T>::value>::type
output(T t)
{
std::cout << t << std::endl;
}
};

Quite a trip, but if you understand all of this you're well on your way to master template metaprogramming. For an in-depth discussion of template metaprogramming I recommend you pick up a copy of C++ Template Metaprogramming (ISBN-13: 9780321227256).

Typelists visitor pattern example

What if you have 50 DocElement types? With the first example you need 50 if statements, with the second, you just need to have a typelist of 50 elements.

You can also think about what happens when you add another DocElement. In the first example, you need to go and change the if-else statements. With type-lists you can just add the new type to the end of the type-list.

The type-list code may seem like a lot of overhead, but you write it once, then you just use it, and instead of adding ifs or cases and code (which can get quite large over time) you just add types to the type-list. From a maintenance perspective the type-list code is far better than a huge switch statement or tens or hundreds of ifs.

As for improvements, I don't know, I'm still waiting for variadic templates and type aliases to be included in VS so that I can simplify the code even more.

Whenever I see a huge pile of repeating code, I start thinking about type-lists and metaprogramming, let the compiler do the work, not bored programmers. And the best part? You get zero penalty at runtime, it's just as efficient as the ifs (if you're careful with inlining)

Runtime typeswitch for typelists as a switch instead of a nested if's?

You'll need the preprocessor to generate a big switch. You'll need get<> to no-op out-of-bound lookups. Check the compiler output to be sure unused cases produce no output, if you care; adjust as necessary ;v) .

Check out the Boost Preprocessor Library if you care to get good at this sort of thing…

template <typename L>
struct type_switch
{
template< typename F >
void operator()( size_t i, F& f )
{
switch ( i ) {
#define CASE_N( N ) \
case (N): return f.operator()<typename impl::get<L,N>::type>();
CASE_N(0)
CASE_N(1)
CASE_N(2)
CASE_N(3) // ad nauseam.
}
};

implementing typelist replace operation using variadic templates

You might simplify your code and get rid of the recursion with

template <typename TList, typename T, typename U> struct ReplaceAll;

template <typename ... Ts, typename T, typename U>
struct ReplaceAll<Typelist<Ts...>, T, U>
{
using Result = Typelist<std::conditional_t<std::is_same_v<Ts, T>, U, Ts>...>;
};

Demo

From typelist to argument pack

This code converts typelist to tuple and calls foo with simple types.

#include <tuple>
#include <iostream>

template<typename H, typename T>
struct typelist
{
typedef H Head;
typedef T Tail;
};

struct null_typelist {};

template<int... Indices>
struct indices {
using next = indices<Indices..., sizeof...(Indices)>;
};

template<int Size>
struct build_indices {
using type = typename build_indices<Size - 1>::type::next;
};

template<>
struct build_indices<0> {
using type = indices<>;
};

template<typename T>
using Bare = typename std::remove_cv<typename std::remove_reference<T>::type>::type;

template<typename Tuple>
constexpr
typename build_indices<std::tuple_size<Bare<Tuple>>::value>::type
make_indices()
{ return {}; }

template<typename T, typename... Args>
struct tuple_push;

template<typename T, typename... Args>
struct tuple_push<T, std::tuple<Args...>>
{
typedef std::tuple<Args..., T> type;
};

template<typename TL>
struct typelist_to_tuple;

template<typename H, typename T>
struct typelist_to_tuple<typelist<H, T>>
{
typedef typename tuple_push<H, typename typelist_to_tuple<T>::type>::type type;
};

template<typename H>
struct typelist_to_tuple<typelist<H, null_typelist>>
{
typedef std::tuple<H> type;
};

struct Float {
float get() const { return 7.5; }
};
struct Int {
int get() const { return 7; }
};

template<typename... Args>
void foo(const Args&... args)
{
}

template<typename T, typename... Args>
void foo(const T& current, const Args&... args)
{
std::cout << current << std::endl;
foo(args...);
}

template<typename Tuple, int... Indices>
void apply(const Tuple& tuple, indices<Indices...>)
{
foo(std::get<Indices>(tuple).get()...);
}

template<typename Tuple>
void apply(const Tuple& tuple)
{
apply(tuple, make_indices<Tuple>());
}

int main()
{
typedef typelist<Int, typelist<Float, typelist<Int, null_typelist>>> list;
typedef typelist_to_tuple<list>::type tuple;
tuple t = std::make_tuple(Int(), Float(), Int());
apply(t);
}

live example

How to get an element of type list by index

I want to avoid using tuples as type lists for use cases, that require
instantiation for passing a list like f(L{})

If you don't want to instanciate std::tuple but you're ok with it in
unevaluated contexts, you may take advantage of std::tuple_element to
implement your typeAt trait:

template <std::size_t I, typename T>
struct typeAt;

template <std::size_t I, typename... Args>
struct typeAt<I, type_list<Args...>> : std::tuple_element<I, std::tuple<Args...>> {};
// ^ let library authors do the work for you

using L = type_list<int, char, float, double>;
using T = typename typeAt<2, L>::type;

static_assert(std::is_same<T, float>::value, "");


Related Topics



Leave a reply



Submit