about
10/26/2022
C++ Story TMP
CppStory

Chapter #8 - TemplateMetaprogramming

Code Generators

8.0 Prologue

Implementing C++ templates required compilers to develop a meta language that executes at compile-time. It manipulates types and constants to build function and class templates - entities that generate functions and classes, directed by types that are supplied by an application. The result is a two phase compilation process:
  1. compile template code with unspecified template parameter A: template<class A> class X { ... };
  2. compile instantiations of the template with a defined type AppClass: X<AppClass> x;
Template Metaprograms run during the second compilation phase.
Quick Starter Example
This code example is modeled after a discussion on stackoverflow.com: iterate over elements of a std::tuple
Like other functional languages, Template MetaProgramming (TMP) implements loops with recursion. It has to do that because there are no variables at compile-time, e.g., no way to change a loop iterator's value.
TMP Code: display items in a std::tuple #include <iostream> #include <typeinfo> #include <tuple> #include "../Display/Display.h" namespace Chap7 { // recursive showTuple definition template<size_t I = 0, typename... Tp> void showTuple(std::tuple<Tp...>& t) { std::cout << std::get<I>(t) << " "; // get Ith item if constexpr (I + 1 != sizeof...(Tp)) // I = I + 1 showTuple<I + 1>(t); } } // if constexpr (pred) { /* code to conditionally compile */ } // - pred == true => block is compiled so recurse // - pred == false => block is not compiled, stops recursion Using code: int main() { displayTitle("Demonstrate TMP"); std::tuple t{ 1, 2.5, 'a', "a string" }; std::cout << "\n "; Chap7::showTuple(t); std::cout << "\n\n"; } Output: Demonstrate TMP ================= 1 2.5 a a string
This simple example illustrates the power of template metaprogramming. In just a few lines of code, we enable display of the contents of std::tuple. The same technique works for STL containers, both sequential and associative, even though the sequential containers hold items with one value_type while associative containers hold items with both key_type and value_type.

8.1 Variadic Functions

A variadic function takes an arbitrary finite number of arguments. Variadic function templates build a sequence of functions one for each parameter arity, up to the largest number of arguments used in an application program. Only those functions that are actually called are compiled into code in the second phase translation. Variadic functions are a classic Template MetaProgramming (TMP) construct. Each function is defined in terms of the function with one fewer arguments. A template function overload for a single argument is provided, and C++ guarantees that that specialization will be compiled if matched. That specialization is used to stop recursion.
Example: Variadic Function displaying its arguments Code: Variadic Function #include <iostream> #include <string> #include "../Display/Display.h" /*-- Helper function --*/ std::string strTrunc( const std::string& src, size_t Max = 50 ) { if (src.size() > Max) { return src.substr(0, Max - 4) + " ..."; } else { return src; } } /*-- Templ specializ'n stops recursive eval --*/ template<typename T> void f(T t) { std::cout << "\n " << t << " : " << strTrunc(typeid(t).name(), 40) << std::endl; } /*-- Recursive definition of template function -- Args is a "parameter pack https://en.cppreference.com */ template<typename T, typename... Args> void f(T t, Args... args) { std::cout << "\n " << t << " : " << typeid(t).name() << std::endl; f(args...); } Using Code int main() { std::cout << "\n Demonstrate Variatic Functions"; std::cout << "\n ================================\n"; displayDemo("-- function with 5 args --\n"); f( 1, 3.5, 'z', "a literal string", std::string("a std::string") ); putline(); displayDemo("-- function with 2 args --\n"); f('q', "only two args"); std::cout << "\n\n"; return 0; } Output Demonstrate Variatic Functions ================================ -- function with 5 args -- 1 : int 3.5 : double z : char a literal string : char const * a std::string : class std::basic_string<char,struct ... -- function with 2 args -- q : char only two args : char const *
Here's a somewhat more useful function that can display the contents of either sequential or associative STL containers.
Example: Recursive display This is a nice example of a template function that gains a lot of versatility from accepting an arbitrary number of parameters (variadic) and overloading the insertion operator operator<< to extend its functionality from sequential containers with scalar value_types to associative containers with value_types that are std::pair<Key, Value>s. Code Source - Eli Bendersky Code: display function #include <iostream> template < template < typename, typename... > class ContainerType, typename ValueType, typename... Args > void print_container( const ContainerType< ValueType, Args... >& c ) { for (const auto& v : c) { std::cout << v << ' '; } std::cout << '\n'; } // Implement << for pairs: this is needed // to print out mappings where range // iteration goes over (key, value) pairs. template <typename T, typename U> std::ostream& operator<<( std::ostream& out, const std::pair<T, U>& p ) { out << "{" << p.first << ", " << p.second << "}"; return out; } Using Code #include "Chap7Vdisplay.h" #include "../Display/Display.h" #include #include #include int main() { displayTitle("Demo Variadic Display Function"); std::vector vInt{ 1,2,3,4 }; std::cout << "\n "; print_container(vInt); std::unordered_map mIntStr { {1, "one"}, { 2,"two" }, { 3, "three" } }; std::cout << "\n "; print_container(mIntStr); putline(2); } Output Demo Variadic Display Function ================================ 1 2 3 4 {1, one} {2, two} {3, three} The template specification at the top of the left panel matches the STL containers, e.g., std::vector<T, Alloc> and std::set<Key, Compare, Alloc>. That allows us to pass any STL container into the print_container function.
If you're puzzled by some of the syntax in the examples we've looked at so far, the next section should help.

8.1.1 Variadic Function Syntax

