Practical Template Meta Programming (Part-1)

Static reflection using Template Meta Programming and Macros

In this blog we will implement a generic Print(user defined struct) utility function capable of printing all the public members of any user defined POD struct. For example:

// We would like to Print MessageA
// which has messageB as member item
struct messageA {
	int a;
	messageB b;
};
// messageB has messageD as member
struct messageB {
	int p;
	messageD d[3];
};
// struct layout
struct messageD {
	double t;
	int v;
};

and we expect the output as shown below after executing the following program:

int main() {
	// aggregate initialisation of struct
	messageA ma = {10,
		{12,
			{
				{1.2,12},
				{1.3,13},
				{1.4,14}
			}
		}
	};
	
	Print(ma);
}

// Expected output
a : 10
  p : 12
  d : [
      t : 1.2
      v : 12
     ,
      t : 1.3
      v : 13
     ,
      t : 1.4
      v : 14
  ]

C++ does not support such utility function by default. In the remaining sections of this blog, we will explore the Print function w.r.t to use of Templates.


How do we usualy print the public members of an object in C++ ? We simply write code to print each members:

std::cout << obj.member1 << std::endl;
std::cout << obj.member2 << std::endl;
std::cout << obj.member3 << std::endl;
std::cout << obj.member4 << std::endl;
and so on...

Therefore, these cout statements perform the actual printing. So, we can not get rid of them. But, we can automatically generate these cout statements with the help of C++ templates, so that we don’t need to write cout statements by hand. If you are new to C++ template you may want to check this introduction video by CppNuts.

So, we are going to generate code using C++ template. Let us see a simple example showing code generation using templates. In the following program, we are trying to generate code to add numbers from 1 to N by adding one number at a time in a function. It is like a recursive function call, but instead of calling the same function in the usual recursion, we invoke a different function every time, in the following example. The non type template parameter N makes the function signatures different.

// p1.cpp
// code generation for adding 1 + 2 + 3 + 4 .. + N
#include <iostream>

template<int N>
int add() {
	return N + add<N-1>();
}

template<>
int add<0>() {
	return 0;
}

int main() {
	std::cout << add<3>() << std::endl;
}

Using gdb to view the generated functions, we see that, 4 different versions of the add function are generated by the compiler. The recursive nature of the add function instantiates new add functions. Naturally, terminating condition is required to stop a recursion, and template recursion is no exception. We fully specialize the add template function with N = 0 and this will act as base condition of code generation. (more about Template Specialization here).

gdb ./a.out
>>> info functions 
All defined functions:

File p1.cpp:
int add<0>();
int add<1>();
int add<2>();
int add<3>();

Clang compiler also helps to view the compiler generated code:

// clang++ -Xclang -ast-print -fsyntax-only p1.cpp
// important parts of the generated code
template<> int add<3>() {
    return 3 + add<3 - 1>();
}
template<> int add<2>() {
    return 2 + add<2 - 1>();
}
template<> int add<1>() {
    return 1 + add<1 - 1>();
}
template<> int add<0>() {
    return 0;
}

Therefore, with the help of template and recursion, the compiler can generate code repeatedly in a specific pattern, i.e. N + add<N-1>() in the above example. Let’s try to generate a few cout statements with the help a similar program structure.

// p2.cpp
// code generation for printing N,N-1,N-2,.....1
#include <iostream>

template<int N>
void print() {
	std::cout << N <<std::endl;
	print<N-1>();
}

template<>
void print<0>() {

}

int main() {
	print<3>();
}

---

// Clang AST output:
// clang++ -Xclang -ast-print -fsyntax-only p2.cpp
template<> void print<3>() {
    std::cout << 3 << std::endl;
    print<3 - 1>();
}
template<> void print<2>() {
    std::cout << 2 << std::endl;
    print<2 - 1>();
}
template<> void print<1>() {
    std::cout << 1 << std::endl;
    print<1 - 1>();
}
template<> void print<0>() {
}

