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:
-
compile template code with unspecified template parameter A:
template<class A> class X { ... };
-
compile instantiations of the template with a defined type AppClass:
X<AppClass> x;
Template Metaprograms run during the second compilation phase.
Quick Starter Example
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 <vector>
#include <unordered_map>
#include <string>
int main() {
displayTitle("Demo Variadic Display Function");
std::vector<int>vInt{ 1,2,3,4 };
std::cout << "\n ";
print_container(vInt);
std::unordered_map<int, std::string>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:
-
((args) || ...) -- unary right fold
-
(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 operato<< --
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:
-
Declaration of a generic DataStructure
-
Forward declaration of a helper function, used to retrieve data from the structure
-
A specialization of the DataStructure that provides for storage and retrieval of
a finite number of composed classes.
-
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
-
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 ]"
-
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.
-
Templates:
-
Template Normal Programming - Part 1 - Arthur O'Dwyer
-
Template Normal Programming - Part 2 - Arthur O'Dwyer
-
Class Template Argument Deduction for Everyone - Stephan T. Lavavej
-
How to Use Class Template Argument Deduction - Stephan T. Lavavej
-
Template Metaprogramming:
-
More variadic templates - arne-mertz.de
-
Tiny Metaprogramming Library - Eric Niebler
-
Practical uses for variadic templates - Craig Scott
-
Variadic templates in C++ - Eli Bendersky
-
Modern C++ Features - Variadic Templates - Arne Mertz
-
C++ Core Guidelines: Rules for Variadic Templates
-
C++17 - fenbf
-
constexpr if - fenbf
-
Fold Expressions - fenbf
-
Parameter pack - cppreference.com
-
parameter packs outside of templates - Mike Spertus
-
TypeLists and a TypeList Toolbox via Variadic Templates - geoyar
-
C++17 - std::apply() and std::invoke() Rangarajan Krishnamoorthy
-
Intro to C++ Variadic Templates - Kevin Ushey
-
How to Make SFINAE Pretty - Part 1, Jonathan Boccara
-
Modern C++ Interfaces - Stephan Dewhurst
-
Modern C++ Interfaces (video) - Stephan Dewhurst
-
type_traits - cppreference.com
-
any_of
-
tricks to elegance - Vincent Reverdy
-
is an STL container - stackoverflow
-
type_traits - cplusplus.com
-
type_traits - cppreference.com
-
Modern C++
-
modern-cpp-features - Anthony Calandra
-
C++17 - fenbf
-
Strong Types by Struct - Vincent Zalzal
-
Simplify C++ - arne-mertz.de
-
abseil-cpp
-
The Old New Thing - Raymond Chen
-
(All The) STL Algorithms - Jonathan Boccara
-
C++ Weekly
-
Lambda Expressions in C++ - MSDN
-
Functional Programming:
-
Monads explained in C# (again)
-
What Does Haskell Have to Do with C++?
-
Category Theory for Programmers - Bartosz Milewski
-
(video playlist) Category Theory for Programmsers - Bartosz Milewski
-
FC++ Library - McNamara & Smaragdakis
-
Functional programming in C++ - Madhukara Phatak
-
Phatak Repository
-
Functional Programming in C++ - Mehreen Tahir
-
Generic Lazy Evaluation in C++ - Ali AslRousta
-
LIBF++ 0.2 -- C++ as a Pure Functional Language - GJDuck
-
The Definition of Functional Programming - modernescpp.com
-
Functional Programming Paradigm - geeksforgeeks.org
-
What is Functional Programming? - Guru99.com