The expression typename... Args is a parameter pack, something close to a list of types. The only thing you can do with a template parameter pack is to use it in a template declaration to define an unspecified finite collection of types. The expression Args... args is a function parameter pack, a list of values. The only thing you can do with a function parameter pack is to expand it, or pass to a function. Fragment from 1st Example: /*-- Templ specializ'n stops recursive eval --*/ template<typename T> void f(T t) { std::cout << "\n " << t << " : " << strTrunc(typeid(t).name(), 40) << std::endl; } /*-- Recursive definition of template function -- Args is a "parameter pack https://en.cppreference.com */ template<typename T, typename... Args> void f(T t, Args... args) { std::cout << "\n " << t << " : " << typeid(t).name() << std::endl; f(args...); } Syntax Explained: Start with the function at the bottom of the left panel: Looking at the template declaration we see typename... Args. That's known as a template parameter pack. Think of the template declaration parameters as a list of types, with typename t being the head of the list and typename... Args being the tail. The ellipsis ... means there are a finite number, possibly zero, of types in the list tail. The corresponding Args... args in the function arguments list is called a function parameter pack. The function body does what it is supposed to do with t, then executes a tail recursion, stripping off the head of the list. So, the function executes on the first t argument, then recursively executes on the remaining list without the t head. For this to work, we need to define what happens with the last item on the list, when the function parameter pack is empty. That's taken care of by the specialization shown in the top of the left panel. That matches only when the function parameter pack is empty. This syntax can be simplified using fold expressions, the next topic.

8.1.2 Fold Expressions

Fold expressions allow us to delegate recursive parts of the definition of variadic structures to standard library code instead of implementing recursive functions as illustrated above. This only works for a relatively small, but useful, subset of functions we may wish to build. We will look at an example, then explain the syntax.
Example: folds over binary operator Code: folds over binary operator template<typename ...Args> auto accumulateOrs(Args... args) { return ((args) || ...); } template<typename ...Args> auto accumulateAnds(Args... args) { return ((args) && ...); } template<typename T, typename ...Args> auto sum(T t, Args... args) { return (t + ... + args); } Using code: displayDemo("\n -- fold on logical operator --"); std::cout << std::boolalpha; std::cout << "\n " << accumulateOrs(0, 0, 0); std::cout << "\n " << accumulateOrs(false, true, false); std::cout << "\n " << accumulateAnds(false, true, false); displayDemo("\n -- fold on addition operator --"); std::cout << "\n " << sum(1, 2.5, -2); std::string s1 = "first"; std::string s2 = " and "; std::string s3 = "second"; std::cout << "\n " << sum(s1, s2, s3); Output: -- fold on logical operator -- false true false -- fold on addition operator -- 1.5 first and second
Code fragment from previous example: template<typename ...Args> auto accumulateOrs(Args... args) { return ((args) || ...); } template<typename T, typename ...Args> auto sum(T t, Args... args) { return (t + ... + args); } Syntax explained: The function arguments for accumulateOrs and sum are just like the function arguments without fold expressions. What's new are the fold expressions:
  1. ((args) || ...)  -- unary right fold
  2. (t + ... + args) -- binary left fold