Now at this point, we can easily print the contents of a vector of known size by following the similar patten of recursive template instantiation. The important point though, is a non type template parameter (N) which can index a particular entry in a vector. In other words, template function carries index value in its own function signature.

// p3.cpp
// code generation for printing contents of a Vector of size N
#include <iostream>
#include <vector>

std::vector<int> v;

template<int N>
void print() {
	std::cout << v[N-1] <<std::endl;
	print<N-1>();
}

template<>
void print<0>() {

}

int main() {
	for(auto i : {23,12,30}) v.push_back(i);
	print<3>();
}
// outoput of g++ p3.cpp
30
12
23

// Clang AST output:
// clang++ -Xclang -ast-print -fsyntax-only p3.cpp
template<> void print<3>() {
    std::cout << v[3 - 1] << std::endl;
    print<3 - 1>();
}
template<> void print<2>() {
    std::cout << v[2 - 1] << std::endl;
    print<2 - 1>();
}
template<> void print<1>() {
    std::cout << v[1 - 1] << std::endl;
    print<1 - 1>();
}
template<> void print<0>() {
}

We will use the similar mechanism to print struct members. So, this leads to the question : What kinds of indices (or things) the template functions should carry so that they can print a specific struct member (offset of members may be) ?

To answer the above question, we will start with a small program:

/**
 * @file p4.cpp
 */
#include <iostream>

struct A {
	int x;
	double y;
};

void print_x(const A& a) {
	std::cout << a.x << std::endl;
}

void print_y(const A& a) {
	std::cout << a.y << std::endl;	
}

int main() {
	A a{2,3.4};
	print_x(a);
	print_y(a);
}

In this program, we have a structure, A which has two members. The action of printing these two members are hardcoded in two different functions. Therefore the information of each of the members (i.e. .x and .y) are encoded in each of the respective function. But it seems that, we can not include this information as part of template, because .x and .y don’t have any identity on its own.

In C++ we also have Pointers to data members(link). For example, &A::x and &A::y are Pointers to data members. These pointers can be used with the help of Built-in pointer-to-member access operator (.*) (link). The following program explains the usage:

/**
 * @file p5.cpp
 */
#include <iostream>

struct A {
	int x;
	double y;
};

// decltype(&A::x) gives type of &A::x which is -> int A::*
void print(const A& a, decltype(&A::x) mp) {
	std::cout << a.*mp << std::endl;
}

// decltype(&A::y) gives type of &A::y which is -> double A::*
void print(const A& a, decltype(&A::y) mp) {
	std::cout << a.*mp << std::endl; /*   .* is a member access operator   */
}

int main() {
	A a{2,3.4};
	print(a,&A::x);
	print(a,&A::y);
}

In the above program, we have overloaded the print function by changing the type of the second argument. Here mp is a pointer to a data member and it has a specific type and we have used decltype to determine the type by the compiler.

Non type template parameter supports pointer to member type(link). Therefore, we can modify the second approach and create a templated print function.

/**
 * @file p6.cpp
 */
#include <iostream>

struct A {
	int x;
	double y;
};

template<typename T, T mp>
void print(const A& a) {
	std::cout << a.*mp << std::endl;
}

int main() {
	A a{2,3.4};
	print<decltype(&A::x),&A::x>(a);
	print<decltype(&A::y),&A::y>(a);
}

In the above program, we have passed &A::x and &A::y as the second template parameter. These are non-type template parameters of type T deduced using decltype. We use mp along with our struct a using the .* operator.

In p6.cpp, the template arguments to the print function are not easy to use from a usability point of view. We can improve it slightly in the following version:

/**
 * @file p7.cpp
 */
#include <iostream>

struct A {
	int x;
	double y;
};

template<typename T, T mp>
struct Meta{};

template<typename T, T mp>
void print(const A& a,Meta<T,mp> *) {
	std::cout << a.*mp << std::endl;
}

