about
10/25/2022
C++ Story Templates
C++ Story Code C++ Story Docs

Chapter #7 - C++ Templates

template functions and classes

7.0 Prologue

Templates support building parameterized types and functions that accept an unspecified type which gets resolved only when an application instantiates the code with a concrete type. Parameterized code will accept, without compilation error, arbitrary calls on instances of the template type. When an application instantiates the parameterized code with a concrete type, compilation of the instantiated code succeeds if the type supports specified operations that are used by the application, else it fails. So there are two C++ template compilation phases:
  1. Compilation of the template library code does a syntax check to identify known errors, but No object code is generated in this first phase since the type of the template parameter is not specified.
  2. Compilation of instantiated application code now has the template argument specification and generates an object file if instantiated syntax is correct, otherwise it fails.
Compilation of templates does not include any function or statement that isn't used. This lazy translation is very useful. We compile the template library successfully and defer translation of functions to the second phase. If the instantiated type has an appropriate method compilation succeeds. If the instantiated type does not have a method used by the template but that method is never called, the translation succeeds. It fails only for called methods that are not matched in the instantiated type(s).
An implication of lazy compilation is that all of the template definition code must go in a header file, because the application that uses the template definition has to see all of it's code to compile successfully, and it gets that only by including a header file.
Quick Starter Example - Lazy Template Compilation
"You only pay for what you need"
- Liberty Mutual advertisement
This example builds two classes: Parameter, used to provide a template parameter, and LazyDemo which uses that type. The LazyDemo<P> class has two methods: LazyDemo<P>::say() which calls Parameter::say() and LazyDemo<P>::shout() which calls Parameter::shout().
If we comment out Parameter::shout(), the code will compile provided that it does not call LazyDemo::shout(), even though LazyDemo::shout() depends on Parameter::shout()! That's because a template method that is never called won't be compiled. You can demonstrate that for yourself by looking at the project's assembly code. That's fairly large due to its include files, but you can just search on "shout" and discover that it isn't in the assembly. Code: Demo of Lazy Template Compil'n class Parameter { public: void say() { std::cout << "\n Parameter here"; } /*----------------------------------- when commented out, illustrates lazy compilation */ //void shout() { // std::string msg = // "\n [very loudly] "; // msg += "Parameter here"; // std::cout << msg; //} }; template<typename P> class LazyDemo { public: void say() { std::cout << "\n LazyDemo here with "; p_.say(); } void shout() { std::string msg = "\n [very loudly] "; msg += "LazyDemo here with "; std::cout << msg; p_.shout(); } private: P p_; }; Using Code int main() { displayDemo("-- Lazy Templ Compl --"); LazyDemo<Parameter> ld; ld.say(); /*----------------------------------- Uncommenting next line will cause compilation error, because Parameter does not define shout. It's not compiled if it isn't used. */ //ld.shout(); std::cout << "\n\n"; } Output -- Lazy Templ Compil -- LazyDemo here with Parameter here Lazy template compilation is very important. It allows us to build template classes that, for some parameters, may want to call a method on that parameter, but for other parameters it still works as long as we don't call a missing method. I've used this effect in several of the demo codes for this story and other codes in the C++ Repository, most notably the Property<T> class example, presented later in this chapter. Conclusion: Lazy template compilation makes template classes very forgiving of their parameter types. If a type doesn't have a method that's used for other parameter types, that causes no problems as long as we don't call the missing method.
C# and Java generics do eagar type checking so many generic operations that would succeed for useful types are not allowed because the compiler can't ensure that they will succeed. You have to use a constraint that requires the generic class to implement an interface with the required method.

7.1 Template Functions

Template function syntax may take one of several forms: template<typename T> void f(T t) { ... } template<class R, class A1, class A2, class A3> R g(A1 a1, A2& a2, A3&& a3) { ... } template<typename T> T h() { // t ε T defined in function scope // then returned } For the first two forms using code provides arguments and the compiler will infer the types and compile a function using those types. In the second form function g takes arguments: a1 by value, a2 by lvalue reference (&), and a3 by rvalue reference (&&). In that second form we've used "class" instead of "typename". Both forms are supported, but typename is preferred syntax. Passing by value makes a copy of the argument in the function's stackframe. Passing with an lvalue reference creates a reference in the function's stackframe, bound to the argument in the caller's scope. lvalue references will not bind to non-const temporaries. rvalue references behave like lvalue references except that they can bind to a temporary, e.g., an rvalue, and are used most frequently with move operations. For the third case, type inference is not possible from the arguments, as there are none, and the compiler does not attempt to analyze the body code to figure that out, so we have to help by providing the type we want, as shown in the last line of code, below. int i{ 3 }; f(i); double d{ 3.1415927 }; const std::string s = "a demo string"; int j = g<int>(d, s, 'z'); std::string s = h<std::string>(); For the first case the compiler knows the type of i and will compile f as if the function was defined as: void f(int i) { ... } and does the same thing with the remaining two cases.

7.2 Overloading Template Functions

Template functions provide generic recipes for their operation. That is often all we need for a program we are implementing. However, frequently we need to handle a range of types, some of which may not compile or operate correctly with the generic pattern. In those cases we look for a function overload that will work correctly. In the code below, we've expanded the T max(T t1,T t2) example from Chapter #1 by allowing the two arguments to have different types. That results in some interesting processing activities. Looking at the output of the using code, we see that:
  1. In the first use case, the arguments are 4 and 2, each inferred to be int and the generic version is compiled for that case.
  2. In the second case, the arguments are 3.5, a double, and 2L, a long int. The C++ language supports comparing a value of double with an integral value, so the generic template is compiled.
  3. For the third case the arguments are 3.5 and 4L. When compared, the 4L is promoted to a double with value 4 and that is returned. Note that the return type is double, not long int. Since there is only one type for the return value, that is inferred to be the more inclusive type.
  4. The fourth case matches the overload, and the C++ language guarantees that if an overload matches it will be the form compiled.
  5. The fifth and last case has arguments of std::string and const char*. That cannot match the overload, but the generic version works correctly. The string is compared to the const char* value by promoting the const char* to a std::string using a string constructor, and the two strings are compared. The larger value, passed as the const char*, is returned as a std::string.
Using two unique parameter types allows max to compile successfully in cases that would fail if we required both arguments to be the same type. Only the first and fourth examples, below, will compile for the original T max(T t1, T t2) function. Returning the result as an auto gets around the problem of how to state the return type if more than one type could be returned, e.g., T1 or T2. Template Code template<typename T1, typename T2> auto max(T1 t1, T2 t2) { displayDemo("--- using generic template ---"); return t1 > t2 ? t1 : t2; } using pChar = const char*; auto max(pChar s1, pChar s2) { displayDemo("--- using overload for const char* ---"); return ((strcmp(s1, s2) > 0) ? s1 : s2); } Output Demonstrate Template Functions ================================ --- using generic template --- max(4,2) returns 4 the return type of the last statement is: int --- using generic template --- max(3.5, 2L) returns 3.5 the return type of the last statement is: double --- using generic template --- max(3.5, 4L) returns 4 the return type of the last statement is: double --- using overload for const char* --- max("aardvark", "zebra") returns zebra the return type of the last statement is: char const * --- using generic template --- max(std::string("a string"), "b string") returns "b string" the return type of the last statement is: class std::basic_string< char,struct std::char_traits<char>, class std::allocator<char> >
Using Code auto test1 = max(4, 2); std::cout << "\n max(4,2) returns " << test1; auto test2 = max(3.5, 2L); std::cout << "\n max(3.5, 2L) returns " << test2; std::cout << "\n the return type of the last statement is: "; std::cout << typeid(test2).name(); auto test3 = max(3.5, 4L); std::cout << "\n max(3.5, 4L) returns " << test3; std::cout << "\n the return type of the last statement is: "; std::cout << typeid(test3).name(); auto test4 = max("aardvark", "zebra"); std::cout << "\n max(\"aardvark\", \"zebra\") returns " << test4; std::cout << "\n the return type of the last statement is: "; std::cout << typeid(test4).name(); auto test5 = max(std::string("a string"), "b string"); std::cout << "\n max(std::string(\"a string\"), \"b string\") returns " << "\"" << test5 << "\""; decltype(test5) what; std::cout << "\n the return type of the last statement is:"; std::cout << "\n " << typeid(what).name();
To understand how template functions work, we need to look at type categories and type inference, the process compilation uses to establish the types that arguments take after being passed to template functions.

7.2.1 Type Categories

Before C++11, there were only two type categories, lvalue and rvalue:
  1. lvalue refers to any named variable, e.g., anything for which we can evaluate an address. It was called lvalue because it could by used on the left side of an assignment expression: char ch = 'z'; Here, ch is an lvalue.
  2. rvalue refers to any entity for which you cannot evaluate an address, as it has no name and is usually a temporary. rvalues can only appear on the right side of an assignment expression, e.g., the 'z' of the previous item.
Things are, since C++17, somewhat more complicated. rvalues split into xvalues and prvalues:
  1. An xvalue is an entity that can be the source of a move operation. These are almost always values of temporary objects created in the scope of some function, and are syntactically returned by value. An xvalue is not copied, but instead moved by transferring its resources to the entity that is the target of the return operation.
  2. A prvalue is an expression that defines the initialization of the destination. Under return value optimization the prvalue initializes the creation of a target instance. When rvo does not apply, the prvalue initializes an xvalue as part of a move operation, provided the returned object has a move constructor and move assignment operator.
For most of your implementations, you won't have to think about these categories unless you are writing code that runs partially at compile-time, e.g., template metaprogramming.

7.2.2 Type Transformations

When passing arguments to functions, the result of template type deduction is not necessarily the same as the type of the argument in the caller's scope. This is not an inference error, but an intentional transformation made for reasons of performance or usability. For non-template functions there is only one such transformation. That occurs for array arguments. When the function's stackframe is constructed, a pointer is created in stack memory, bound to the first element of the array, instead of copying the array. All access to the array, within the function, accesses the caller's array via that pointer. It is common to refer to this process as type decay. That is, the array type decays to a pointer type in the function's scope. For template functions, transformations of function argument types are more complicated. The most important of these are:
  1. passing a template parameter by value will strip off constant, volatile, and reference (cvr) qualifiers
  2. lvalue references (T&) can bind only to lvalues and const rvalues. The resulting type is always an lvalue reference. C++ classifies literal strings as lvalues. All other literals, e.g., 42 and 3.14159, are classified as rvalues.
  3. rvalue references (T&& in a context with no type deduction) can bind only to rvalues.
    • Widget&& w = createWidget();
    • void f(Widget&& w);
  4. universal a.k.a. forwarding references (T&& in a context with type deduction) can bind to anything.
    • template<typename T>
      void f(T&& t);
    • auto&& x1 = x2;
For the details look here:
Type Transformation Details In this section we will look at type transformations that occur when passing arguments to functions, especially template functions. Unfortunately you cannot use the C++ typeid operator to explore these effects. We see below, that passing an argument to a template function by value strips its constant, volatile, and reference (cvr) qualifiers. But typeid is a template function, so it will strip off some of the things we are looking for. The Boost libraries have a work-around with their boost::typeindex::type_id_with_cvr() operator1. I've used that for all of the type analysis reported below. We see, from Table 1., that in all non-template cases tested, the only case with type decay is the array argument. This is true in general, e.g., for non-template functions, the only type decay that happens is with array arguments.

Table 1. - Type of arg in bodies of non-template functions

arg definition f(int arg) f(const int* arg) f(int arg[3])
int i{ 3 }; int NA NA
const int& j = &i; int NA NA
const int* pI = &i; NA int const * NA
int iarr[4] NA NA int *
42 int NA NA
Template functions are a different story. When compiling an instantiated template, the compiler infers argument types from syntax of invocations. That inference has several rules:
  1. Passing an argument by value removes all const, volatile, and reference (cvr) qulifiers. Array arguments decay to pointers.
  2. Passing an argument by lvalue reference (T&) doesn't remove qualifiers and arrays do not decay (because they won't be copied). An lvalue reference can only bind to lvalues or const rvalues. Note that C++ classifies literal strings as lvalues. All other literals, e.g., ints and doubles, are rvalues.
  3. Passing an argument by pointer doesn't remove qualifiers, but arrays do decay (because the argument is a pointer).
  4. Passing an argument by universal reference (T&& where T is being deduced) doesn't strip qualifiers and arrays do not decay. Universal references can bind to both lvalues and rvalues.

Table 2. - Type Transformations for template functions

arg definition template<class T>
f1(T arg)
template<class T>
f2(T& arg)
template<class T>
f2C(const T& arg)
template<class T>
f3(T* arg)
template<class T>
f3C(const T* arg)
template<class T>
f4(T&& arg)
int i{ 3 };
lvalue
T = int T& = int & const T& = int const & NA NA T&& = int &
const int& j = &i;
lvalue
T = int T& = int const & const T& = int const & NA NA T&& = int const &
const int* pI = &i;
lvalue
T = int const * T& = int const * & const T& = int const * const & T* = int const * const T* = int const * T&& = int const * &
iarr[4]
lvalue
T = int * T& = int (&)[4] const T& = int const (&)[4] T* = int * const T* = int const * T&& = int (&)[4]
"a string literal"
lvalue
T = char const * T& = char const (&)[17] const T& = char const (&)[17] T* = const char * const T* = char const * T&& = char const & *
42
rvalue
T = int doesn't compile const T& = int const & NA NA T&& = int &&

  1. Boost libraries are available from their site, boost.org. The use of boost::typeindex::type_id_with_cvr() for this purpose was cited in Effective Modern C++ by Scott Meyers.
Usually you don't have to think about the rules. Inference just works as you would expect. On rare occasions you may need to consider them to understand unexpected compilation or run-time behavior of template code.

7.2.3 Substitution Failure Is Not An Error (SFINAE)

When compiling overloaded template functions argument type deduction may fail for one or perhaps several overloads. These deductions are based on substitutions in each of the overloads. Substitution failure is not a compile error, so the build should succeed unless none of the deductions succeed.
SFINAE Details In the example below, we've supplied two function overloads, each displaying the contents of a collection. The first displays contents of an array, and the second, contents of a vector. In the Using Code main there are three cases:
  1. First case: no SFINAE - show(array) The array matches both overloads, but show(const T (&array)[N]) is a more specific match and is compiled.
  2. Second case: SFINAE with build success - show(vInt) vInt fails to match show(const T (&array)[N]) but does match show(const CÁ cont) and is compiled. SFINAE prevents the substitution failure from causing a compile error.
  3. Third case: SFINAE with build failure not due to substitution - show(3.14159) Substitution of the double 3.14159 into the first overload fails but is not a compile error. Substitution into the second overload succeeds, but the body fails to compile since a double does not satify the requirements of a range-based for loop.
SFINAE Code #include <iostream> #include <vector> #include "../Display/Display.h" template<typename T, size_t N> void show(const T (&array)[N]) { displayDemo("--- show array contents ---"); std::cout << "\n "; for (size_t i = 0; i < N; ++i) std::cout << array[i] << " "; } template<typename C> void show(const C& cont) { displayDemo("--- show container contents ---"); std::cout << "\n "; for (auto item : cont) { std::cout << item << " "; } } Using Code int main() { displayTitle("SFINAE Demo"); std::cout << "\n displaying array:"; double array[5]{ -0.5, 0.0, 0.5, 1.0, 1.5 }; show(array); std::cout << "\n displaying vector"; std::vector<int> vInt{ -1, 0, 1, 2, 3 }; show(vInt); // show(array); // attempted template argument deduction for both show functions // - show(const T (&array)[n]) succeeded, built, and used // - show(const C& cont) succeeded but less specific so not used // show(vInt) // attempted template argument deduction for both show functions // - show(const T (&array)[n]) failed, vInt is not an array, // but this is not a compilation error (SFINAE) // - show(const C& cont) succeeded, built, and used // show(3.14159); // argument deduction for show(const T (&array)[N]) fails // - double is not an array // argument deduction succeeds with show(const C& cont) // - compilation of body for double fails, i.e., // no iterator, begin(), or end() for range-based for std::cout << "\n\n"; } Output SFINAE Demo ============= displaying array: --- show array contents --- -0.5 0 0.5 1 1.5 displaying vector --- show container contents --- -1 0 1 2 3 The point of all this is that you can write function code that works for some types but not for others as long as there is a type deduction for some other function overload that succeeds and provides working code for any of the types your program will use for this set invocations.
We will look at SFINAE for template classes in the next chapter7 - Template Metaprogramming.

7.3 Template Function Examples

There are a number of standard template functions that prove useful, especially for template metaprogramming:

Table 1. - Standard Template Functions

Function name Return Type Semantics
std::move(T& t) static_cast<typename std::remove_reference<T>::type&&(t) Casts t to an r value type, e.g., can be moved from. Nothing is actually moved.
std::forward(T&& t) type of t in the caller's context Used in a function to un-decay T's type1. Revert back to caller's value category (rvalue or lvalue). This is a cast operation. Nothing is actually forwarded.
std::apply<F&& f, Tuple&& t> value returned by f Use tuple items as arguments of function f. The t argument may be a std::tuple, std::array, or std::pair.
std::invoke(F&& f, Args&&... args) std::Invoke_Result_t<F, Args...> Invoke the callable object f with parameters Args.

  1. Remember that passed arguments are always lvalues because they have their parameter name and a location in the function's stackframe.
This next table holds type traits that are often associated with the standard functions cited in the previous table.

Table 2. - Selected type_traits

type_trait Semantics
template<class T> struct decay decay has a public member type which converts T to the decayed type that results from making a call f(T t) inside the scope of f. That is, decay<T>::type is the decayed type.
template<class T> struct remove_reference remove_reference::type evaluates as T with a reference removed if T is a reference type, otherwise evaluates as T.
Next we will look at some example template function designs. The first two come from the CppUtilities repository. Converter<T>::toString(t), in the CodeUtilities folder, converts the value of t ε T to its string representation. And, given the conversion string, ConvStr, Converter<T>::toValue(ConvStr) converts back to a new instance with the original value.
Converter Example Converter function /////////////////////////////////////////////////////// // Converter class // - supports converting unspecified types to and // from strings // - type is convertible if it provides insertion // and extraction operators template <typename T> class Converter { public: static std::string toString(const T& t); static T toValue(const std::string& src); }; //----< convert t to a string >------------------ template <typename T> std::string Converter<T>::toString(const T& t) { std::ostringstream out; out << t; return out.str(); } //----< convert a string to an instance of T >--- /* * - the string must have been generated by * Converter<T>::toString(const T& t) * - T::operator>> must be the inverse of T::operator<< */ template<typename T> T Converter<T>::toValue(const std::string& src) { std::istringstream in(src); T t; in >> t; return t; } Using Code title("test std::string Converter<T>::toString(T)"); std::string conv1 = Converter<double>::toString(3.1415927); std::string conv2 = Converter<int>::toString(73); std::string conv3 = Converter<std::string>::toString("a_test_string plus more"); std::cout << "\n Converting from values to strings: "; std::cout << conv1 << ", " << conv2 << ", " << conv3; putline(); title("test T Converter<T>::toValue(std::string)"); std::cout << "\n Converting from strings to values: "; std::cout << Converter<double>::toValue(conv1) << ", "; std::cout << Converter<int>::toValue(conv2) << ", "; std::cout << Converter<std::string>::toValue(conv3); Output test std::string Converter::toString(T) -------------------------------------------- Converting from values to strings: 3.14159, 73, a_test_string plus more test T Converter::toValue(std::string) ------------------------------------------- Converting from strings to values: 3.14159, 73, a_test_string
Converter is simple because it uses std::ostringstream to convert from values to their string representation, and it uses std::istringstream to convert back. Thus, the std::stringstream library classes do all the heavy lifting. The second example, found in the StringUtilities folder, presents string utility functions trim and split with capabilities similar those in the C# string class.
String Utilities String Utilities Code /* * - remove whitespace from front and back of string argument * - does not remove newlines */ template <typename T> inline std::basic_string<T> trim( const std::basic_string<T>& toTrim ) { if (toTrim.size() == 0) return toTrim; std::basic_string<T> temp; std::locale loc; typename std::basic_string<T>::const_iterator iter = toTrim.begin(); while (isspace(*iter, loc) && *iter != '\n') { if (++iter == toTrim.end()) { break; } } for (; iter != toTrim.end(); ++iter) { temp += *iter; } typename std::basic_string<T>::reverse_iterator riter; size_t pos = temp.size(); for (riter = temp.rbegin(); riter != temp.rend(); ++riter) { --pos; if (!isspace(*riter, loc) || *riter == '\n') { break; } } if (0 <= pos && pos < temp.size()) temp.erase(++pos); return temp; } /*--- split sentinel separated strings into vector of trimmed strings ---*/ template <typename T> inline std::vector<std::basic_string<T>> split( const std::basic_string<T>& toSplit, T splitOn = ',' ) { std::vector<std::basic_string<T>> splits; std::basic_string<T> temp; typename std::basic_string<T>::const_iterator iter; for (iter = toSplit.begin(); iter != toSplit.end(); ++iter) { if (*iter != splitOn) { temp += *iter; } else { splits.push_back(trim(temp)); temp.clear(); } } if (temp.length() > 0) splits.push_back(trim(temp)); return splits; } /*--- show collection of string splits ----------------------*/ template <typename T> inline void showSplits( const std::vector<std::basic_string<T>>& splits, std::ostream& out = std::cout ) { out << "\n"; for (auto item : splits) { if (item == "\n") out << "\n--" << "newline"; else out << "\n--" << item; } out << "\n"; } Using Code #include <cctype> #include <iostream> #include "StringUtilities.h" #include "../CodeUtilities/CodeUtilities.h" #ifdef TEST_STRINGUTILITIES using namespace Utilities; int main() { Title("Testing Utilities Package"); putline(); title("test split(std::string, ',')"); std::string test = "a, \n, bc, de, efg, i, j k lm nopq rst"; std::cout << "\n test string = " << test; std::vector<std::string> result = split(test); showSplits(result); title("test split(std::string, ' ')"); std::cout << "\n test string = " << test; result = split(test, ' '); showSplits(result); putline(2); return 0; } #endif Output Testing Utilities Package =========================== test split(std::string, ',') ------------------------------ test string = a, , bc, de, efg, i, j k lm nopq rst --a --newline --bc --de --efg --i --j k lm nopq rst test split(std::string, ' ') ------------------------------ test string = a, , bc, de, efg, i, j k lm nopq rst --a, -- , --bc, --de, --efg, --i, --j --k --lm --nopq --rst
For our last template function example, the code below shows how to create generic lambdas. They are defined using auto parameters, as shown. This is equivalent to a template function, but doesn't use template syntax. That it is generic is due to type deduction generated for the auto declarators.
Generic Lambda Code Generic Lambda Code auto genericLambda = [](auto arg) { std::cout << "\n the type of genericLambda's arg is: " << typeid(arg).name(); std::cout << "\n arg's value is: " << arg; }; Using Code displaySubtitle("Demo generic lambda"); genericLambda(double{ 3.5 }); genericLambda("this is a string"); Output Demo generic lambda --------------------- the type of genericLambda's arg is: double arg's value is: 3.5 the type of genericLambda's arg is: char const * arg's value is: this is a string
We will look at template functions again, in Chapter #7 - Template Metaprogramming, where we explore some of the functions used to provide displays for code demonstrations throughout this story.

7.4 Template Classes

Template classes use syntax as shown in the blocks below. Everywhere that the class name SynDemo is used as a type it needs the template parameter, e.g., SynDemo<T>. Remember that both template declarations and definitions must appear in the class's header file, as shown below. Here you will find the included file display.h. Template Code in SynDemo.h namespace Chap6 { template<typename T> class SynDemo { public: void value(T t); T value(); private: T t_; }; template<typename T> void SynDemo<T>::value(T t) { t_ = t; } template<typename T> T SynDemo<T>::value() { return t_; } } Using Code in Demo.cpp #include <iostream> #include <string> #include "../Display/Display.h" #include "SynDemo.h" int main() { displayTitle("Demonstrating Template Syntax"); using namespace Chap6; SynDemo<std::string> sd; sd.value("hello world"); std::cout << "\n " << sd.value(); putline(2); } Output Demonstrating Template Syntax =============================== hello world Because of the two phase compilation for templates used by C++, the class's method implementations must be placed in its header file.

7.4.1 Stack<T> Class Example

The Stack<T> class example, below, illustrates this syntax for a class typical of professionally developed code. It also illustrates:
  1. Template members:
    A copy constructor template <class U> stack(const stack<U>&); and assignment operator template <class U> stack<U>& operator=(const stack<U>&); are declared as methods with a potentially different type U. That could be the same as T, providing the usual copy and assignment operations, but could also be different, allowing one, for example, to assign a stack<int> to a stack<double>. The example code illustrates that use. Template type inference is the reason we can design this flexibility.
  2. Inner classes:
    Inner classes aren't often used for C++ programs, but if a parent class needs services of a small and simple class that is specialized for the parent, then using an inner class for the child makes sense. The struct stacknode is an example of that use.
  3. Friend relationships:
    We try to avoid using friend relationships because they expand encapsulation from the granting class to include friends as well. However, when needed it's easy to use, as shown here. This stack<T> class needs to allow template members, cited in the first item, to access its private data. That's because stack<T> is a different class than stack<U>.
This example was adapted from one presented in Effective C++, by Scott Meyers.
Stack Class Stack Code template<class T> class stack { template <class U> friend class stack; private: struct stacknode { T data; stacknode *next; stacknode( const T& newdata, stacknode *nextnode ) : data(newdata), next(nextnode) { } }; stacknode *top; public: stack(); ~stack(); void push(const T& object); T pop(void); void flush(); int size() const; // member templates template <class U> stack( const stack<U>& ); template <class U> stack<T>& operator=( const stack<U>& ); }; // //----< void constructor >------------- template<class T> stack<T>::stack() : top(0) { } //----< destructor >------------------- template <class T> stack<T>::~stack(void) { while (top) { stacknode *next_to_die = top; top = top->next; delete next_to_die; } } //----< push data onto stack >--------- template<class T> void stack<T>::push(const T &object) { top = new stacknode(object, top); } //----< pop data from stack >---------- template <class T> T stack<T>::pop(void) { if (!top) { throw std::out_of_range( "\n attempt to pop empty stack\n" ); } stacknode *save = top; top = top->next; T data = save->data; delete save; return data; } //----< empty stack >------------------ template <class T> void stack<T>::flush() { stacknode* node = top; while(node) { stacknode *next_to_die = node; node = node->next; delete next_to_die; } } //---< number of elements on stack >--- template <class T> int stack<T>::size() const { stacknode* node = top; int count = 0; while(node) { count++; node = node->next; } return count; } // //--< copy and promo ctor, a member template >-- template <class T> template <class U> stack<T>::stack( const stack<U>& s ) : top(0) { stack<U>::stacknode* node = const_cast<stack<U>::stacknode*>(s.top); while(node) { this->push(node->data); node = node->next; } } //--< assignment from stack of compatible type >-- template <class T> template <class U> stack<T>& stack<T>::operator=(const stack<U>& s) { if((void*)this == (void*)&s) return *this; flush(); stack<U>::stacknode* node2 = const_cast<stack<U>::stacknode*>(s.top); while(node2) { this->push(static_cast<T>(node2->data)); node2 = node2->next; } return *this; } Using Code #include <iostream> #include "stack.h" using namespace std; template <class T> void print_field(T t) { cout.width(10); cout << t; } //----< test stub >-------------------- void main() { cout << "\nTesting Template Based Stack Class\n"; try { stack<int> int_stack; stack<double> double_stack; int x=1, y=2, z=3; double u=-1.5, v=0.5, w=2.5; cout << "\n pushing stack: "; print_field(x); int_stack.push(x); cout << "\n pushing stack: "; print_field(y); int_stack.push(y); cout << "\n pushing stack: "; print_field(z); int_stack.push(z); cout << endl; cout << "\n stack size = " << int_stack.size() << endl; stack<double> copyStack = int_stack; // copy construction with data conversion cout << "\n popping stack: "; print_field(int_stack.pop()); cout << "\n popping stack: "; print_field(int_stack.pop()); cout << "\n popping stack: "; print_field(int_stack.pop()); cout << "\n"; cout << "\n stack size = " << int_stack.size() << endl; cout << "\n popping double copy of int stack:"; cout << "\n popping stack: "; print_field(copyStack.pop()); cout << "\n popping stack: "; print_field(copyStack.pop()); cout << "\n popping stack: "; print_field(copyStack.pop()); cout << "\n"; cout << "\n pushing stack: "; print_field(u); double_stack.push(u); cout << "\n pushing stack: "; print_field(v); double_stack.push(v); cout << "\n pushing stack: "; print_field(w); double_stack.push(w); cout << endl; stack<int> int2_stack; int2_stack = double_stack; // assignment with data conversion cout << "\n popping stack: "; print_field(double_stack.pop()); cout << "\n popping stack: "; print_field(double_stack.pop()); cout << "\n popping stack: "; print_field(double_stack.pop()); cout << "\n"; cout << "\n popping int copy of double stack:"; cout << "\n popping stack: "; print_field(int2_stack.pop()); cout << "\n popping stack: "; print_field(int2_stack.pop()); cout << "\n popping stack: "; print_field(int2_stack.pop()); cout << "\n"; int2_stack.pop(); // popping empty stack cout << "\n\n"; } catch(exception& ex) { cout << "\n " << ex.what() << endl; } catch(...) { cout << "\n stack error\n\n"; } } Output Testing Template Based Stack Class pushing stack: 1 pushing stack: 2 pushing stack: 3 stack size = 3 popping stack: 3 popping stack: 2 popping stack: 1 stack size = 0 popping double copy of int stack: popping stack: 1 popping stack: 2 popping stack: 3 pushing stack: -1.5 pushing stack: 0.5 pushing stack: 2.5 popping stack: 2.5 popping stack: 0.5 popping stack: -1.5 popping int copy of double stack: popping stack: -1 popping stack: 0 popping stack: 2 attempt to pop empty stack
You won't use this class for your own designs. The Standard Template Library (STL) provides a stack adapter class1 and C++ developers know how that works, so you should use it. The STL provides a rich set of examples of template classes including one for each of the STL containers. You will find a demonstration of each of them in STL-Containers.html in the Demonstrations part of the CppRepositories.html.

7.4.2 Directory Explorer Example

The next example, DirExplorerT, shows how to build a component - a directory navigator - to be reusable, e.g., can be used in many different applications without changing any of its code. The way that is done is to parameterize the navigator on an application specific class, DirExplorerT<App>. The application class is expected to provide methods doFile(const std::string& fileName) and doDir(const std::string& dirName) that handle all application specific requirements for using file and directory information.
DirExplorerT Code: DirExplorerT #include <vector> #include "../FileSystem/FileSystem.h" namespace FileSystem { template<typename App> class DirExplorerT { public: using patterns = std::vector<std::string>; static std::string version() { return "ver 1.2"; } DirExplorerT(const std::string& path); void addPattern(const std::string& patt); void hideEmptyDirectories(bool hide); void maxItems(size_t numFiles); void showAllInCurrDir(bool showAllCurrDirFiles); bool showAllInCurrDir(); void recurse(bool doRecurse = true); void search(); void find(const std::string& path); bool done(); void showStats(); size_t fileCount(); size_t dirCount(); private: App app_; std::string path_; patterns patterns_; bool hideEmptyDir_ = false; bool showAll_ = false; // show files in current dir // even if maxItems_ exceeded size_t maxItems_ = 0; size_t dirCount_ = 0; size_t fileCount_ = 0; bool recurse_ = false; }; //---< ctor using default pattern >-- template<typename App> DirExplorerT<App>::DirExplorerT( const std::string& path ) : path_(path) { patterns_.push_back("*.*"); } //---< add patts selecting files >--- template<typename App> void DirExplorerT<App>::addPattern( const std::string& patt ) { if ( patterns_.size() == 1 && patterns_[0] == "*.*" ) patterns_.pop_back(); patterns_.push_back(patt); } //---< option to hide empty dirs >--- template<typename App> void DirExplorerT<App> ::hideEmptyDirectories(bool hide) { hideEmptyDir_ = hide; } //---< max num files to display >---- template<typename App> void DirExplorerT<App>::maxItems( size_t numFiles ) { maxItems_ = numFiles; app_.maxItems(maxItems_); } //---< show all files in dir >------- template<typename App> void DirExplorerT<App> ::showAllInCurrDir( bool showAllCurrDirFiles ) { showAll_ = showAllCurrDirFiles; } //---< show all files in dir? >------ template<typename App> bool DirExplorerT<App> ::showAllInCurrDir() { return showAll_; } //---< recusively walk dir tree >-- template<typename App> void DirExplorerT<App> ::recurse(bool doRecurse) { recurse_ = doRecurse; } //---< start at path_ >------------ template<typename App> void DirExplorerT<App>::search() { if (showAllInCurrDir()) app_.showAllInCurrDir(true); find(path_); } //---< search directories >---------- /* Recursively find all dirs and files on specified path, executing doDir when entering a directory and doFile when finding a file */ template<typename App> void DirExplorerT<App>::find( const std::string& path ) { if (done()) // stop searching return; bool hasFiles = false; std::string fpath = FileSystem::Path::getFullFileSpec(path); if (!hideEmptyDir_) app_.doDir(fpath); for (auto patt : patterns_) { std::vector<std::string> files = FileSystem::Directory ::getFiles(fpath, patt); if (!hasFiles && hideEmptyDir_) { if (files.size() > 0) { app_.doDir(fpath); hasFiles = true; } } for (auto f : files) { app_.doFile(f); } } if (done()) // stop recursion return; std::vector<std::string> dirs = FileSystem::Directory:: getDirectories(fpath); for (auto d : dirs) { if (d == "." || d == "..") continue; std::string dpath = fpath + "\\" + d; if (recurse_) { find(dpath); } else { app_.doDir(dpath); } } } //---< num files processed >--------- template<typename App> size_t DirExplorerT<App>::fileCount() { return App.fileCount(); } //---< num dirs processed >---------- template<typename App> size_t DirExplorerT<App>::dirCount() { return App.dirCount(); } //---< counts for files & dirs >----- template<typename App> void DirExplorerT<App>::showStats() { app_.showStats(); } template<typename App> bool DirExplorerT<App>::done() { return app_.done(); } } Code: Application.h #include <iostream> #include <string> class Application { public: Application(); // App defines handling files and dirs, // when to quit, and how to display final // results. // None of this requires alteration of // DirExplorerT's code. void doFile( const std::string& filename ); void doDir( const std::string& dirname ); size_t fileCount(); size_t dirCount(); bool done(); void showStats(); // configure application options void showAllInCurrDir( bool showAllFilesInCurrDir ); bool showAllInCurrDir(); void maxItems(size_t maxItems); private: size_t fileCount_ = 0; size_t dirCount_ = 0; size_t maxItems_ = 0; bool showAll_ = false; }; inline Application::Application() { std::cout << "\n Using App methods " << doFile and doDir\n"; } inline void Application::doFile( const std::string& filename ) { ++fileCount_; if(showAll_ || !done()) { std::cout << "\n file--> " << filename; } } inline void Application::doDir( const std::string& dirname ) { ++dirCount_; std::cout << "\n dir---> " << dirname; } inline size_t Application::fileCount() { return fileCount_; } inline size_t Application::dirCount() { return dirCount_; } inline void Application::showAllInCurrDir( bool showAllFilesInCurrDir ) { showAll_ = showAllFilesInCurrDir; } inline bool Application::showAllInCurrDir() { return showAll_; } inline void Application::maxItems( size_t maxItems ) { maxItems_ = maxItems; } //---< counts for files and dirs >----- inline void Application::showStats() { std::cout << "\n\n processed " << fileCount_ << " files in " << dirCount_ << " directories"; if(done()) { std::cout << "\n stopped - max num files exceeded"; } } inline bool Application::done() { return (0 < maxItems_ && maxItems_ < fileCount_); } Using Code #include "DirExplorerT.h" #include "Application.h" #include "../StringUtilities/StringUtilities.h" #include "../CodeUtilities/CodeUtilities.h" #include <iostream> #include <string> using namespace Utilities; using namespace FileSystem; std::string customUsage() { /* code elided */ return usage; } int main(int argc, char *argv[]) { Title("Demonstrate DirExplorer-Template"); ProcessCmdLine pcl(argc, argv); pcl.usage(customUsage()); preface("Command Line: "); pcl.showParse(); putline(); if (pcl.parseError()) { pcl.usage(); std::cout << "\n\n"; return 1; } DirExplorerT<Application> de(pcl.path()); for (auto patt : pcl.patterns()) { de.addPattern(patt); } if (pcl.hasOption('s')) { de.recurse(); } if (pcl.hasOption('h')) { de.hideEmptyDirectories(true); } if (pcl.hasOption('a')) { de.showAllInCurrDir(true); } if (pcl.maxItems() > 0) { de.maxItems(pcl.maxItems()); } de.search(); de.showStats(); std::cout << "\n\n"; return 0; } Output Demonstrate DirExplorer-Template ================================== Command Line: Path: . options: patterns: Regex: .* Using Application methods doFile and doDir dir---> C:\github\JimFawcett\CppUtilities... file--> Application.cpp file--> Application.h file--> Application1.cpp.html file--> Application1.h.html file--> DirExplorer-Template-classes.gliffy file--> DirExplorer-Template-classes.jpg file--> DirExplorer-Template.jpg file--> DirExplorer-Template.vcxproj file--> DirExplorer-Template.vcxproj.filters file--> DirExplorer-Template.vcxproj.user file--> DirExplorerT.cpp file--> DirExplorerT.h file--> DirExplorerT1.cpp.html file--> DirExplorerT1.h.html dir---> C:\github\JimFawcett\CppUtilities... processed 14 files in 2 directories
There is a significant amount of code in DirExplorerT to digest, but the effort will show you a very powerful method of building flexible reusable components.
  1. There are two STL adapters, stack<T> and queue<T>. These were built as adapters, wrapping by default the deque<T>, because they intend to provide access only to the top for the stack and both ends for the queue. That behavior voilates the model for STL container classes that provide iteration over entire container contents. So, they are created as adapters that remove support of that model.

7.5 Template Parameters

Template parameters may be:
  1. Type Parameters: typename TypeName [= defaultTypeName]
    TypeName is a formal parameter that represents an unspecified struct or class;
    defaultTypeName is the name of a class or struct.
    Example: template<typename X=std::string> class Y { ... };
  2. Template type parameters: typename TypeName, template<class TypeName> class TemplateTypeName
    TemplateTypeName is a formal parameter that represents an unspecified template struct or class
    Example: template<typename C, template <class C> class X> class Y { ... };
    This specifies that X is a template class that has the parameter C, e.g., the second parameter X is templated on the first parameter C.
  3. Value parameters: Type [= value]
    Example: size_t N = 10
    template<size_t N=10> class Array { ... };
    This specifies that the templated class uses N with value 10 unless specified otherwise.
Type parameters may hold processing that customizes behavior of a template class for specific applications. they may also be used to define specific data structures for an application. Often an application defines type parameters for its unique processing to be used with an existing template class framework. Template type parameters are themselves templates. You will see in the Template Functors example, below, use of a functor, X<C> that is parameterized on C, the type of an STL container. It is used in a traversal function to operate on all the members of the STL container. Value parameters are values rather than classes. The std::Array<T,N> class uses a value parameter, N to define the number of elements it holds. The Template Functors code example, below, has a global function Traverse that is defined with a template parameter, C, and a template template parameter, X<C>: template <class C, template <class C> class X>
void tranverse(
  typename C::iterator& Begin, typename C::iterator& End,
  X<C>& x,
  void(X<C>::*fptr)(typename C::iterator&)
) {
  ...
}
where C represents an STL container, and X is a function object that operates on C in a way specified by the application that defines X<C>.
Template Functors Template Functor Code /////////////////////////////////////////////////////////////// // FunctorsEtc.cpp - Demonstrate Functors, Function Pointers // // with template arguments // // // // Jim Fawcett, CSE67 - Object Oriented Design, Spring 2009 // /////////////////////////////////////////////////////////////// #include <iterator> #include <string> // Functor interface, to support substitutability template <typename C> struct IFunctor { virtual void operator()(typename C::iterator& iter)=0; }; // Global funct accepts iterators & base functor reference, // templatized on a container argument. template <typename C> void Traverse( typename C::iterator& Begin, typename C::iterator& End, IFunctor<C>& funct ) { C::iterator iter; for(iter=Begin; iter!=End; ++iter) funct(iter); } // Global funct accepts iterators & member function pointer // templatized on a container argument template <class C, template <class C> class X> void Traverse( typename C::iterator& Begin, typename C::iterator& End, X<C>& x, void(X<C>::*fptr)(typename C::iterator&) ) { C::iterator iter; for(iter=Begin; iter!=End; ++iter) { (x.*fptr)(iter); } } Using Code #include <string> #include <iostream> // one possible operation to apply to some container class aFunctor : public IFunctor<std::string> { void operator()(std::string::iterator& iter) { // seperate each char with space std::cout << *iter << " "; } }; // template class has member function we will point to template <typename C> class X { public: void doOp(typename C::iterator& iter) { std::cout << *iter << " "; } }; // alias for template function pointer to member typedef void (X<std::string>::*fptr)( std::string::iterator& ); void main() { std::string test = "CSE687 - Object Oriented Design"; // testing functor aFunctor func; std::cout << "\n "; Traverse(test.begin(), test.end(), func); // testing function pointer to member std::cout << "\n "; X<std::string> x; fptr f = &X<std::string>::doOp; Traverse<std::string>( test.begin(), test.end(), x, f ); // this syntax works too std::cout << "\n "; Traverse<std::string>( test.begin(), test.end(), x, &X<std::string>::doOp); std::cout << "\n\n"; } Output C S E 6 8 7 - O b j e c t O r i e n t e d D e s i g n C S E 6 8 7 - O b j e c t O r i e n t e d D e s i g n C S E 6 8 7 - O b j e c t O r i e n t e d D e s i g n
In section 6.6 we discuss template class specialization, a mechanism for providing not only a generic pattern for implementing a template class, as all templates do, but also alternate implementations for special cases for which the generic pattern may not work well.

7.6 Template Class Specialization

Templates provide the means to define classes that are configured with other subordinate classes. In the code block, below, we're configuring class X to use helper classes P and Q. Each application can decide which helpers to use and/or implement another set of helpers specific to that application. Template specialization gives us another degree of design freedom. Specialization occurs when we define a template class with one or more template arguments, but also provide one or more additional classes with the same name, but with at least one of the arguments specialized. In the code below, we've specialized X<P,Q> for the cases: X<P,Q1>, X<P1,Q>, and X<P1,Q1>. Each specialization provides its own class implementation and each will be different in some way needed for those specific cases. Note that these are different classes. The compiler is required, when compiling a template class, to look for specializations, and if one matches the declaration it's compiling, it must compile the specialized class. The code block below illustrates template specialization syntax. The following logger example demonstrates how specialization is used for practical applications. Template Class /*---------------------------------------- Classes P0, P1,Q0, and Q1 defined before this code. */ /*----------------------------------------- Class used for generic operations */ template<typename P = P0, typename Q = Q0> class X { public: X() { std::cout << "\n Generic X Template"; } void doProc() { p_.doProc(); q_.doProc(); } private: P p_; Q q_; }; /*----------------------------------------- Class specialized to use Q1 */ template<typename P> class X<P, Q1> { public: X() { std::cout << "\n X partially specialized for Q1"; } void doProc() { p_.doProc(); q_.doProc(); } private: P p_; Q1 q_; }; /*----------------------------------------- Class specialized to use P1 */ template<typename Q> class X<P1, Q> { public: X() { std::cout << "\n X partially specialized for P1"; } void doProc() { p_.doProc(); q_.doProc(); } private: P1 p_; Q q_; }; /*----------------------------------------- Class fully specialized to use P1 and Q1 */ template<> class X<P1, Q1> { public: X() { std::cout << "\n X fully specialized for P1 & Q1"; } void doProc() { p_.doProc(); q_.doProc(); } private: P1 p_; Q1 q_; }; Using Code: int main() { displayDemo("-- generic processing --"); X<P0, Q0> x1; x1.doProc(); displayDemo( "\n -- using default template args --" ); X<> x2; x2.doProc(); displayDemo( "\n -- Using full specializ'n --" ); X<P1, Q1> x3; x3.doProc(); displayDemo( "\n -- Using specializ'n for P1 --" ); X<P1, Q0> x4; x4.doProc(); displayDemo( "\n -- Using specializ'n for Q1 --" ); X<P0, Q1> x5; x5.doProc(); putline(2); } Output -- generic processing -- Generic X Template doing P0 processing doing Q0 processing -- using default template args -- Generic X Template doing P0 processing doing Q0 processing -- Using full specializ'n -- X fully specialized for P1 & Q1 doing P1 processing doing Q1 processing -- Using specializ'n for P1 -- X partially specialized for P1 doing P1 processing doing Q0 processing -- Using specializ'n for Q1 -- X partially specialized for Q1 doing P0 processing doing Q1 processing Template specialization provides a generic class, expected to handle common program needs. Each specialization adds another class that replaces the generic one for a specific case, e.g. a specific parameter type when instantiated. There may be as many specializations as you need. The logger example, below, is simpler than that presented above. It is here to illustrate template specialization. It uses specialization to provide:
  1. A generic logger class, Logger<S, F, T>, describing Logger structure. It's not intended for use, but provides the layout for template arguments. S is a text-based message, F is a message formatter, and T is a timer.
  2. An unadorned logger, Logger<S, FNull, TNull>, that provides basic logging, i.e., writes text to a specified stream. The first argument, S, will usually be a std::string, but could also be a structured message. The FNull argument provides a do nothing formatter, and the TNull argument provides a do nothing timer.
  3. A partial specialization on F, to provide application specific formatting. Note that this parameter is a template, e.g., F<S>, allowing F to handle different types of text messages.
  4. A partial specialization on T, to provide elapsed time annotations on each message.
This example deviates from the usual pattern in that the generic class is just used to provide a framework for subsequent specializations, so every template the application instantiates is a specialization. This design style is used less often than providing a working generic class, but sometimes it's easier to build this way.
Simplified Logger illustrates specialization Code: Specialized Logger struct TNull {}; template<typename S> struct FNull {}; template< typename S, template<typename S> typename F = FNull, typename T = TNull > class Logger { /*-- used for specializ'n structure --*/ }; /*--------------------------------------- unadorned Logger ---------------------------------------*/ template<typename S> class Logger<S, FNull, TNull> { public: Logger(std::ostream* pStr) : pStream_(pStr) {} ~Logger() {} void write(S s) { (*pStream_) << prefix_ << s; } private: std::string prefix_ = "\n "; std::ostream* pStream_; }; /*--------------------------------------- template class partial specialization on Formatter class - this is partial class specialization since S and F are still unspecified ---------------------------------------*/ template<typename S> struct Formatter { const char* prefix_ = "\n <-- "; const char* suffix_ = " -->"; std::string transform(const S& s) { return prefix_ + s + suffix_; } }; template< typename S, template<typename S> typename F > class Logger<S, F, TNull> { public: Logger(std::ostream* pStr) : pStream_(pStr) {} ~Logger() {} void write(S s) { (*pStream_) << f.transform(s); } private: std::string prefix_ = "\n "; std::ostream* pStream_; F<S> f; }; /*--------------------------------------- template class partial specialization on Timer class - See Timer.h for details ---------------------------------------*/ template<typename S, typename T> class Logger<S, FNull, T> { public: Logger(std::ostream* pStr) : pStream_(pStr) {} ~Logger() { timer_.stop(); } void start() { timer_.start(); } void stop() { timer_.stop(); } void write(S s) { (*pStream_) << prefix_ << std::setw(6) << timer_.elapsedMicroseconds() << " microsec : " << s; } private: std::string prefix_ = "\n "; T timer_; std::ostream* pStream_; }; Using Code int main() { displayTitle("Demonstrate Logger Specializ'n"); displayDemo("--- Unadorned Logger ---\n"); /* using defaults F=FNull, T=TNull */ Logger<std::string> log(&std::cout); log.write("first log item"); log.write("second log item"); displayDemo( "\n -- Specializ'n for format logs --\n" ); /* using default T = TNull */ Logger<std::string, Formatter> flog(&std::cout); flog.write("first formatted log item"); flog.write("second formatted log item"); displayDemo( "\n -- Specializ'n for timed logs --\n" ); /* cite FNull because defaults only at end */ Logger<std::string, FNull, Timer> tlog(&std::cout); tlog.start(); tlog.write("first timed log item"); tlog.write("second timed log item"); tlog.write("third timed log item"); std::cout << "\n\n"; } Output Demonstrate Logger Specializ'n =============================== --- Unadorned Logger --- first log item second log item --- Specializ'n for format logs --- <-- first formatted log item --> <-- second formatted log item --> --- Specializ'n for timed logs --- 41 microsec : first timed log item 482 microsec : second timed log item 790 microsec : third timed log item A more fully featured implementation would probably also provide a full specialization for both formatting and timing.
This concludes our discussion of templates, overloading, and specialization. In the next chapter we discuss Template Metaprogramming.

7.7 Epilogue - Two More Examples and References

The examples below are too large for the main part of this chapter. They are here to illustrate very typical uses of templates:
  1. Graph<V,E> illustrates the use of template parameters to hold information. The V type holds information specific to each instance of a graph vertex, perhaps a name, and the E type holds information specific to each instance of a graph edge, perhaps the type of relationship between the dependency parent and child vertices.
  2. Logger<T,C> uses a value parameter, C, to define categories of loggers, by creating a different logger class for each value of C. So, for example, we might define Logger<std::string,1> and Logger<std::string,2> to log information from two threads, t1 and t2.
The first example is a Directed Graph class. You can find complete code in the CppGraph.html repository. A directed graph consists of vertices that are connected with directed edges, from parent vertex to child vertex. Graphs are used to represent hierarchal relationships, e.g., package dependency or social network friend relationships. The example is here, because it uses two template type parameters, V and E, that represent information held in each vertex and each edge. For example, a class dependency graph would probably contain a class name in each vertex and in each edge a class relationship type, e.g., inheritance, composition, aggregation, or using. Before looking at the example, it will help to look at the Graph Documentation for the CppGraph repository. We show diagrams there that will help you understand the code below.
Directed Graph Class Vertex and Graph Code namespace GraphLib { ///////////////////////////////////////////////////////// // Vertex class template<typename V, typename E> class Vertex { public: typedef std::pair<int, E> Edge; // graph index of target vertex, edge type typename typedef std::vector<Edge>::iterator iterator; iterator begin(); iterator end(); Vertex(V v, size_t id); Vertex(V v); void add(Edge& edge); // compiler generated copy ctor, copy assignOp correct // Vertex(const Vertex<V,E>& v); // Vertex<V,E>& operator=(const Vertex<V,E>& v); Edge& operator[](size_t i); Edge operator[](size_t i) const; V& value(); size_t& id(); size_t size(); bool& mark(); private: std::vector<Edge> _edges; V _v; size_t _id; static size_t count; bool _mark; }; //--< reserve memory for, and initialize, static count >-- template<typename V, typename E> size_t Vertex<V,E>::count = 0; //--< set and return boolean mark, used for traversal >-- template<typename V, typename E> bool& Vertex<V,E>::mark() { return _mark; } //----< return iterator pointing to first edge >--------- template<typename V, typename E> typename Vertex<V,E>::iterator Vertex<V,E>::begin() { return _edges.begin(); } //--< return iterator pointing to one past last edge >--- template<typename V, typename E> typename Vertex<V,E>::iterator Vertex<V,E>::end() { return _edges.end(); } //----< construct instance, specifying unique id >----- template<typename V, typename E> Vertex<V,E>::Vertex(V v, size_t id) : _v(v), _id(id), _mark(false) {} //--< construct instance - creates id sequentially >--- template<typename V, typename E> Vertex<V,E>::Vertex(V v) : _v(v), _id(count++), _mark(false) {} //----< add edge to vertex edge collection >----------- template<typename V, typename E> void Vertex<V,E>::add(Edge& edge) { _edges.push_back(edge); } //----< index non-const vertex's edges >--------------- template<typename V, typename E> typename Vertex<V,E>::Edge& Vertex<V,E>::operator[](size_t i) { return _edges[i]; } //----< index const vertex's edges >------------------- template<typename V, typename E> typename Vertex<V,E>::Edge Vertex<V,E>::operator[](size_t i) const { return _edges[i]; } //---< set and read value of vertex's held type, V >--- template<typename V, typename E> V& Vertex<V,E>::value() { return _v; } //----< return vertex's id >--------------------------- template<typename V, typename E> size_t& Vertex<V,E>::id() { return _id; } //----< return number of edges >----------------------- template<typename V, typename E> size_t Vertex<V,E>::size() { return _edges.size(); } /////////////////////////////////////////////////// // Graph class template<typename V, typename E> class Graph { public: typename typedef std::vector< Vertex<V,E> >::iterator iterator; iterator begin(); iterator end(); // compiler generated copy ctor, copy assignOp correct // Graph(const Graph<V,E>& g); // Graph<V,E>& operator=(const Graph<V,E>& g); Vertex<V,E>& operator[](size_t i); Vertex<V,E> operator[](size_t i) const; void addVertex(Vertex<V,E> v); void addEdge( E eval, Vertex<V,E>& parent, Vertex<V,E>& child ); size_t findVertexIndexById(size_t id); size_t size(); template<typename F> void dfs(Vertex<V,E>& v, F f); private: std::vector< Vertex<V,E> > adj; std::unordered_map<size_t, size_t> idMap; // id maps to graph index template<typename F> void dfsCore(Vertex<V,E>& v, F f); }; //----< return iterator pointing to first vertex >----- template<typename V, typename E> typename Graph<V,E>::iterator Graph<V,E>::begin() { return adj.begin(); } //--< return iterator pointing one past last vertex >-- template<typename V, typename E> typename Graph<V,E>::iterator Graph<V,E>::end() { return adj.end(); } //----< index non-const graph's vertex collection >---- template<typename V, typename E> typename Vertex<V,E>& Graph<V,E>::operator[](size_t i) { return adj[i]; } //----< index const graph's vertex collection >-------- template<typename V, typename E> typename Vertex<V,E> Graph<V,E>::operator[](size_t i) const { return adj[i]; } //----< add vertex to graph's vertex collection >------ template<typename V, typename E> void Graph<V,E>::addVertex(Vertex<V,E> v) { adj.push_back(v); idMap[v.id()] = adj.size() - 1; } //----< return number of vertices in graph's coll >---- template<typename V, typename E> size_t Graph<V,E>::size() { return adj.size(); } //----< return index of vertex with specified id >----- template<typename V, typename E> size_t Graph<V,E>::findVertexIndexById(size_t id) { return idMap[id]; } //----< add edge from parent to child vertices >------ template<typename V, typename E> void Graph<V,E>::addEdge( E eVal, Vertex<V,E>& parent, Vertex<V,E>& child ) { size_t childIndex = findVertexIndexById(child.id()); if(childIndex == adj.size()) throw std::exception("no edge child"); size_t parentIndex = findVertexIndexById(parent.id()); if(parentIndex == adj.size()) throw std::exception("no edge parent"); Vertex<V,E>::Edge e; e.first = childIndex; e.second = eVal; adj[parentIndex].add(e); } //---< recursive depth first search with action f >---- template<typename V, typename E> template<typename F> void Graph<V,E>::dfsCore(Vertex<V,E>& v, F f) { f(v); v.mark() = true; for(auto edge : v) { if(adj[edge.first].mark() == false) dfsCore(adj[edge.first], f); } for(auto& vert : adj) { if(vert.mark() == false) dfsCore(vert, f); } } //--< depth first srch, clears marks for next srch >--- template<typename V, typename E> template<typename F> void Graph<V,E>::dfs(Vertex<V,E>& v, F f) { dfsCore(v, f); for(auto& vert : adj) vert.mark() = false; } Using Code #include <iostream> #include <fstream> #include "Graph.h" using namespace GraphLib; typedef Graph<std::string, std::string> graph; typedef Vertex<std::string, std::string> vertex; typedef Display<std::string, std::string> display; void showVert(Vertex<std::string, std::string>& v) { std::cout << "\n " << v.id(); } template<typename V, typename E> void TshowVert(Vertex<V,E>& v) { std::cout << "\n " << v.id(); } #ifdef TEST_GRAPH int main() { std::cout << "\n Testing Graph Library"; std::cout << "\n =======================\n"; try { std::cout << "\n Constructing Graph instance"; std::cout << "\n -----------------------------"; graph g; vertex v1("v1"); vertex v2("v2"); vertex v3("v3"); vertex v4("v4"); vertex v5("v5", 50); g.addVertex(v2); g.addVertex(v1); g.addVertex(v3); g.addVertex(v4); g.addVertex(v5); g.addEdge("e1",v1,v2); g.addEdge("e2",v1,v3); g.addEdge("e3",v2,v3); g.addEdge("e4",v4,v3); g.addEdge("e5",v5,v2); display::show(g); std::cout << "\n"; std::cout << "\n Making copy of instance"; std::cout << "\n -------------------------"; graph gcopy = g; display::show(gcopy); std::cout << "\n"; std::cout << "\n Modifying copy's values"; std::cout << "\n -------------------------"; for(auto& v : gcopy) v.value() += "copy"; display::show(gcopy); std::cout << "\n"; std::cout << "\n Assigning instance to copy"; std::cout << "\n ----------------------------"; gcopy = g; display::show(gcopy); std::cout << "\n"; std::cout << "\n Vertices with no Parents:"; std::cout << "\n ---------------------------"; std::vector< vertex > verts = display::vertsWithNoParents(g); std::cout << "\n "; for(size_t i=0; i<verts.size(); ++i) std::cout << verts[i].value().c_str() << " "; std::cout << "\n"; std::cout << "\n Testing Depth First Search function pointer"; std::cout << "\n ---------------------------------------------"; for(auto& vert : g) { std::cout << "\n starting at id " << vert.id(); g.dfs(vert, TshowVert<std::string, std::string>); // this works too: // g.dfs(vert, showVert); } std::cout << "\n"; std::cout << "\n Testing Depth First Search with Functor"; std::cout << "\n -----------------------------------------"; class showFunctor { public: void operator()( Vertex<std::string, std::string>& vert ) { std::cout << "\n From functor: vertix id = " << vert.id(); std::cout << ", number of edges = " << vert.size(); } }; g.dfs(g[0], showFunctor()); std::cout << "\n"; std::cout << "\n Testing Serialization to XML"; std::cout << "\n ------------------------------"; std::string str = GraphToXmlString(g); std::cout << str << "\n"; std::ofstream out("testGraph.xml"); out << str << "\n"; std::cout << "\n Testing Graph construction from XML"; std::cout << "\n -------------------------------------"; graph gtest; GraphFromXmlString(gtest, str); display::show(gtest); std::cout << "\n\n"; } catch(std::exception& ex) { std::cout << "\n\n " << ex.what() << "\n\n"; } std::cout << "\n\n"; return 0; } #endif Output Testing Graph Library ======================= Constructing Graph instance ----------------------------- vertex id = 1, value = v2 edge points to vertex with id = 2 and value = v3, edge value = e3 vertex id = 0, value = v1 edge points to vertex with id = 1 and value = v2, edge value = e1 edge points to vertex with id = 2 and value = v3, edge value = e2 vertex id = 2, value = v3 vertex id = 3, value = v4 edge points to vertex with id = 2 and value = v3, edge value = e4 vertex id = 50, value = v5 edge points to vertex with id = 1 and value = v2, edge value = e5 Making copy of instance ------------------------- vertex id = 1, value = v2 edge points to vertex with id = 2 and value = v3, edge value = e3 vertex id = 0, value = v1 edge points to vertex with id = 1 and value = v2, edge value = e1 edge points to vertex with id = 2 and value = v3, edge value = e2 vertex id = 2, value = v3 vertex id = 3, value = v4 edge points to vertex with id = 2 and value = v3, edge value = e4 vertex id = 50, value = v5 edge points to vertex with id = 1 and value = v2, edge value = e5 Modifying copy's values ------------------------- vertex id = 1, value = v2copy edge points to vertex with id = 2 and value = v3copy, edge value = e3 vertex id = 0, value = v1copy edge points to vertex with id = 1 and value = v2copy, edge value = e1 edge points to vertex with id = 2 and value = v3copy, edge value = e2 vertex id = 2, value = v3copy vertex id = 3, value = v4copy edge points to vertex with id = 2 and value = v3copy, edge value = e4 vertex id = 50, value = v5copy edge points to vertex with id = 1 and value = v2copy, edge value = e5 Assigning original instance to copy ------------------------------------- vertex id = 1, value = v2 edge points to vertex with id = 2 and value = v3, edge value = e3 vertex id = 0, value = v1 edge points to vertex with id = 1 and value = v2, edge value = e1 edge points to vertex with id = 2 and value = v3, edge value = e2 vertex id = 2, value = v3 vertex id = 3, value = v4 edge points to vertex with id = 2 and value = v3, edge value = e4 vertex id = 50, value = v5 edge points to vertex with id = 1 and value = v2, edge value = e5 Vertices with no Parents: --------------------------- v1 v4 v5 Testing Depth First Search with function pointer -------------------------------------------------- starting at id 1 1 2 0 3 50 starting at id 0 0 1 2 3 50 starting at id 2 2 1 0 3 50 starting at id 3 3 2 1 0 50 starting at id 50 50 1 2 0 3 Testing Depth First Search with Functor ----------------------------------------- From functor: vertix id = 1, number of edges = 1 From functor: vertix id = 2, number of edges = 0 From functor: vertix id = 0, number of edges = 2 From functor: vertix id = 3, number of edges = 1 From functor: vertix id = 50, number of edges = 1 Testing Serialization to XML ------------------------------ <graph> <vertex id="1" value="v2"> <edge targetId="2" value="e3"> </edge> </vertex> <vertex id="0" value="v1"> <edge targetId="1" value="e1"> </edge> <edge targetId="2" value="e2"> </edge> </vertex> <vertex id="2" value="v3"> </vertex> <vertex id="3" value="v4"> <edge targetId="2" value="e4"> </edge> </vertex> <vertex id="50" value="v5"> <edge targetId="1" value="e5"> </edge> </vertex> </graph> Testing Graph construction from XML ------------------------------------- vertex id = 1, value = v2 edge points to vertex with id = 2 and value = v3, edge value = e3 vertex id = 0, value = v1 edge points to vertex with id = 1 and value = v2, edge value = e1 edge points to vertex with id = 2 and value = v3, edge value = e2 vertex id = 2, value = v3 vertex id = 3, value = v4 edge points to vertex with id = 2 and value = v3, edge value = e4 vertex id = 50, value = v5 edge points to vertex with id = 1 and value = v2, edge value = e5
In the Logger class example, below, Logger has a larger design than the earlier simplified Logger example. It takes a template type parameter T, the type of message being logged, and value parameter size_t C, representing a logger category. Logger uses its C parameter to define a logging level, e.g., debug or demonstration or results or some combination of the three.
Logger Class Logger Code namespace Utilities { enum Level { results = 1, demo = 2, debug = 4, all = 7 }; template <typename T, size_t C = 0> struct ILogger { virtual ~ILogger() {} virtual ILogger<T, C>& add(std::ostream*) = 0; virtual ILogger<T, C>& write(T t, size_t level = Level::all) = 0; virtual void head(T t = "") = 0; virtual void prefix(T prfix = "\n ") = 0; virtual void wait() = 0; virtual void waitForWrites() = 0; virtual void level(size_t lv) = 0; virtual void name(const std::string& nm) = 0; }; template <typename T, size_t C = 0> class Logger : public ILogger<T, C> { public: Logger(const std::string& nm = ""); ~Logger(); ILogger<T, C>& add(std::ostream* pOstrm); virtual ILogger<T, C>& write(T t, size_t level = 0x7); virtual void head(T t = ""); virtual void prefix(T prfix = "\n "); virtual void level(size_t lv); void name(const std::string& nm); std::string name(); void wait(); void waitForWrites(); protected: std::vector<std::ostream*> dstStrm; BlockingQueue<T> blockingQueue_; void threadProc(); std::string name_; std::thread writeThread_; T head_; std::string prefix_ = "\n "; size_t level_ = 0x7; // Level::debug + Level::demo + Level::results; }; /*--- object factory ---------------------------------- * * Creates static logger, so calling makeLogger with * the same value for C will use the same logger. */ template<typename T, size_t C> inline ILogger<T, C>& makeLogger() { static Logger<T, C> logger; return logger; } /*--- initialize logger with name -------------------*/ template<typename T, size_t C> Logger<T, C>::Logger(const std::string& nm) : name_(nm) { dstStrm.push_back(&std::cout); std::thread temp(&Logger<T, C>::threadProc, this); writeThread_ = std::move(temp); } /*--- wait for all writes to be sent ----------------*/ template<typename T, size_t C> Logger<T, C>::~Logger() { if (writeThread_.joinable()) writeThread_.detach(); for (auto ptrStrm : dstStrm) { std::ofstream* ptrOfStrm = dynamic_cast<std::ofstream*>(ptrStrm); if (ptrOfStrm) { ptrOfStrm->close(); delete ptrOfStrm; } } } /*--- reset name ------------------------------------*/ template<typename T, size_t C> void Logger<T, C>::name(const std::string& nm) { name_ = nm; } /*--- retrieve name ---------------------------------*/ template<typename T, size_t C> std::string Logger<T, C>::name() { return name_; } /*--- deQ thread processing -------------------------*/ template<typename T, size_t C> void Logger<T,C>::threadProc() { while (true) { T t = blockingQueue_.deQ(); if (t == "quit") break; for (auto item : dstStrm) { (*item) << t; } } } /*-- enQ stop message, wait for write thread exit --*/ template<typename T, size_t C> void Logger<T, C>::wait() { blockingQueue_.enQ("quit"); writeThread_.join(); } /*-- wait for Q to empty before writing again ------*/ template<typename T, size_t C> void Logger<T, C>::waitForWrites() { while (blockingQueue_.size() > 0) std::this_thread::sleep_for( std::chrono::milliseconds(20) ); } /*--- add another stream for concurrent writes -----*/ template<typename T, size_t C> ILogger<T, C>& Logger<T, C>::add(std::ostream* pOstrm) { if(pOstrm != nullptr) dstStrm.push_back(pOstrm); return *this; } /*---------------------------------------------------- * write a log message * - probably one of many in a log stream */ template<typename T, size_t C> ILogger<T, C>& Logger<T, C>::write(T t, size_t lv) { if (lv & level_) { blockingQueue_.enQ(prefix_ + t); } return *this; } /*---------------------------------------------------- * write a head message * - expected to be the first in log conversation */ template<typename T, size_t C> void Logger<T, C>::head(T t) { T temp = (t.size() > 0) ? t : name(); T prfix = (prefix_ == "") ? "\n" : prefix_; head_ = temp + prfix + DateTime().now(); write(head_); } /*--- set message prefix ---------------------------*/ template<typename T, size_t C> void Logger<T,C>::prefix(T prfix) { prefix_ = prfix; } /*---------------------------------------------------- * set logging level * - results ==> normal output * - demo ==> demonstration output * - debug --> show debugging information * can be any combination, e.g., demo + results */ template<typename T, size_t C> void Logger<T, C>::level(size_t lv) { level_ = lv; } /*--- helper to open file stream --------------------*/ inline std::ostream* makeStream( const std::string& fileName ) { std::ofstream* pOfstrm = new std::ofstream; pOfstrm->open(fileName); if (pOfstrm->good()) return pOfstrm; else return nullptr; } Using Code #include <string> #include "Logger.h" int main() { displayTitle("Testing Logger"); using namespace Utilities; Logger<std::string> logger("test"); // to see logger demo own test comment next statement logger.level(Level::results); logger.write("-- constructed logger --\n", Level::demo); logger.add(makeStream("test.log")); logger.write("\n -- added stream --\n", Level::demo); logger.add(makeStream("does not exist")); //logger.prefix(" "); logger.head(); logger.write("\n -- called head --\n", Level::demo); logger.prefix(""); logger.write("\n -- called write --\n", Level::demo); logger.write("\n Hi ").write("there "); logger.write("from Logger.cpp"); logger.write("\n"); logger.write( "\n -- waiting for writes to complete --\n", Level::demo ); logger.waitForWrites(); logger.write("\n setting level = results"); logger.level(Level::results); logger.write("\n a debug msg", Level::debug); logger.write("\n a demo", Level::demo); logger.write("\n a result", Level::results); logger.write( "\n -- waiting for writes to complete --\n", Level::demo ); logger.waitForWrites(); logger.write("\n setting level = demo"); logger.level(Level::demo); logger.write("\n a debug msg", Level::debug); logger.write("\n a demo", Level::demo); logger.write("\n a result", Level::results); logger.waitForWrites(); logger.write("\n setting level = debug"); logger.level(Level::debug); logger.write("\n a debug msg", Level::debug); logger.write("\n a demo", Level::demo); logger.write("\n a result", Level::results); logger.waitForWrites(); logger.write("\n setting level = results + demo"); logger.level(Level::results + Level::demo); logger.write("\n a debug msg", Level::debug); logger.write("\n a demo", Level::demo); logger.write("\n a result", Level::results); logger.waitForWrites(); // to see logger demo own test comment next statement logger.level(Level::results); logger.write( "\n -- calling makeLogger factory --\n", Level::demo ); ILogger<std::string, 0>& logInstance = makeLogger<std::string, 0>(); logInstance.add(makeStream("staticlog.log")); logInstance.head("test logger factory"); logInstance.write("log msg #1").write("log msg #2"); ILogger<std::string, 0>& logInstance2 = makeLogger<std::string, 0>(); logInstance2.head("test 2nd instance of factory"); logInstance2.write("log2 msg #1").write("log2 msg #2"); logInstance2.write("log2 msg #3").write("log2 msg #4"); const auto& logFactory = []()->ILogger<std::string,0>& { return makeLogger<std::string, 0>(); }; logFactory().write("\n using makeLogger"); logger.wait(); logInstance.write("\n done waiting for logger"); logInstance.wait(); putline(); displaySubtitle("Testing Assertions"); Assert(true, "if you see this Assert raised"); Assert(false, "a message", __LINE__); try { Assert( false, "another message", __LINE__, true ); } catch (std::exception & ex) { std::cout << std::string("\n ") + ex.what(); } putline(); Requires(1 == 1, "1 == 1", __LINE__); Requires(1 == 2, "1 == 2", __LINE__); try { Requires( 1 == 3, "1 == 3", __LINE__, true ); } catch (std::exception & ex) { std::cout << std::string("\n ") + ex.what(); } putline(); Ensures(1 == 1, "1 == 1", __LINE__); Ensures(1 == 2, "1 == 2", __LINE__); try { Ensures( 1 == 3, "1 == 3", __LINE__, true ); } catch (std::exception & ex) { std::cout << std::string("\n ") + ex.what(); } putline(2); } Output test Wed Dec 4 08:20:27 2019 Hi there from Logger.cpp setting level = results a result setting level = demo a demo setting level = debug a debug msg setting level = results + demo a demo a result
Both designs have merit. I will eventually merge the best features of each and put that in the Logger Repository. C++ templates are a very powerful feature of the language. We use them to build components that can be reused by instantiating with application specific types. Because templates are constructed at compile time, template type deduction makes it suprisingly effective to build these reusable types. We will explore this further in the next chapter where we look at template metaprogramming.

7.8 Programming Exercises

  1. Write a calculator class that provides methods for addition, subtraction, multiplication and division. Use template parameters to support those operations for all the C++ arithmetic types (that includes unsigned types).
    Can you make this work for complex numbers? You will need a specialization for that. what will you do for division?
  2. Write a CircularBuffer class that has an "add" method that accepts a value of unspecified type and appends it to a private member collection - one of the STL containers. After the method has been called N times, it appends the new item and discards the oldest item, keeping the collection size at N items. Provide an iterator that starts at the most recent and increments toward the oldest.
    Is there an STL container that will make this very easy to implement? Please demonstrate that your CircularBuffer instances behave as expected.
  3. Write a query facility for STL containers that provides a declarative interface, something like C#'s LINQ, providing methods like select, where, sort, ... You're trying to do a proof of concept, not a complete industrial strength library. You might think about selecting some container and flattening into a std::vector. Since queries may modify the contents you don't want to operate directly on the original container. If you make most of the methods return a Query& then you can chain them, e.g.: std::vector = Query.select(container).where(predicate).sort().toVector(); This is just pseudo-code with all of the template syntax left for you to work out. It would be interesting to work out several operations, using the STL algorithms where appropriate.

7.9 References

Template Normal Programming - Arthur O'Dwyer'
video - part 1, slides - part 1 , video - part 2, slides - part 2 , blog
class template argument deduction - Stephan T. Lavavej
isocpp.org - templates
Class template argument deduction in C++17 - Timur Doumler
Understanding lvalues, rvalues and their references - Fluent C++
  Next Prev Pages Sections About Keys