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 a utility function by default. In the remaining sections of this blog, we will explore the Print
function with respect to the 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.
- Use a single Print function to print all the data members.
- 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.