int main() {
	A a{2,3.4};
	using X = Meta<decltype(&A::x),&A::x>;
	using Y = Meta<decltype(&A::y),&A::y>;

	print(a,(X*)nullptr);
	print(a,(Y*)nullptr);
}

Here we have used the empty struct Meta only to encode type information in the template parameter. We create X and Y meta types to avoid using the long type information while calling the print function.

So, it seems we have partially accomplished our goal of printing struct members. There are two problems to handle at this point.

  1. Use a single Print function to print all the data members.
  2. Avoid writing the Meta structs by hand.

Let us solve the (1) first.

To solve the first problem, we need to create a single list of Meta structs and then use recursion to iterate over each item of that list. For this purpose, we introduce one more empty struct called ListOfMeta which can hold variable number of template parameters. We can encode all the Meta structs as template parameters into ListOfMeta and pass it to the print function.

/**
 * @file p8.cpp
 */
#include <iostream>

struct A {
	int x;
	double y;
};

template<typename T, T mp>
struct Meta{};

template<typename ...>
struct ListOfMeta{};

template<typename T, T mp,typename ...Rest>
void print(const A& a,ListOfMeta<Meta<T,mp>,Rest ...> *) {
	std::cout << a.*mp << std::endl;
}

int main() {
	A a{2,3.4};
	using X = Meta<decltype(&A::x),&A::x>;
	using Y = Meta<decltype(&A::y),&A::y>;
	using L = ListOfMeta<X,Y>;

	print(a,(L*)nullptr);
}

But we are still not performing the recursion to loop over all the Meta information stored in L. To iterate over all the items, we can call print again inside the print function with the ListOfMeta<Rest ...> parameter which holds the remaining of the meta information about the members. Of course, we also have to write a base case to stop recursive template instantiation.

/**
 * @file p9.cpp
 */
#include <iostream>

struct A {
	int x;
	double y;
};

template<typename T, T mp>
struct Meta{};

template<typename ...>
struct ListOfMeta{};

template<typename T, T mp,typename ...Rest>
void print(const A& a,ListOfMeta<Meta<T,mp>,Rest ...> *) {
	std::cout << a.*mp << std::endl;
	print(a,(ListOfMeta<Rest ...> *)nullptr);
}

// the following overloaded deinition of print is to 
// handle the base of recursion with single template item inside
// ListOfMeta
template<typename T,T mp>
void print(const A& a,ListOfMeta<Meta<T,mp>> *) {
	std::cout << a.*mp << std::endl;
}

int main() {
	A a{2,3.4};
	using X = Meta<decltype(&A::x),&A::x>;
	using Y = Meta<decltype(&A::y),&A::y>;
	using L = ListOfMeta<X,Y>;

	print(a,(L*)nullptr);
}

Using gdb we will be able to see the following generated functions. There are two of the print functions, as we have only two members in the struct A.

>>> info functions 
All defined functions:

File p9.cpp:
int main();
void print<double A::*, &A::y>(A const&, ListOfMeta<Meta<double A::*, &A::y> >*);
void print<int A::*, &A::x, Meta<double A::*, &A::y> >(A const&, ListOfMeta<Meta<int A::*, &A::x>, Meta<double A::*, &A::y> >*);

Once we have solved the iteration issue, we need to solve the issue of writing the Meta structs and ListOfMeta by hand. To solve this, we will take help of some macros.

#define MACRO_CONCAT(A,B) A##_##B
#define MACRO_EXPAND(x) x

#define MAKE_META_LIST_1(arg, ...)   Meta<decltype(arg),arg>
#define MAKE_META_LIST_2(arg, ...)   Meta<decltype(arg),arg>, MACRO_EXPAND(MAKE_META_LIST_1(__VA_ARGS__))
#define MAKE_META_LIST_3(arg, ...)   Meta<decltype(arg),arg>, MACRO_EXPAND(MAKE_META_LIST_2(__VA_ARGS__))