The first expands to args1 || args2 || ... and the second expands to t + args1 + args2 + ....
This example is a classic use of fold expressions. Next, we will show, in pushToCout1 that this simple code structure is often not quite what we want.
Example: pushToCout1 This example folds over the insertion operator <<, but that does not afford us a direct way to add spaces or comma seperation between inserts. See the results at the bottom of the right panel. Everything is mashed together. Code: push to std::cout template<typename ...Args> void pushToCout1(Args&&... args) { (std::cout << ... << std::forward<Args>(args)); } Using code: displayDemo("-- pushToCout1 -- fold on operator<< --"); std::cout << "\n "; pushToCout1("argggghhh!", 1, 2.5, 'z'); std::cout << "\n "; pushToCout1('z', 2.5, 1, "argggghhh!"); Output: -- pushToCout1 -- fold on operator<< -- argggghhh!12.5z z2.51argggghhh!
The next examples, pushToCout2 and pushToCout3 generalize that a bit by using functions on expanded items from the parameter pack. This example uses a fixed lambda. You can customize only by the arguments passed to the lambda.
Example: pushToCout2 - uses fixed function object This version defines a lambda that operates on each expanded args item by prefixing with ", ". We create the lambda inside a creational function that prepends the prefix by inserting into a std::ostringstream instance followed by the item. The purpose of the creational function is to manage the stringstream instance. In the next version we will extend this idea by allowing the user to inject an application specific creational function. Code: push to std::cout ver2 auto makeHelper( std::string prefix = ", ", std::string firstPrefix = "" ) { int debug = 0; auto f = [prefix, firstPrefix](auto a) mutable { static std::ostringstream out2; out2 << a; std::string temp = firstPrefix + out2.str(); out2.str(""); firstPrefix = prefix; return temp; }; debug++; return f; } template<typename ...Args> void pushToCout2(const Args&... args) { static auto f = makeHelper(", "); (std::cout << ... << (f(args))); } Using code: displayDemo( "\n -- pushToCout2 -- fold on operator<< --" ); std::cout << "\n "; pushToCout2("aaaahhhhh!", 1, 2.5, 'z'); std::cout << "\n "; pushToCout2('z', 2.5, 1, "aaaahhhhh!"); Output: -- pushToCout2 -- fold on operator<< -- aaaahhhhh!, 1, 2.5, z z, 2.5, 1, aaaahhhhh!
The final version, pushToCout3, improves the design by passing an unspecified lambda into pushToCout3 so an application can provide its own preferred operation.
Example: pushToCout3 - accepts function object Code: push to std::cout ver3 auto makeHelper( std::string prefix = ", ", std::string firstPrefix = "" ) { int debug = 0; auto f = [prefix, firstPrefix](auto a) mutable { static std::ostringstream out2; out2 << a; std::string temp = firstPrefix + out2.str(); out2.str(""); firstPrefix = prefix; return temp; }; debug++; return f; template<typename F, typename ...Args> void pushToCout3(F f, const Args&... args) { (std::cout << ... << (f(args))); } Using code: displayDemo( "\n -- pushToCout3 -- fold on operator<< --" ); std::cout << "\n "; pushToCout3(makeHelper(), "aaaahhhhh!", 1, 2.5, 'z'); std::cout << "\n "; pushToCout3(makeHelper(), 'z', 2.5, 1, "aaaahhhhh!"); Output: -- pushToCout3 -- fold on operator<< -- aaaahhhhh!, 1, 2.5, z z, 2.5, 1, aaaahhhhh!
We've looked at features provided by C++ to build generic functions aided by template metaprogramming constructs. Now it's time to turn to template metaprogramming for classes.

8.2 Variadic Classes

Variadic template classes build instantiations with a finite arbitrary number of class parameters. We specify a list of template parameter types with typename... Args and a list of template function arguments with Args... args. The first example constructs a derived class that inherits an arbitrary number of unspecified classes. That supports building a customizable product that can mixin features as needed by an application. Over time we build up a set of base capabilities that are reused for each new product.
Variadic Template Class Mixins Fig 1. Mixin Hierarchy The top half of the left panel presents code for three test classes A, B, and C. They have no significance other than supporting the demonstration of mixins. Class D is the class that inherits an arbitray number of mixin classes. A use-case for this structure is the development of code for a product family that has several different features. So A, B, and C are feature classes and D represents the core product. Each member of the product family would use different feature classes. One product member might use A and D. Another might use B and C and D. This structure supports a very graceful evolution of products. You need a new feature? Add a new feature class and mixin with any existing features needed. Code: Template Mixins #include <iostream> namespace Chap7 { class A { public: A() { std::cout << "\n an A here"; } A(const A& a) { std::cout << "\n copy A here"; } virtual ~A() {} }; class B { public: B() { std::cout << "\n a B here"; } B(const B& b) { std::cout << "\n copy B here"; } virtual ~B() {} }; class C { public: C() { std::cout << "\n a C here"; } C(const C& c) { std::cout << "\n copy C here"; } virtual ~C() {} void do_C_op() { std::cout << "\n doing C's operation"; } }; template<typename... Mixins> class D : public Mixins... { private: bool hasMixins = false; public: // D() fails to compile because same as // D(const Mixins& ... mixins) // when there are no mixins // D() { // std::cout << "\n a D here"; // } D(const Mixins& ... mixins) : Mixins(mixins)... { if (sizeof...(mixins) == 0) std::cout << "\n a D here"; else { hasMixins = true; std::cout << "\n copy D here"; } } virtual ~D() {} void say() { if (!hasMixins) std::cout << "\n class D with no mixins here"; else { std::cout << "\n " << typeid(this).name(); //std::cout << "\n " // << typeid(this).raw_name(); } C* pC = dynamic_cast<C*>(this); if (pC) pC->do_C_op(); std::cout << "\n"; } }; } Using Code: #include "Chap7VariadicClassMixins.h" using namespace Chap7; int main() { std::cout << "\n Variadic mixin template classes"; std::cout << "\n =================================\n"; A a; B b; C c; D<> d; d.say(); D<A> dA(a); dA.say(); D<B, C> dBC(b, c); dBC.say(); // Will fail to compile // - incomplete argument list // D<B, C> dBC(b); std::cout << "\n\n"; return 0; } Output: Variadic mixin template classes ================================= an A here a B here a C here a D here class D with no mixins here copy A here copy D here class Chap7::D<class Chap7::A> * __ptr64 copy B here copy C here copy D here class Chap7::D< class Chap7::B,class Chap7::C > * __ptr64 doing C's operation
The next example creates a class with an arbitrary number of composed members of unspecified classes. Where the mixin example demonstrated composing functionality from base mixins, this example demonstrates composition of data into a heterogeneous data structure.
Variadic Template Class Compositions Some of these ideas are explored in: riptutorial.com [ can we generate getter setter functions? ] Fig 2. Composition Structure In the left panel we have:
  1. Declaration of a generic DataStructure
  2. Forward declaration of a helper function, used to retrieve data from the structure
  3. A specialization of the DataStructure that provides for storage and retrieval of a finite number of composed classes.
  4. Definitions of a helper function that gets composed items by index (similar to the mechanism used by std::tuple)
A use case for this is a help system. Suppose we are building a mixin product family like the one in the previous example. We want to provide a main help for the core product in DataStructure and then plug in help modules for each product feature class included in the product. Here, we will simply define plug in modules A, B, C, etc. and plug in the ones that match our product. Code: Variadic Class Composition template<typename ... T> struct DataStructure {}; template<size_t id, typename T> struct GetHelper; template<typename T, typename ... Rest> struct DataStructure<T, Rest ...> { DataStructure( const T& first, const Rest& ... rest ) : first(first), rest(rest...) {} T first; DataStructure<Rest ... > rest; template<size_t id> auto get() { return GetHelper< id, DataStructure<T, Rest...> >::get(*this); } }; template<typename T, typename ... Rest> struct GetHelper< 0, DataStructure<T, Rest ... > > { static T get( DataStructure<T, Rest...>& data ) { return data.first; } }; template< size_t id, typename T, typename ... Rest > struct GetHelper< id, DataStructure<T, Rest ... > > { static auto get( DataStructure<T, Rest...>& data ) { return GetHelper< id - 1, DataStructure<Rest ...> >::get(data.rest); } }; Using Code: #include "Chap7VariadicClassComp.h" #include "../Display/Display.h" #include <iostream> #include <vector> int main() { displayTitle( "Variadic Class Data by Composition" ); DataStructure< int, float, std::string > data1(1, 2.1, "Hello"); std::cout << "\n " << data1.get<0>(); std::cout << "\n " << data1.get<1>(); std::cout << "\n " << data1.get<2>(); putline(); DataStructure< int, float, std::string > data2{ 2, 3.1, "Hello again" }; std::cout << "\n " << data2.get<0>(); std::cout << "\n " << data2.get<1>(); std::cout << "\n " << data2.get<2>(); putline(); DataStructure<std::vector<int>> data3(std::vector<int>{1, 2, 3, 4, 5}); std::vector<int> out = data3.get<0>(); std::cout << "\n "; for (auto item : out) { std::cout << item << " "; } putline(2); return 0; } Output: Variadic Class Data by Composition ==================================== 1 2.1 Hello 2 3.1 Hello again 1 2 3 4 5

8.2.1 Variadic Class Syntax

Variadic class syntax is the same parameter pack syntax used for functions, simply adapted to the class template syntax.
Code fragment from 1st example: template<typename... Mixins> class D : public Mixins... { private: bool hasMixins = false; public: D(const Mixins& ... mixins) : Mixins(mixins)... { if (sizeof...(mixins) == 0) std::cout << "\n a D here"; else { hasMixins = true; std::cout << "\n copy D here"; } } virtual ~D() {} void say() { if (!hasMixins) std::cout << "\n class D with no mixins here"; else { std::cout << "\n " << typeid(this).name(); } C* pC = dynamic_cast<C*>(this); if (pC) pC->do_C_op(); std::cout << "\n"; } }; Syntax Explained: The parameter pack typename... Mixins declares a list of a finite arbitrary number of unspecified class types to be used by class D. The second line in the left panel declares that D inherits from all of those classes. Other uses of variadic class templates could use them in other ways. The constructor D(const Mixins& ... mixins) : Mixins(mixins)... Accepts an initializing value for each mixin class and calls its constructor in an initialization sequence, the last part of the expression above. The ellipsis, "..." causes an expansion of the parameter packs, e.g., Mixins& ... mixins expands into a comma separated list of arguments, Mixin1& mixin1, Mixin2& mixin2, ... The term Mixins(mixins)... expands into a comma separated list of Mixin1(mixin1), Misin2(mixin2), ... This is just what we need to create a valid constructor syntax.

8.3 Compile-Time Entities

Implementing templates required C++ compilers to develop a compile-time functional language. That language handles types, type lists, and constant integral values while compiling template code. The template metaprogramming language (TMPL) needs representations for types, constant integral values and compile-time expressions (an association of a type and a constant value). Let's see how that can be done. Type Representation: template<typename T> struct TypeRep { using type = T; }; Using Code: std::cout << "\n " << typeid(TypeRep<double>::type).name(); Output: double The declaration using type = T; creates a type member in TypeRep accessed by the expression TypeRep<T>::type. The code listed in the top panel on the right is generated at compile-time and executed at run-time. Why do we need this? We'll see in several examples to come. Value Representation: template<typename T, T v> struct ValueRep { static constexpr T value = v; }; Using Code: std::cout << "\n " << ValueRep<int, 3>::value; Output: 3 Values at compile-time have to be integral constants, e.g., ints, long ints, pointers, etc. The constexpr qualifier declares that T is one of these. If not, compilation will fail. Expression Representation: template<typename T, T v> struct ExpressionRep { using type = T; static constexpr T value = v; constexpr operator T() { return v; } }; Using Code: std::cout << "\n " << typeid(ExpressionRep<int, 3>::type).name() << ", " << ExpressionRep<int, 3>::value; Output: int, 3 All C++ expressions have types and most have values. The template struct ExpressionRep combines TypeRep and ValueRep to represent expressions at compile-time. It also includes something new - a constexpr function operator T() which is a cast that can execute at compile-time. constexpr functions must use only types and values that can be represented at compile-time in order to run at compile-time. Otherwise the compiler generates code to execute them at run-time. Here's another example that adds two more constexpr functions. Another Expression Representation: template<typename T, T v> struct ExpRep2 { using type = T; static constexpr T value = v; constexpr operator T() { return v; } static constexpr T get() { return v; } static constexpr T addOne() { return (v + 1); } static constexpr T subOne() { return (v - 1); } }; Using Code: constexpr int v = 0; std::cout << "\n " << typeid(ExpRep2<size_t, v>).name() << ", " << ExpRep2<size_t, v>::get(); std::cout << "\n " << ExpRep2<int, v>::addOne(); std::cout << "\n " << ExpRep2<int, v>(); std::cout << "\n " << ExpRep2<int, v>::subOne(); Output: struct ExpRep2, 0 1 0 -1 The addition of these two constexpr functions are not very useful except to illustrate how compile-time functions work. Again, remember that the compiler generated this result at compile-time and generated code at compile-time to display it at run-time.

8.3.1 Standard Compile-Time Entities

Standard C++17 defines a number of useful compile-time entities. Here's a complete list of entities in the <type_traits> library: cppreference.com. One important entity is a standard representation for compile-time expressions, integral_constant, similar to the ExpressionRep we looked at at the beginning of this section. std::integral_const     cplusplus.com template <class T, T v> struct integral_constant { static constexpr T value = v; typedef T value_type; typedef integral_constant<T,v> type; constexpr operator T() const noexcept { return v; } constexpr T operator()() const noexcept { return v; } }; This struct serves as the base for a number of representation classes used in TMP. It adds two things to note: a definition of type that is equivalent to itself and an operator()() that makes it a functor, i.e., a callable object. The next example uses integral_const to define a type trait is_void. Remember that integral_const is a struct that has a value member. We define aliases false_type and true_type to be structs containing values that are false and true respectively. We then define a generic version of is_void<T> that inherits from false_type so it contains a false value and a specialization is_void<void> that inherits from true_type and so contains a true value. Now, we can use this trait to test at compile time whether a type is void or not, as shown below. Defining a type trait: typedef integral_const<bool, false> false_type; typedef integral_const<bool, true> true_type; /* generic type is false */ template<typename T> struct is_void : std::false_type {}; /* specialization for void is true */ template<> struct is_void<void> : std::true_type {}; Using Code: std::cout << "\n " << is_void<void>::value; std::cout << "\n " << is_void<int>::value; Output: true false std::is_void<T> is one of the simplest std type traits. The example above shows how it is implemented. All the traits use this approach, but of course some are more complex (see next example). If you look at the code in CppStory repository in folder Chapter7-TypeTraits, you will see all the code presented here and can run it. It just mimics what the standard library code implements.

8.4 Type Traits

Type traits are template expression classes that test for specific type attributes, like is_void, above. Examples from the standard type_traits library are: is_void, is_class, and is_rvalue_reference. Here's a complete list of standard type_traits. The type_traits library does not provide traits to detect STL containers, so we will do one for std::vector in this next example. The code follows closely an example provided in stackoverflow. Custom type_trait is_vector namespace impl { template<typename T> struct is_vector : std::false_type {}; template <typename... Args> struct is_vector< std::vector<Args...> >:std::true_type {}; } template<typename T> struct is_vector { static constexpr bool const value = impl::is_vector<std::decay_t<T >>::value; }; /*----------------------------------------------------- The core part of the definition is in impl namespace. That is then used in the final definition, stripping cvr qualifiers from T with std::decay_t<T>. Namespace impl is needed to avoid an infinite recurse in that final definition fix-up. -----------------------------------------------------*/ Using Code: displayDemo("--- is_vector ---"); std::cout << "\n " << is_vector< std::vector<int> >::value; std::cout << "\n " << is_vector< std::vector<double> >::value; std::cout << "\n " << is_vector< std::unordered_map<int, std::string> >::value; Output: --- is_vector --- true true false We've used a variadic template in the example above so that both std::vector<T> and std::vector<T,Allocator> would be detected. Without the variadic template only the first would be detected. Many of the STL containers have several template arguments, so this solution covers all those cases. For each of the STL containers we can define traits using the above code and merely substitute the name of the container for vector.

8.5 Compile-Time Selections

When writing template code, we often have to perform different operations depending on the types of the template parameters. But when writing the library code we don't know what types the using application has supplied. It is quite common that some operation will compile for some types and not other types. In that case we want to select the code to use based on type traits. However, we can't use run-time selection. When our template code is translated, the compiler can't know what will happen at run-time so all of the code compiled has to be valid for each type used by the application. But that is unlikely to be the case. What we need is a compile-time selection process that will only compile code valid for the applied type and ignore code intended for other types. constexpr if works very well in this situation. constexpr if if constexpr(pred) { /* compiled only if code for pred == true */ } else { /* compiled only if code for pred == false */ } Example: /*--------------------------------- suppose function has to deal with vectors and stacks */ /* assumes v is know in code body */ if constexpr(is_vector<T>) { T.push_back(v); } else { T.push(v); } There is another selection mechanism that works differently. std::enable_if<pred> is a template struct that will have an embedded type if pred evaluates to true and otherwise will not have an embedded type. The idea is to use std::enable_if<predicate> as a template argument. If the predicate is false overload resolution will fail due to the missing type. So overload resolution will only succeed for that overload when the predicate condition is true. Otherwise some other overload is chosen if available, or compilation fails. This example uses code from a post by Eli Bendersky. std::enable_if template < class T, class std::enable_if< std::is_integral<T>::value, T >::type * = nullptr > void do_stuff(T& t) { std::cout << "\n doing integral stuff with type " << typeid(t).name(); } template < class T, class std::enable_if< std::is_class<T>::value, T >::type * = nullptr > void do_stuff(T& t) { std::cout << "\n doing class stuff with type " << typeid(t).name(); } Using Code: displayDemo("--- enable_if ---"); class X {}; X x; int i = 42; do_stuff(i); do_stuff(x); std::string s("a string"); do_stuff(s); Output: --- enable_if --- doing integral stuff with type int doing class stuff with type class `int __cdecl main(void)'::`2'::X doing class stuff with type class std::basic_string<char,struct ...

8.6 TMP Applications

Most of the data generated for this story was displayed using functions with a lot of TMP. All of the display functionality was developed in code for project Chapter7-Display. That depends on traits definitions provided by Chapter7-TypeTraits. We've shown fragments of that below.
Demonstration: Custom Type Traits Chap7TypeTraits.h, Chap7TypeTraits.cpp
Code Fragment: Chap7TypeTraits.h /* ---------------------------------------------- define type traits not supplied by the standard type_traits library */ namespace impl { template<typename T> struct is_pair :std::false_type {}; template <typename... Args> struct is_pair < std::pair<Args...>> :std::true_type {}; template<typename T> struct is_tuple :std::false_type {}; template <typename... Args> struct is_tuple < std::tuple<Args...> > :std::true_type {}; template<typename T> struct is_array :std::false_type {}; template <typename T, size_t N> struct is_array < std::array<T, N>> :std::true_type {}; template<typename T> struct is_basic_string :std::false_type {}; template <typename... Args> struct is_basic_string < std::basic_string<Args...> > :std::true_type {}; template<typename T> struct is_vector :std::false_type {}; template <typename... Args> struct is_vector < std::vector<Args...>> :std::true_type {}; template<typename T> struct is_deque :std::false_type {}; template <typename... Args> struct is_deque < std::deque<Args...>> :std::true_type {}; /* code elided */ template<typename T> struct is_priority_queue :std::false_type {}; template <typename... Args> struct is_priority_queue < std::priority_queue<Args...> > :std::true_type {}; } template<typename T> struct is_pair { static constexpr bool const value = impl::is_pair<std::decay_t<T >> ::value; }; template<typename T> struct is_tuple { static constexpr bool const value = impl::is_tuple<std::decay_t<T >> ::value; }; template<typename T> struct is_string :std::false_type {}; template<> struct is_string<std::string> : std::true_type {}; template<typename T> struct is_array { static constexpr bool const value = impl::is_array<std::decay_t<T >> ::value; }; template<typename T> struct is_basic_string { static constexpr bool const value = impl::is_basic_string< std::decay_t<T > > ::value; }; template<typename T> struct is_vector { static constexpr bool const value = impl::is_vector<std::decay_t<T >> ::value; }; template<typename T> struct is_deque { static constexpr bool const value = impl::is_deque<std::decay_t<T >> ::value; }; /* code elided */ template<typename T> struct is_priority_queue { static constexpr bool const value = impl::is_priority_queue< std::decay_t<T > > ::value; }; template<typename T> using is_seqcont = std::integral_constant< bool, is_array<T>::value || is_string<T>::value || is_basic_string<T>::value || is_vector<T>::value || is_deque<T>::value || is_forward_list<T>::value || is_list<T>::value >; template<typename T> using is_assoccont = std::integral_constant< bool, is_set<T>::value || is_multiset<T>::value || is_map<T>::value || is_multimap<T>::value || is_unordered_set<T>::value || is_unordered_multiset<T>::value || is_unordered_map<T>::value || is_unordered_multimap<T>::value >; template<typename T> using is_adaptercont = std::integral_constant< bool, is_stack<T>::value || is_queue<T>::value || is_priority_queue<T>::value >; Using Code: Chap7TypeTraits.cpp #include "Chap7TypeTraits.h" #include "../Chapter7-Display/Chap7Display.h" /* code elided */ int main() { /* code elided */ displayDemo("--- is_vector ---"); std::cout << "\n " << is_vector<std::vector<int>>::value; std::cout << "\n " << is_vector<std::vector<double>>::value; std::cout << "\n " << is_vector< std::unordered_map<int, std::string> >::value; displayDemo("--- enable_if ---"); class X {}; X x; int i = 42; do_stuff(i); do_stuff(x); std::string s("a string"); do_stuff(s); std::cout << "\n\n"; displayDemo("--- std containers ---"); testSTL_Traits(std::array<int, 1>{1}); testSTL_Traits(std::string{}); testSTL_Traits(std::basic_string<char>{""}); testSTL_Traits(std::vector<int>{}); testSTL_Traits(std::deque<int>{}); testSTL_Traits(std::forward_list<int>{}); testSTL_Traits(std::list<int>{}); testSTL_Traits(std::set<int>{}); testSTL_Traits(std::multiset<int>{}); testSTL_Traits(std::map<int,int>{}); testSTL_Traits(std::multimap<int,int>{}); testSTL_Traits(std::unordered_set<int>{}); testSTL_Traits(std::unordered_multiset<int>{}); testSTL_Traits(std::unordered_map<int, int>{}); testSTL_Traits(std::unordered_multimap<int, int>{}); testSTL_Traits(std::stack<int>{}); testSTL_Traits(std::queue<int>{}); testSTL_Traits(std::priority_queue<int>{}); putline(2); } Output: /* output elided */ --- std containers --- is sequential container is array: class std::array<int,1> is sequential container is std::string: class std::basic_string<char,struct std::char ... is std::basic_string: class std::basic_string<char,struct std::char ... is sequential container is std::string: class std::basic_string<char,struct std::char_ ... is std::basic_string: class std::basic_string<char,struct std::char_ ... is sequential container is std::vector: class std::vector<int,class std::allocator< ... is sequential container is std::deque: class std::deque<int,class std::allocator<i ... is sequential container is std::forward_list: class std::forward_list<in ... is sequential container is std::list: class std::list<int,class std::all ... is associative container is std::set: class std::set<int,struct std::less ... is associative container is std::multiset: class std::multiset<int,struct ... is associative container is std::map: class std::map<int,int,struct std:: ... is associative container is std::multimap: class std::multimap<int,int,st ... is associative container is std::unordered_set: class std::unordered_set< ... is associative container is std::unordered_multiset: class std::unordered_mu ... is associative container is std::unordered_map: class std::unordered_map< ... is associative container is std::unordered_multimap: class std::unordered_mu ... is adapter container is std::stack: class std::stack<int,class std::d ... is adapter container is std::queue: class std::queue<int,class std::d ... is adapter container is std::priority_queue: class std::priority_queue&l ...
The custom type traits demonstrated in the previous example were used to build a very flexible display system. The displayValues function can accept one or more comma separated arguments due to its variadic design. It also accepts some of the STL containers, std::pairs, and std::tuples, due to TMP selections in the body of the function.
Demonstration: Flexible Display using TMP Chap7Display.h, Chap7Display.cpp Code Fragment: Chap7Display.h #include "../Chapter7-TypeTraits/Chap7TypeTraits.h" //#include "../CustomTraits/CustomTraits.h" #include <iostream> #include <typeinfo> inline auto putline = []( size_t n = 1, const std::string& msg = "" ) { for (size_t i = 0; i < n; ++i) { std::cout << "\n"; } if (msg.size() > 0) std::cout << msg; }; inline void displayTitle(const std::string& title) { std::cout << "\n " << title; std::cout << "\n " << std::string(title.size() + 2, '=') << std::endl; } inline void displaySubtitle(const std::string& title) { std::cout << "\n " << title; std::cout << "\n " << std::string(title.size() + 2, '-'); } inline void displayDemo(const std::string& msg) { std::cout << "\n " << msg; } /*---- display selected type values ----*/ // https://stackoverflow.com/questions/1198260/ // how-can-you-iterate-over-the-elements-of-an-stdtuple template<std::size_t I = 0, typename... Tp> inline typename std::enable_if< I == sizeof...(Tp), void >::type displayTuple(const std::tuple<Tp...>& t) { } template<std::size_t I = 0, typename... Tp> inline typename std::enable_if < I < sizeof...(Tp), void >::type displayTuple(const std::tuple<Tp...> & t) { std::cout << std::get<I>(t) << " "; displayTuple<I + 1, Tp...>(t); } template <typename T> void displayValues( const std::initializer_list<T>& lst, const std::string& msg = "", std::string prefix = "\n " ) { for (auto item : lst) { try { if constexpr ( std::is_scalar<T>::value || is_string<T>::value ) { std::cout << prefix << item; } else if constexpr (is_pair<T>::value) { std::cout << prefix << "{ " << item.first << ", " << item.second << " }"; } else if constexpr (is_tuple<T>::value) { std::cout << prefix; displayTuple(item); } else if constexpr (is_vector<T>::value) { for (auto elem : item) { displayValues({ elem }, "", prefix); prefix = ", "; } } else if constexpr ( is_unordered_map<T>::value ) { for (auto elem : item) { std::cout << prefix << "{ " << elem.first << ", " << elem.second << " }"; } } prefix = ", "; if (msg.size() > 0) std::cout << msg; } catch (std::exception & ex) { std::cout << "\n " << ex.what(); } } } /*---- display type sizes ----*/ template<typename T> void displayType( T&& t, const std::string& msg = "", bool showSize = true ) { std::cout << "\n "; if (showSize) std::cout << sizeof(t) << " = size of "; std::string typeName = typeid(t).name(); if (typeName.size() > 75) typeName = typeName.substr(0, 75) + "..."; std::cout << typeName; if (msg.size() > 0) std::cout << msg; } template<typename T> void displayOnlyType( const T& t, const std::string& msg = "" ) { std::cout << "\n "; std::string typeName = typeid(t).name(); if (typeName.size() > 75) typeName = typeName.substr(0, 75) + "..."; std::cout << typeName; if (msg.size() > 0) std::cout << msg; } template<typename T> void displayTypeArgument( const std::string& msg = "", bool showSize = true ) { std::cout << "\n "; if (showSize) std::cout << sizeof(T) << " = size of "; std::string typeName = typeid(T).name(); if (typeName.size() > 50) typeName = typeName.substr(0, 46) + " ..."; std::cout << typeName; if (msg.size() > 0) std::cout << msg; } /*--- variadic display function ---*/ // Template specialization that stops // recursive evaluation template<typename T> void displayValues(T t) { displayValues({ t }); std::cout << "\n"; } // Recursive definition of template function // Args is a "parameter pack // https://en.cppreference.com/w/cpp // /language/parameter_pack template<typename T, typename... Args> void displayValues(T t, Args... args) { displayValues({ t }); displayValues(args...); } Using Code: Chap7Display.cpp #include <tuple> #include "Chap7Display.h" int main() { displayTitle("Testing Chap7Display"); displayDemo("--- displayTuple ---\n "); std::tuple tp{ 1, 2.5 , 'a', "a string" }; displayTuple(tp); putline(1, " "); displayTuple(std::tuple{ 1, 2.5 , 'a', "a string" }); displayDemo("\n --- displayValues ---\n "); displayValues( 1, 3, 4.5, "a string", std::pair{ 1, "one" } ); displayValues( std::vector<int>{-1, 0, 1}, std::unordered_map<int, std::string>{ {1, "one"}, { 2, "two" } } ); displayValues({ 1.0, 1.5, 2.0, 2.5 }); displayDemo("\n --- displayType ---\n "); displayType(1); displayType(2.5); displayType('z'); displayType("another string"); displayDemo("\n --- displayOnlyType ---\n "); displayOnlyType(1); displayOnlyType(2.5); displayOnlyType('z'); displayOnlyType("another string"); displayDemo("\n --- displayTypeArgument ---\n "); displayTypeArgument<int>(); displayTypeArgument<double>(); displayTypeArgument<char>(); displayTypeArgument<std::string>(); putline(2); } Testing Chap7Display ====================== --- displayTuple --- 1 2.5 a a string 1 2.5 a a string --- displayValues --- 1 3 4.5 a string { 1, one } -1, 0, 1 { 1, one } { 2, two } 1, 1.5, 2, 2.5 --- displayType --- 4 = size of int 8 = size of double 1 = size of char 15 = size of char const [15] --- displayOnlyType --- int double char char const [15] --- displayTypeArgument --- 4 = size of int 8 = size of double 1 = size of char 40 = size of class std::basic_string<char...
It wouldn't be difficult to make the displayValues function work for all of the STL containers. The only reason this code doesn't is that I built and used it for display before I finished the type traits shown in the first dropdown.

8.7 Epilogue

I've learned a lot preparing this chapter. I'm relatively satisfied with the material as-is. However, I think with some more-informed design it can be simplified and made more readable. That is on the agenda, but it's time to move on to other things for a while.

8.8 Exercises

  1. Write a numeric formatter that returns the string representation of any of the numeric types and their aggregates, [optionally] in a field with of N characters. so, the string representation for the floating point number 3.5 in a field of 6 might appear as either "3.5   " or as "   3.5".
    Extend the formatter to work with strings, where, if the string is longer than the field width, you trucate to the first N characters, then replace the last three characters in the truncated string with "...", so my name in a field of 8 would appear as "Jim F...".
    What makes this a TMP question is that you need to overload on several types, e.g., double, int[5], ... For example: int iarr[]{ 1, 3, 4 }; format(iarr) --> "[ 1, 3, 4 ]"
  2. Write a set of template function overloads:
      template <typename T>
      void WhoAmI(const T& t)
      {
        ...
      }
    
    that write a message to the console stating that t is one of:
    • fundamental type
    • STL sequentioal container
    • STL associative container

8.9 References

As I started writing this chapter it became evident that to make this really effective I need to know more:
  • Some of the relatively obscure additions to C++14 and C++17
  • Understand some of the more technical, very good, work already done in this area
  • Some of the functional programming ideas that are common knowledge in that community, but not to me
After spending several days digging, I concluded that this is a very deep well. So, I am going to continue working on this chapter, learning as I go, but trying to finish quickly. My intent is to then continue exploring quietly until I know enough to do a very well crafted revision. So I've collected a set of references I've used so far, below. Some I know to be excellent resources, some I'm not so sure about. As I continue to dig, some will disappear, others will appear, so keep posted.
  1. Templates:
    1. Template Normal Programming - Part 1 - Arthur O'Dwyer
    2. Template Normal Programming - Part 2 - Arthur O'Dwyer
    3. Class Template Argument Deduction for Everyone - Stephan T. Lavavej
    4. How to Use Class Template Argument Deduction - Stephan T. Lavavej
  2. Template Metaprogramming:
    1. More variadic templates - arne-mertz.de
    2. Tiny Metaprogramming Library - Eric Niebler
    3. Practical uses for variadic templates - Craig Scott
    4. Variadic templates in C++ - Eli Bendersky
    5. Modern C++ Features - Variadic Templates - Arne Mertz
    6. C++ Core Guidelines: Rules for Variadic Templates
    7. C++17 - fenbf
    8. constexpr if - fenbf
    9. Fold Expressions - fenbf
    10. Parameter pack - cppreference.com
    11. parameter packs outside of templates - Mike Spertus
    12. TypeLists and a TypeList Toolbox via Variadic Templates - geoyar
    13. C++17 - std::apply() and std::invoke() Rangarajan Krishnamoorthy
    14. Intro to C++ Variadic Templates - Kevin Ushey
    15. How to Make SFINAE Pretty - Part 1, Jonathan Boccara
    16. Modern C++ Interfaces - Stephan Dewhurst
    17. Modern C++ Interfaces (video) - Stephan Dewhurst
    18. type_traits - cppreference.com
    19. any_of
    20. tricks to elegance - Vincent Reverdy
    21. is an STL container - stackoverflow
    22. type_traits - cplusplus.com
    23. type_traits - cppreference.com
  3. Modern C++
    1. modern-cpp-features - Anthony Calandra
    2. C++17 - fenbf
    3. Strong Types by Struct - Vincent Zalzal
    4. Simplify C++ - arne-mertz.de
    5. abseil-cpp
    6. The Old New Thing - Raymond Chen
    7. (All The) STL Algorithms - Jonathan Boccara
    8. C++ Weekly
    9. Lambda Expressions in C++ - MSDN
  4. Functional Programming:
    1. Monads explained in C# (again)
    2. What Does Haskell Have to Do with C++?
    3. Category Theory for Programmers - Bartosz Milewski
    4. (video playlist) Category Theory for Programmsers - Bartosz Milewski
    5. FC++ Library - McNamara & Smaragdakis
    6. Functional programming in C++ - Madhukara Phatak
    7. Phatak Repository
    8. Functional Programming in C++ - Mehreen Tahir
    9. Generic Lazy Evaluation in C++ - Ali AslRousta
    10. LIBF++ 0.2 -- C++ as a Pure Functional Language - GJDuck
    11. The Definition of Functional Programming - modernescpp.com
    12. Functional Programming Paradigm - geeksforgeeks.org
    13. What is Functional Programming? - Guru99.com
  Next Prev Pages Sections About Keys