#define MAKE_META_LIST(N, arg, ...) \
		MACRO_CONCAT(MAKE_META_LIST,N)(arg,__VA_ARGS__)

template<typename>
struct MetaClass{};

#define CreateMetaInfo(STRUCT_NAME, N,...) \
		template<>	\
		struct MetaClass<STRUCT_NAME> { \
			using type = ListOfMeta<MAKE_META_LIST(N,__VA_ARGS__)>;	\
		};

MACRO_CONCAT is a macro to concatanate two items. The most important one is the MAKE_META_LIST macro. With this macro, we create the List of Meta structs. To understand the formation of the list, lets consider the line:

MACRO_CONCAT(MAKE_META_LIST,N)(arg,__VA_ARGS__)

Here N = No of Meta items. Assume N = 2, which will use the macro MAKE_META_LIST_2, and which in turn uses MAKE_META_LIST_1 macro and we end up with a comma separated list of two Meta<decltype(arg),arg> Meta structs. We need to make this list type associate of the parent struct A. To do this, we used MetaClass struct and CreateMetaInfo macro. The newly introduced MetaClass has the following definition and it takes 1 template parameter.

template<typename>
struct MetaClass{};

// usage of this MetaClass
// We spcialize with a user defined struct type

template<>	\
struct MetaClass<STRUCT_NAME> { \
    using type = ListOfMeta<MAKE_META_LIST(N,__VA_ARGS__)>;	\
};

Here we have used the using directive to declare a type inside the specialization of MetaClass for our struct A.

So, thats it, let’s see the final verison:

/**
 * @file p9.cpp
 */
#include <iostream>

struct A {
	int x;
	double y;
};

template<typename T, T mp>
struct Meta{};

template<typename ...>
struct ListOfMeta{};

template<typename T, T mp,typename ...Rest>
void print(const A& a,ListOfMeta<Meta<T,mp>,Rest ...> *) {
	std::cout << a.*mp << std::endl;
	print(a,(ListOfMeta<Rest ...> *)nullptr);
}

template<typename T,T mp>
void print(const A& a,ListOfMeta<Meta<T,mp>> *) {
	std::cout << a.*mp << std::endl;
}

#define MACRO_CONCAT(A,B) A##_##B
#define MACRO_EXPAND(x) x

#define MAKE_META_LIST_1(arg, ...)   Meta<decltype(arg),arg>
#define MAKE_META_LIST_2(arg, ...)   Meta<decltype(arg),arg>, MACRO_EXPAND(MAKE_META_LIST_1(__VA_ARGS__))
#define MAKE_META_LIST_3(arg, ...)   Meta<decltype(arg),arg>, MACRO_EXPAND(MAKE_META_LIST_2(__VA_ARGS__))

#define MAKE_META_LIST(N, arg, ...) \
		MACRO_CONCAT(MAKE_META_LIST,N)(arg,__VA_ARGS__)

template<typename>
struct MetaClass{};

#define CreateMetaInfo(STRUCT_NAME, N,...) \
		template<>	\
		struct MetaClass<STRUCT_NAME> { \
			using type = ListOfMeta<MAKE_META_LIST(N,__VA_ARGS__)>;	\
		};	

CreateMetaInfo(A,2,&A::x,&A::y);

template<typename T>
void Print(const T& obj) {
	using L = typename MetaClass<T>::type;
	print(obj,(L*)nullptr);
}

int main() {
	A a{2,3.4};
	Print(a);
}

As we can see CreateMetaInfo(A,2,&A::x,&A::y) macro is used one time, to generate the MetaClass and the ListOfMeta needed for the print function to work. In the helper Print function we have used using L = typename MetaClass<T>::type; to extract out the ListOfMeta information and subsequently to pass to the recursive print function which does the actual printing work.

For the purpose of this blog, I have reduced some of the details, required to handle nested struct, enums, or arrays. I have implemented these features in this (link).

Continuing on the topic of templates and Meta Programming, in the next part, we will see how we can achieve the same solution without using any macros.


See also