Chapter 4: Advanced Template Concepts

In the previous chapters, we learned the core fundamentals of C++ templates. At this point, you should be able to write templates that are perhaps not very complex. However, there are many more details concerning templates, and this chapter is dedicated to these more advanced topics. These include the following topics that we address next:

  • Understanding name binding and dependent names
  • Exploring template recursion
  • Understanding template argument deduction
  • Learning forwarding references and perfect forwarding
  • Using the decltype specifier and the std::declval type operator
  • Understanding friendship in templates

On completing this chapter, you will acquire a deeper knowledge of these advanced template concepts and be able to understand and write more complex template code.

We will start this chapter by learning about name binding and dependent names.

Understanding name binding and dependent names

The term name binding refers to the process of finding the declaration of each name that is used within a template. There are two kinds of names used within a template: dependent names and non-dependent names. The former are names that depend on the type or value of a template parameter that can be a type, non-type, or template parameter. Names that don’t depend on template parameters are called non-dependent. The name lookup is performed differently for dependent and non-dependent names:

  • For dependent names, it is performed at the point of template instantiation.
  • For non-dependent names, it is performed at the point of the template definition.

We will first look at non-dependent names. As previously mentioned, name lookup happens at the point of the template definition. This is located immediately before the template definition. To understand how this works, let’s consider the following example:

template <typename T>
struct processor;          // [1] template declaration
void handle(double value)  // [2] handle(double) definition
{
   std::cout << "processing a double: " << value << '
';
}
template <typename T>
struct parser              // [3] template definition
{
   void parse()
   {
      handle(42);          // [4] non-dependent name
   }
};
void handle(int value)     // [5] handle(int) definition
{
   std::cout << "processing an int: " << value << '
';
}
int main()
{
   parser<int> p;          // [6] template instantiation
   p.parse();
}

There are several points of reference that are marked in the comments on the right side. At point [1], we have the declaration of a class template called parser. This is followed at point [2] by the definition of a function called handle that takes a double as its argument. The definition of the class template follows at point [3]. This class contains a single method called run that invokes a function called handle with the value 42 as its argument, at point [4].

The name handle is a non-dependent name because it does not depend on any template parameter. Therefore, name lookup and binding are performed at this point. handle must be a function known at point [3] and the function defined at [2] is the only match. After the class template definition, at point [5] we have the definition of an overload for the function handle, which takes an integer as its argument. This is a better match for handle(42), but it comes after the name binding has been performed, and therefore it will be ignored. In the main function, at point [6], we have an instantiation of the parser class template for the type int. Upon calling the run function, the text processing a double: 42 will be printed to the console output.

The next example is designed to introduce you to the concept of dependent names. Let’s look at the code first:

template <typename T>
struct handler          // [1] template definition
{
   void handle(T value)
   {
      std::cout << "handler<T>: " << value << '
';
   }
};
template <typename T>
struct parser           // [2] template definition
{
   void parse(T arg)
   {
      arg.handle(42);   // [3] dependent name
   }
};
template <>
struct handler<int>     // [4] template specialization
{
   void handle(int value)
   {
      std::cout << "handler<int>: " << value << '
';
   }
};
int main()
{
   handler<int> h;         // [5] template instantiation
   parser<handler<int>> p; // [6] template instantiation
   p.parse(h);
}

This example is slightly different from the previous one. The parser class template is very similar, but the handle functions have become members of another class template. Let’s analyze it point by point.

At the point mark with [1] in the comments, we have the definition of a class template called handler. This contains a single, public method called handle that takes an argument of the T type and prints its value to the console. Next, at point [2], we have the definition of the class template called parser. This is similar to the previous one, except for one key aspect: at point [3], it invokes a method called handle on its argument. Because the type of the argument is the template parameter T, it makes handle a dependent name. Dependent names are looked up at the point of template instantiation, so handle is not bound at this point. Continuing with the code, at point [4], there is a template specialization for the handler class template for the type int. As a specialization, this is a better match for the dependent name. Therefore, when the template instantiation happens at point [6], handler<int>::handle is the name that is bound to the dependent name used at [3]. Running this program will print handler<int>: 42 to the console.

Now that we’ve seen how name binding occurs, let’s learn how this relates to template instantiation.

Two-phase name lookup

The key takeaway from the previous section is that name lookup happens differently for dependent names (those that depend on a template parameter) and non-dependent names (those that do not depend on a template parameter, plus the template name and names defined in the current template instantiation). When the compiler passes through the definition of a template it needs to figure out whether a name is dependent or non-dependent. Further name lookup depends on this categorization and happens either at the template definition point (for non-dependent names) or the template instantiation point (for dependent names). Therefore, instantiation of a template happens in two phases:

  • The first phase occurs at the point of the definition when the template syntax is checked and names are categorized as dependent or non-dependent.
  • The second phase occurs at the point of instantiation when the template arguments are substituted for the template parameters. Name binding for dependent names happens at this point.

This process in two steps is called two-phase name lookup. To understand it better, let’s consider another example:

template <typename T>
struct base_parser
{
   void init()
   {
      std::cout << "init
";
   }
};
template <typename T>
struct parser : base_parser<T>
{
   void parse()
   {
      init();        // error: identifier not found
      std::cout << "parse
";
   }
};
int main()
{
   parser<int> p;
   p.parse();
}

In this snippet, we have two class templates: base_parser, which contains a public method called init, and parser, which derives from base_parser and contains a public method called parse. The parse member function calls a function called init and the intention is that it’s the base-class method init that is invoked here. However, the compiler will issue an error, because it’s not able to find init. The reason this happens is that init is a non-dependent name (as it does not depend on a template parameter). Therefore, it must be known at the point of the definition of the parser template. Although a base_parser<T>::init exists, the compiler cannot assume it’s what we want to call because the primary template base_parser can be later specialized and init can be defined as something else (such as a type, or a variable, or another function, or it may be missing entirely). Therefore, name lookup does not happen in the base class, only on its enclosing scope, and there is no function called init in parser.

This problem can be fixed by making init a dependent name. This can be done either by prefixing with this-> or with base_parser<T>::. By turning init into a dependent name, its name binding is moved from the point of template definition to the point of template instantiation. In the following snippet, this problem is solved by invoking init through the this pointer:

template <typename T>
struct parser : base_parser<T>
{
   void parse()
   {
      this->init();        // OK
      std::cout << "parse
";
   }
};

Continuing this example, let’s consider that a specialization of base_parser for the int type is made available after the definition of the parser class template. This can look as follows:

template <>
struct base_parser<int>
{
   void init()
   {
      std::cout << "specialized init
";
   }
};

Furthermore, let’s consider the following use of the parser class template:

int main()
{
   parser<int> p1;
   p1.parse();
   parser<double> p2;
   p2.parse();
}

When you run this program, the following text will be printed to the console:

specialized init
parse
init
parse

The reason for this behavior is that p1 is an instance of parser<int> and there is a specialization of its base class, base_parser<int> that implements the init function and prints specialized init to the console. On the other hand, p2 is an instance of parser<double>. Since a specialization of base_parser for the double type is not available, the init function from the primary template is being called and this only prints init to the console.

The next subject of this broader topic is using dependent names that are types. Let’s learn how that works.

Dependent type names

In the examples seen so far, the dependent name was a function or a member function. However, there are cases when a dependent name is a type. This is demonstrated with the following example:

template <typename T>
struct base_parser
{
   using value_type = T;
};
template <typename T>
struct parser : base_parser<T>
{
   void parse()
   {
      value_type v{};                       // [1] error
      // or
      base_parser<T>::value_type v{};       // [2] error
      std::cout << "parse
";
   }
};

In this snippet, base_parser is a class template that defines a type alias for T called value_type. The parser class template, which derives from base_parser, needs to use this type within its parse method. However, both value_type and base_parser<T>::value_type do not work, and the compiler is issuing an error. value_type does not work because it’s a non-dependent name and therefore it will not be looked up in the base class, only in the enclosing scope. base_parser<T>::value_type does not work either because the compiler cannot assume this is actually a type. A specialization of base_parser may follow and value_type could be defined as something else than a type.

In order to fix this problem, we need to tell the compiler the name refers to a type. Otherwise, by default, the compiler assumes it’s not a type. This is done with the typename keyword, at the point of definition, shown as follows:

template <typename T>
struct parser : base_parser<T>
{
   void parse()
   {
      typename base_parser<T>::value_type v{}; // [3] OK
      std::cout << "parse
";
   }
};

There are actually two exceptions to this rule:

  • When specifying a base class
  • When initializing class members

Let’s see an example for these two exceptions:

struct dictionary_traits
{
    using key_type = int;
    using map_type = std::map<key_type, std::string>;
    static constexpr int identity = 1;
};
template <typename T>
struct dictionary : T::map_type      // [1]
{
    int start_key { T::identity };   // [2]
    typename T::key_type next_key;   // [3]
};
int main()
{
    dictionary<dictionary_traits> d;
}

The dictionay_traits is a class used as the template argument for the dictionary class template. This class derives from T::map_type (see line [1]) but the use of the typename keyword is not required here. The dictionary class defines a member called start_key, which is an int initialized with the value of T::identity (see line [2]). Again, the typename keyword is not needed here. However, if we want to define yet another member of the type T::key_type (see line [3]) we do need to use typename.

The requirements for using typename have been relaxed in C++20 making the use of type names easier. The compiler is now able to deduce that we are referring to a type name in a multitude of contexts. For instance, defining a member variable as we did on line [3] previously no longer requires prefixing with the typename keyword.

In C++20, typename is implicit (can be deduced by the compiler) in the following contexts:

  • In using declarations
  • In the declaration of data members
  • In the declaration or definition of function parameters
  • In trailing return types
  • In default arguments of type-parameters of a template
  • In the type-id of a static_cast, const_cast, reinterpret_cast, or dynamic_cast statement

Some of these contexts are exemplified in the following snippet:

template <typename T>
struct dictionary : T::map_type
{
   int start_key{ T::identity };
   T::key_type next_key;                              // [1]
   using value_type = T::map_type::mapped_type;       // [2]
   void add(T::key_type const&, value_type const&) {} // [3]
};.

At all the lines marked with [1], [2], and [3] in this snippet, prior to C++20, the typename keyword was required to indicate a type name (such as T::key_type or T::map_type::mapped_type). When compiled with C++20, this is no longer necessary.

Note

In Chapter 2, Template Fundamentals, we have seen that the keywords typename and class can be used to introduce type template parameters and they are interchangeable. The keyword typename here, although it has a similar purpose, cannot be substituted with the keyword class.

Not only types can be dependent names but other templates too. We look at this topic in the next subsection.

Dependent template names

There are cases when the dependent name is a template, such as a function template or a class template. However, the default behavior of the compiler is to interpret the dependent name as a non-type, which leads to errors concerning the usage of the comparison operator <. Let’s demonstrate this with an example:

template <typename T>
struct base_parser
{
   template <typename U>
   void init()
   {
      std::cout << "init
";
   }
};
template <typename T>
struct parser : base_parser<T>
{
   void parse()
   {
      // base_parser<T>::init<int>();        // [1] error
      base_parser<T>::template init<int>();  // [2] OK
      std::cout << "parse
";
   }
};

This is similar to the previous snippets, but the init function in base_parser is also a template. The attempt to call it using the base_parser<T>::init<int>() syntax, as seen at point [1], results in a compiler error. Therefore, we must use the template keyword to tell the compiler the dependent name is a template. This is done as shown at point [2].

Keep in mind that the template keyword can only follow the scope resolution operator (::), member access through pointer (->), and the member access (.). Examples of correct usage are X::template foo<T>(), this->template foo<T>(), and obj.template foo<T>().

The dependent name does not have to be a function template. It can also be a class template, shown as follows:

template <typename T>
struct base_parser
{
   template <typename U>
   struct token {};
};
template <typename T>
struct parser : base_parser<T>
{
   void parse()
   {
      using token_type = 
         base_parser<T>::template token<int>; // [1]
      token_type t1{};
      typename base_parser<T>::template token<int> t2{}; 
                                                     // [2]
      std::cout << "parse
";
   }
};

The token class is an inner class template of the base_parser class template. It can be either used as in the line marked with [1], where a type alias is defined (which is then used to instantiate an object) or as at line [2], where it is used directly to declare a variable. Notice that the typename keyword is not necessary at [1], where the using declaration indicates we are dealing with a type, but is required at [2] because the compiler would otherwise assume it’s a non-type name.

The use of the typename and template keywords is not required in some contexts of the current template instantiation being observed. This will be the topic of the next subsection.

Current instantiation

The requirement to use the typename and template keywords to disambiguate dependent names may be avoided in the context of a class template definition where the compiler is able to deduce some dependent names (such as the name of a nested class) to refer to the current instantiation. This means some errors can be identified sooner, at the point of definition instead of the point of instantiation.

The complete list of names that can refer to the current instantiation, according to the C++ Standard, §13.8.2.1 - Dependent Types, is presented in the following table:

Table 4.1

Table 4.1

The following are the rules for considering a name as part of the current instantiation:

  • An unqualified name (that does not appear on the right side of the scope resolution operator ::) found in the current instantiation or its non-dependent base
  • A qualified name (that appears on the right side of the scope resolution operator ::) if its qualifier (the part that appears on the left side of the scope resolution operator) names the current instantiation and is found in the current instantiation or its non-dependent base
  • A name used in a class member access expression where the object expression is the current instantiation and the name is found in the current instantiation or its non-dependent base

    Note

    It is said that a base class is a dependent class if it is a dependent type (depends on a template parameter) and is not in the current instantiation. Otherwise, a base class is said to be a non-dependent class.

These rules may sound a bit harder to comprehend; therefore, let’s try to understand them with the help of several examples, as follows:

template <typename T>
struct parser
{
   parser* p1;          // parser is the CI
   parser<T>* p2;       // parser<T> is the CI
   ::parser<T>* p3;     // ::parser<T> is the CI
   parser<T*> p4;       // parser<T*> is not the CI
   struct token
   {
      token* t1;                  // token is the CI
      parser<T>::token* t2;       // parser<T>::token is the CI
      typename parser<T*>::token* t3; 
                         // parser<T*>::token is not the CI
   };
};
template <typename T>
struct parser<T*>
{
   parser<T*>* p1;   // parser<T*> is the CI
   parser<T>*  p2;   // parser<T> is not the CI
};

In the primary template parser, the names parser, parser<T>, and ::parser<T> all refer to the current instantiation. However, parser<T*> does not. The class token is a nested class of the primary template parser. In the scope of this class, token and parser<T>::token are both denoting the current instantiation. The same is not true for parser<T*>::token. This snippet also contains a partial specialization of the primary template for the pointer type T*. In the context of this partial specialization, parser<T*> is the current instantiation, but parser<T> is not.

Dependent names are an important aspect of template programming. The key takeaway from this section is that names are categorized as dependent (those that depend on a template parameter) and non-dependent (those that don’t depend on a template parameter). Name binding happens at the point of definition for non-dependent types and at the point of instantiation for dependent types. In some cases, the keywords typename and template are required to disambiguate the use of names and tell the compiler that a name refers to a type or a template. In the context of a class template definition, the compiler is, however, able to figure out that some dependent names refer to the current instantiation, which enables it to identify errors sooner.

In the next section, we move our attention to a topic that we briefly touched already, which is template recursion.

Exploring template recursion

In Chapter 3, Variadic Templates, we discussed variadic templates and saw that they are implemented with a mechanism that looks like recursion. In fact, it is overloaded functions and class template specializations respectively. However, it is possible to create recursive templates. To demonstrate how this works, we’ll look at implementing a compile-time version of the factorial function. This is typically implemented in a recursive manner, and a possible implementation is the following:

constexpr unsigned int factorial(unsigned int const n)
{
   return n > 1 ? n * factorial(n - 1) : 1;
}

This should be trivial to understand: return the result of multiplying the function argument with the value returned by calling the function recursively with the decremented argument, or return the value 1 if the argument is 0 or 1. The type of the argument (and the return value) is unsigned int to avoid calling it for negative integers.

To compute the value of the factorial function at compile time, we need to define a class template that contains a data member holding the value of the function. The implementation looks as follows:

template <unsigned int N>
struct factorial
{
   static constexpr unsigned int value = 
      N * factorial<N - 1>::value;
};
template <>
struct factorial<0>
{
   static constexpr unsigned int value = 1;
};
int main()
{
   std::cout << factorial<4>::value << '
';
}

The first definition is the primary template. It has a non-type template parameter representing the value whose factorial needs to be computed. This class contains a static constexpr data member called value, initialized with the result of multiplying the argument N and the value of the factorial class template instantiated with the decremented argument. The recursion needs an end case and that is provided by the explicit specialization for the value 0 (of the non-type template argument), in which case the member value is initialized with 1.

When encountering the instantiation factorial<4>::value in the main function, the compiler generates all the recursive instantiations from factorial<4> to factorial<0>. These look as follows:

template<>
struct factorial<4>
{
   inline static constexpr const unsigned int value = 
      4U * factorial<3>::value;
};
template<>
struct factorial<3>
{
   inline static constexpr const unsigned int value = 
      3U * factorial<2>::value;
};
template<>
struct factorial<2>
{
   inline static constexpr const unsigned int value = 
      2U * factorial<1>::value;
};
template<>
struct factorial<1>
{
   inline static constexpr const unsigned int value = 
      1U * factorial<0>::value;
};
template<>
struct factorial<0>
{
   inline static constexpr const unsigned int value = 1;
};

From these instantiations, the compiler is able to compute the value of the data member factorial<N>::value. It should be mentioned again that when optimizations are enabled, this code would not even be generated, but the resulting constant is used directly in the generated assembly code.

The implementation of the factorial class template is relatively trivial, and the class template is basically only a wrapper over the static data member value. We can actually avoid it altogether by using a variable template instead. This can be defined as follows:

template <unsigned int N>
inline constexpr unsigned int factorial = N * factorial<N - 1>;
template <>
inline constexpr unsigned int factorial<0> = 1;
int main()
{
   std::cout << factorial<4> << '
';
}

There is a striking similarity between the implementation of the factorial class template and the factorial variable template. For the latter, we have basically taken out the data member value and called it factorial. On the other hand, this may also be more convenient to use because it does not require accessing the data member value as in factorial<4>::value.

There is a third approach for computing the factorial at compile time: using function templates. A possible implementation is shown next:

template <unsigned int n>
constexpr unsigned int factorial()
{
   return n * factorial<n - 1>();
}
template<> constexpr unsigned int factorial<1>() { 
                                               return 1; }
template<> constexpr unsigned int factorial<0>() { 
                                               return 1; }
int main()
{
   std::cout << factorial<4>() << '
';
}

You can see there is a primary template that calls the factorial function template recursively, and we have two full specializations for the values 1 and 0, both returning 1.

Which of these three different approaches is the best is probably arguable. Nevertheless, the complexity of the recursive instantiations of the factorial templates remained the same. However, this depends on the nature of the template. The following snippet shows an example of when complexity increases:

template <typename T>
struct wrapper {};
template <int N>
struct manyfold_wrapper
{
   using value_type = 
      wrapper<
             typename manyfold_wrapper<N - 1>::value_type>;
};
template <>
struct manyfold_wrapper<0>
{
   using value_type = unsigned int;
};
int main()
{
   std::cout << 
    typeid(manyfold_wrapper<0>::value_type).name() << '
';
   std::cout << 
    typeid(manyfold_wrapper<1>::value_type).name() << '
';
   std::cout << 
    typeid(manyfold_wrapper<2>::value_type).name() << '
';
   std::cout << 
    typeid(manyfold_wrapper<3>::value_type).name() << '
';
}

There are two class templates in this example. The first is called wrapper and has an empty implementation (it doesn’t actually matter what it contains) but it represents a wrapper class over some type (or more precisely a value of some type). The second template is called manyfold_wrapper. This represents a wrapper over a wrapper over a type many times over, hence the name manyfold_wrapper. There is no end case for an upper limit of this number of wrappings, but there is a start case for the lower limit. The full specialization for value 0 defines a member type called value_type for the unsigned int type. As a result, manyfold_wrapper<1> defines a member type called value_type for wrapper<unsigned int>, manyfold_wrapper<2> defines a member type called value_type for wrapper<wrapper<unsigned int>>, and so on. Therefore, executing the main function will print the following to the console:

unsigned int
struct wrapper<unsigned int>
struct wrapper<struct wrapper<unsigned int> >
struct wrapper<struct wrapper<struct wrapper<unsigned int> > >

The C++ standard does not specify a limit for the recursively nested template instantiations but does recommend a minimum limit of 1,024. However, this is only a recommendation and not a requirement. Therefore, different compilers have implemented different limits. The VC++ 16.11 compiler has the limit set at 500, GCC 12 at 900, and Clang 13 at 1,024. A compiler error is generated when this limit is exceeded. Some examples are shown here:

For VC++:

fatal error C1202: recursive type or function dependency context too complex

For GCC:

fatal error: template instantiation depth exceeds maximum of 900 (use '-ftemplate-depth=' to increase the maximum)

For Clang:

fatal error: recursive template instantiation exceeded maximum depth of 1024

use -ftemplate-depth=N to increase recursive template instantiation depth

For GCC and Clang, the compiler option -ftemplate-depth=N can be used to increase this maximum value for nested template instantiations. Such an option is not available for the Visual C++ compiler.

Recursive templates help us solve some problems in a recursive manner at compile time. Whether you use recursive function templates, variable templates, or class templates depends on the problem you are trying to solve or perhaps your preference. However, you should keep in mind there are limits to the depth template recursion works. Nevertheless, use template recursion judiciously.

The next advanced topic to address in this chapter is template argument deduction, both for functions and classes. We start next with the former.

Function template argument deduction

Earlier in this book, we have briefly talked about the fact that the compiler can sometimes deduce the template arguments from the context of the function call, allowing you to avoid explicitly specifying them. The rules for template argument deduction are more complex and we will explore this topic in this section.

Let’s start the discussion by looking at a simple example:

template <typename T>
void process(T arg)
{
   std::cout << "process " << arg << '
';
}
int main()
{
   process(42);          // [1] T is int
   process<int>(42);     // [2] T is int, redundant
   process<short>(42);   // [3] T is short
}

In this snippet, process is a function template with a single type template parameter. The calls process(42) and process<int>(42) are identical because, in the first case, the compiler is able to deduce the type of the type template parameter T as int from the value of the argument passed to the function.

When the compiler tries to deduce the template arguments, it performs the matching of the types of the template parameters with the types of the arguments used to invoke the function. There are some rules that govern this matching. The compiler can match the following:

  • Types (both cv-qualified and non-qualified) of the form T, T const, T volatile:

    struct account_t

    {

       int number;

    };

    template <typename T>

    void process01(T) { std::cout << "T "; }

    template <typename T>

    void process02(T const) { std::cout << "T const "; }

    template <typename T>

    void process03(T volatile) { std::cout << "T volatile "; }

    int main()

    {

       account_t ac{ 42 };

       process01(ac);  // T

       process02(ac);  // T const

       process03(ac);  // T volatile

    }

  • Pointers (T*), l-value references (T&), and r-value references (T&&):

    template <typename T>

    void process04(T*) { std::cout << "T* "; }

    template <typename T>

    void process04(T&) { std::cout << "T& "; }

    template <typename T>

    void process05(T&&) { std::cout << "T&& "; }

    int main()

    {

       account_t ac{ 42 };

       process04(&ac);  // T*

       process04(ac);  // T&

       process05(ac);  // T&&

    }

  • Arrays such as T[5], or C[5][n], where C is a class type and n is a non-type template argument:

    template <typename T>

    void process06(T[5]) { std::cout << "T[5] "; }

    template <size_t n>

    void process07(account_t[5][n])

    { std::cout << "C[5][n] "; }

    int main()

    {

       account_t arr1[5] {};

       process06(arr1);  // T[5]

       account_t ac{ 42 };

       process06(&ac);   // T[5]

       account_t arr2[5][3];

       process07(arr2);  // C[5][n]

    }

  • Pointers to functions, with the form T(*)(), C(*)(T), and T(*)(U), where C is a class type and T and U are type template parameters:

    template<typename T>

    void process08(T(*)()) { std::cout << "T (*)() "; }

    template<typename T>

    void process08(account_t(*)(T))

    { std::cout << "C (*) (T) "; }

    template<typename T, typename U>

    void process08(T(*)(U)) { std::cout << "T (*)(U) "; }

    int main()

    {

       account_t (*pf1)() = nullptr;

       account_t (*pf2)(int) = nullptr;

       double    (*pf3)(int) = nullptr;

       process08(pf1);    // T (*)()

       process08(pf2);    // C (*)(T)

       process08(pf3);    // T (*)(U)

    }

  • Pointers to member functions with one of the following forms, T (C::*)(), T (C::*)(U), T (U::*)(), T (U::*)(V), C (T::*)(), C (T::*)(U), and D (C::*)(T), where C and D are class types and T, U, and V are type template parameters:

    struct account_t

    {

       int number;

       int get_number() { return number; }

       int from_string(std::string text) {

          return std::atoi(text.c_str()); }

    };

    struct transaction_t

    {

       double amount;

    };

    struct balance_report_t {};

    struct balance_t

    {

       account_t account;

       double    amount;

       account_t get_account()  { return account; }

       int get_account_number() { return account.number; }

       bool can_withdraw(double const value)

          {return amount >= value; };

       transaction_t withdraw(double const value) {

          amount -= value; return transaction_t{ -value }; }

       balance_report_t make_report(int const type)

    {return {}; }

    };

    template<typename T>

    void process09(T(account_t::*)())

    { std::cout << "T (C::*)() "; }

    template<typename T, typename U>

    void process09(T(account_t::*)(U))

    { std::cout << "T (C::*)(U) "; }

    template<typename T, typename U>

    void process09(T(U::*)())

    { std::cout << "T (U::*)() "; }

    template<typename T, typename U, typename V>

    void process09(T(U::*)(V))

    { std::cout << "T (U::*)(V) "; }

    template<typename T>

    void process09(account_t(T::*)())

    { std::cout << "C (T::*)() "; }

    template<typename T, typename U>

    void process09(transaction_t(T::*)(U))

    { std::cout << "C (T::*)(U) "; }

    template<typename T>

    void process09(balance_report_t(balance_t::*)(T))

    { std::cout << "D (C::*)(T) "; }

    int main()

    {

       int (account_t::* pfm1)() = &account_t::get_number;

       int (account_t::* pfm2)(std::string) =

          &account_t::from_string;

       int (balance_t::* pfm3)() =

          &balance_t::get_account_number;

       bool (balance_t::* pfm4)(double) =

          &balance_t::can_withdraw;

       account_t (balance_t::* pfm5)() =

          &balance_t::get_account;

       transaction_t(balance_t::* pfm6)(double) =

          &balance_t::withdraw;

       balance_report_t(balance_t::* pfm7)(int) =

          &balance_t::make_report;

       process09(pfm1);    // T (C::*)()

       process09(pfm2);    // T (C::*)(U)

       process09(pfm3);    // T (U::*)()

       process09(pfm4);    // T (U::*)(V)

       process09(pfm5);    // C (T::*)()

       process09(pfm6);    // C (T::*)(U)

       process09(pfm7);    // D (C::*)(T)

    }

  • Pointers to data members such as T C::*, C T::*, and T U::*, where C is a class type and T and U are type template parameters:

    template<typename T>

    void process10(T account_t::*)

    { std::cout << "T C::* "; }

    template<typename T>

    void process10(account_t T::*)

    { std::cout << "C T::* "; }

    template<typename T, typename U>

    void process10(T U::*) { std::cout << "T U::* "; }

    int main()

    {

       process10(&account_t::number);   // T C::*

       process10(&balance_t::account);  // C T::*

       process10(&balance_t::amount);   // T U::*

    }

  • A template with an argument list that contains at least one type template parameter; the general form is C<T>, where C is a class type and T is a type template parameter:

    template <typename T>

    struct wrapper

    {

       T data;

    };

    template<typename T>

    void process11(wrapper<T>) { std::cout << "C<T> "; }

    int main()

    {

       wrapper<double> wd{ 42.0 };

       process11(wd); // C<T>

    }

  • A template with an argument list that contains at least one non-type template argument; the general form is C<i>, where C is a class type and i a non-type template argument:

    template <size_t i>

    struct int_array

    {

       int data[i];

    };

    template<size_t i>

    void process12(int_array<i>) { std::cout << "C<i> "; }

    int main()

    {

       int_array<5> ia{};

       process12(ia); // C<i>

    }

  • A template template argument with an argument list that contains at least one type template parameter; the general form is TT<T>, where TT is a template template parameter and T is a type template:

    template<template<typename> class TT, typename T>

    void process13(TT<T>) { std::cout << "TT<T> "; }

    int main()

    {

       wrapper<double> wd{ 42.0 };

       process13(wd);    // TT<U>

    }

  • A template template argument with an argument list that contains at least one non-type template argument; the general form is TT<i>, where TT is a template template parameter and i is a non-type template argument:

    template<template<size_t> typename TT, size_t i>

    void process14(TT<i>) { std::cout << "TT<i> "; }

    int main()

    {

       int_array<5> ia{};

       process14(ia);    // TT<i>

    }

  • A template template argument with an argument list that has no template arguments dependent on a template parameter; this has the form TT<C>, where TT is the template template parameter and C is a class type:

    template<template<typename> typename TT>

    void process15(TT<account_t>) { std::cout << "TT<C> "; }

    int main()

    {

       wrapper<account_t> wa{ {42} };

       process15(wa);    // TT<C>

    }

Although the compiler is able to deduce many types of template parameters, as previously seen, there are also limitations to what it can do. These are exemplified in the following list:

  • The compiler cannot deduce the type of a type template argument, from the type of a non-type template argument. In the following example, process is a function template with two template parameters: a type template called T, and a non-type template i of the type T. Calling the function with an array of five doubles does not allow the compiler to determine the type of T, even though this is the type of the value specifying the size of the array:

    template <typename T, T i>

    void process(double arr[i])

    {

       using index_type = T;

       std::cout << "processing " << i

    << " doubles" << ' ';

    std::cout << "index type is "

    << typeid(T).name() << ' ';

    }

    int main()

    {

       double arr[5]{};

       process(arr);         // error

       process<int, 5>(arr); // OK

    }

  • The compiler is not able to determine the type of a template argument from the type of a default value. This is exemplified ahead in the code with the function template process, which has a single type template parameter, but two function parameters, both of type T and both with default values.

The process() call (without any arguments) fails because the compiler cannot deduce the type of the type template parameter T from the default values of the function parameters. The process<int>() call is OK because the template argument is explicitly provided. The process(6) call is also OK, because the type of the first function parameter can be deduced from the supplied argument, and, therefore, the type template argument can also be deduced:

template <typename T>

void process(T a = 0, T b = 42)

{

   std::cout << a << "," << b << ' ';

}

int main()

{

   process();        // [1] error

   process<int>();   // [2] OK

   process(10);      // [3] OK

}

  • Although the compiler can deduce function template arguments from pointer to functions or pointer to member functions, as we have seen earlier, there are a couple of restrictions to this capability: it cannot deduce arguments from pointers to function templates, nor from a pointer to a function that has an overloaded set with more than one overloaded function matching the required type.

In the code ahead, the function template invoke takes a pointer to a function that has two arguments, the first of the type template parameter T, and the second an int, and returns void. This function template cannot be passed a pointer to alpha (see [1]) because this is a function template, nor to beta (see [2]), because this has more than one overload that can match the type T. However, it is possible to call it with a pointer to gamma (see [3]), and it will correctly deduce the type of the second overload:

template <typename T>

void invoke(void(*pfun)(T, int))

{

   pfun(T{}, 42);

}

template <typename T>

void alpha(T, int)

{ std::cout << "alpha(T,int)" << ' '; }

void beta(int, int)

{ std::cout << "beta(int,int)" << ' '; }

void beta(short, int)

{ std::cout << "beta(short,int)" << ' '; }

void gamma(short, int, long long)

{ std::cout << "gamma(short,int,long long)" << ' '; }

void gamma(double, int)

{ std::cout << "gamma(double,int)" << ' '; }

int main()

{

   invoke(&alpha);  // [1] error

   invoke(&beta);   // [2] error

   invoke(&gamma);  // [3] OK

}

  • Another limitation of the compiler is the argument deduction of the primary dimension of an array. The reason is this is not part of function parameter types. The exceptions to this limitation are the cases when the dimension refers to a reference or pointer type. The following code snippet demonstrates these restrictions:
    • The call to process1() at [1] generates an error because the compiler is not able to deduce the value of the non-type template argument Size, since this refers to the primary dimension of an array.
    • The call to process2() at the point marked with [2] is correct because the non-type template parameter Size refers to the second dimension of an array.
    • On the other hand, the calls to process3() (at [3]) and process4() (at [4]) are both successful, since the function argument is either a reference or a pointer to a single-dimensional array:

      template <size_t Size>

      void process1(int a[Size])

      { std::cout << "process(int[Size])" << ' '; };

      template <size_t Size>

      void process2(int a[5][Size])

      { std::cout << "process(int[5][Size])" << ' '; };

      template <size_t Size>

      void process3(int(&a)[Size])

      { std::cout << "process(int[Size]&)" << ' '; };

      template <size_t Size>

      void process4(int(*a)[Size])

      { std::cout << "process(int[Size]*)" << ' '; };

      int main()

      {

         int arr1[10];

         int arr2[5][10];

         process1(arr1);   // [1] error

         process2(arr2);   // [2] OK

         process3(arr1);   // [3] OK

         process4(&arr1);  // [4] OK

      }

  • If a non-type template argument is used in an expression in the function template parameter list, then the compiler cannot deduce its value.

In the following snippet, ncube is a class template with a non-type template parameter N representing a number of dimensions. The function template process also has a non-type template parameter N, but this is used in an expression in the template parameter list of the type of its single parameter. As a result, the compiler cannot deduce the value of N from the type of the function argument (as seen at [1]) and this must be specified explicitly (as seen at [2]):

template <size_t N>

struct ncube

{

   static constexpr size_t dimensions = N;

};

template <size_t N>

void process(ncube<N - 1> cube)

{

   std::cout << cube.dimensions << ' ';

}

int main()

{

   ncube<5> cube;

   process(cube);    // [1] error

   process<6>(cube); // [2] OK

}

All the rules for template argument deduction discussed in this section also apply to variadic function templates. However, everything that was discussed was in the context of function templates. Template argument deduction works for class templates too and we will explore this topic in the next section.

Class template argument deduction

Before C++17, template argument deduction only worked for functions but not classes. This meant that when a class template had to be instantiated, all the template arguments had to be supplied. The following snippet shows several examples:

template <typename T>
struct wrapper
{
   T data;
};
std::pair<int, double> p{ 42, 42.0 };
std::vector<int>       v{ 1,2,3,4,5 };
wrapper<int>           w{ 42 };

By leveraging template argument deduction for function templates, some standard types feature helper functions that create an instance of the type without the need to explicitly specify template arguments. Such examples are std::make_pair for std::pair and std::make_unique for std::unique_ptr. These helper function templates, used in corroboration with the auto keyword, avoid the need for specifying template arguments for class templates. Here is an example:

auto p = std::make_pair(42, 42.0);

Although not all standard class templates have such a helper function for creating instances, it’s not hard to write your own. In the following snippet, we can see a make_vector function template used to create a std::vector<T> instance, and a make_wrapper function template to create a wrapper<T> instance:

template <typename T, typename... Ts, 
          typename Allocator = std::allocator<T>>
auto make_vector(T&& first, Ts&&... args)
{
   return std::vector<std::decay_t<T>, Allocator> {
      std::forward<T>(first),
      std::forward<Ts>(args)... 
   };
}
template <typename T>
constexpr wrapper<T> make_wrapper(T&& data)
{
   return wrapper{ data };
}
auto v = make_vector(1, 2, 3, 4, 5);
auto w = make_wrapper(42);

The C++17 standard has simplified the use of class templates by providing template argument deduction for them. Therefore, as of C++17, the first snippet shown in this section can be simplified as follows:

std::pair   p{ 42, 42.0 };   // std::pair<int, double>
std::vector v{ 1,2,3,4,5 };  // std::vector<int>
wrapper     w{ 42 };         // wrapper<int>

This is possible because the compiler is able to deduce the template arguments from the type of the initializers. In this example, the compiler deduced it from the initializing expression of the variables. But the compiler is also able to deduce template arguments from new expressions and function-style cast expressions. These are exemplified next:

template <typename T>
struct point_t
{
   point_t(T vx, T vy) : x(vx), y(vy) {}
private:
   T x;
   T y;
};
auto p = new point_t(1, 2);   // [1] point<int>
                              // new expression
std::mutex mt;
auto l = std::lock_guard(mt); // [2] 
// std::lock_guard<std::mutex>
// function-style cast expression

The way template argument deduction works for class templates is different than for function templates but it relies on the latter. When encountering the name of a class template in a variable declaration or function-style cast, the compiler proceeds to build a set of so-called deduction guides.

There are fictional function templates representing constructor signatures of a fictional class type. Users can also provide deduction guides and these are added to the list of compiler-generated guides. If overload resolution fails on the constructed set of fictional function templates (the return type is not part of the matching process since these functions represent constructors), then the program is ill-formed and an error is generated. Otherwise, the return type of the selected function template specialization becomes the deduced class template specialization.

To understand this better, let’s see how the deduction guides actually look. In the following snippet, you can see some of the guides generated by the compiler for the std::pair class. The actual list is longer and, for brevity, only some are presented here:

template <typename T1, typename T2>
std::pair<T1, T2> F();
template <typename T1, typename T2>
std::pair<T1, T2> F(T1 const& x, T2 const& y);
template <typename T1, typename T2, typename U1, 
          typename U2>
std::pair<T1, T2> F(U1&& x, U2&& y);

This set of implicitly deduced guides is generated from the constructors of the class template. This includes the default constructor, the copy constructor, the move constructor, and all the conversion constructors, with the arguments copied in the exact order. If the constructor is explicit, then so is the deduction guide. However, if the class template does not have any user-defined constructor, a deduction guide is created for a hypothetical default constructor. A deduction guide for a hypothetical copy constructor is always created.

User-defined deduction guides can be provided in the source code. The syntax is similar to that of functions with a trailing return type but without the auto keyword. Deduction guides can be either functions or function templates. What is important to keep in mind is that these must be provided in the same namespace as the class template they apply to. Therefore, if we were to add a user-defined deduction guide for the std::pair class, it must be done in the std namespace. An example is shown here:

namespace std
{
   template <typename T1, typename T2>
   pair(T1&& v1, T2&& v2) -> pair<T1, T2>;
}

The deduction guides shown so far were all function templates. But as mentioned earlier, they don’t have to be function templates. They can be regular functions too. To demonstrate this, let’s consider the following example:

std::pair  p1{1, "one"};    // std::pair<int, const char*>
std::pair  p2{"two", 2};    // std::pair<const char*, int>
std::pair  p3{"3", "three"};
                    // std::pair<const char*, const char*>

With the compiler-degenerated deduction guides for the std::pair class, the deduced types are std::pair<int, const char*> for p1, std::pair<const char*, int> for p2, and std::pair<const char*, const char*> for p3. In other words, the type deduced by the compiler where literal strings are used is const char* (as one should expect). We could tell the compiler to deduce std::string instead of const char* by providing several user-defined deduction guides. These are shown in the following listing:

namespace std
{
   template <typename T>
   pair(T&&, char const*) -> pair<T, std::string>;
   template <typename T>
   pair(char const*, T&&) -> pair<std::string, T>;
   pair(char const*, char const*) -> 
      pair<std::string, std::string>;
}

Notice that the first two are function templates, but the third one is a regular function. Having these guides available, the deduced types for p1, p2, and p3 from the previous example are std::pair<int, std::string>, std::pair<std::string, int> and std::pair<std::string, std::string> respectively.

Let’s look at one more example for user-defined guides, this time for a user-defined class. Let’s consider the following class template that models a range:

template <typename T>
struct range_t
{
   template <typename Iter>
   range_t(Iter first, Iter last)
   {
      std::copy(first, last, std::back_inserter(data));
   }
private:
   std::vector<T> data;
};

There is not much to this implementation but, in fact, it is enough for our purpose. Let’s consider you want to construct a range object from an array of integers:

int arr[] = { 1,2,3,4,5 };
range_t r(std::begin(arr), std::end(arr));

Running this code will generate an error. Different compilers would generate different error messages. Perhaps Clang provides the error messages that best describe the problem:

error: no viable constructor or deduction guide for deduction of template arguments of 'range_t'

   range_t r(std::begin(arr), std::end(arr));

           ^

note: candidate template ignored: couldn't infer template argument 'T'

      range_t(Iter first, Iter last)

      ^

note: candidate function template not viable: requires 1 argument, but 2 were provided

   struct range_t

Nevertheless, regardless of what the actual error message is, the meaning is the same: template argument deduction for range_t failed. In order to make deduction work, a user-defined deduction guide needs to be provided and it needs to look as follows:

template <typename Iter>
range_t(Iter first, Iter last) -> 
   range_t<
      typename std::iterator_traits<Iter>::value_type>;

What this deduction guide is instructing is that when a call to the constructor with two iterator arguments is encountered, the value of the template parameter T should be deduced to be the value type of the iterator traits. Iterator traits is a topic that will be addressed in Chapter 5, Type Traits and Conditional Compilation. However, with this available, the previous snippet runs without problems and the compiler deduces the type of the r variable to be range_t<int>, as intended.

At the beginning of this section, the following example was provided, where the type of w was said to be deduced as wrapper<int>:

wrapper w{ 42 }; // wrapper<int>

In C++17, this is not actually true without a user-defined deduction guide. The reason is that wrapper<T> is an aggregate type and class template argument deduction does not work from aggregate initialization in C++17. Therefore, to make the preceding line of code work, a deduction guide as follows needs to be provided:

template <typename T>
wrapper(T) -> wrapper<T>;

Fortunately, the need for such a user-defined deduction guide was removed in C++20. This version of the standard provides support for aggregate types (as long as any dependent base class has no virtual functions or virtual base classes and the variable is initialized from a non-empty list of initializers).

Class template argument deduction only works if no template arguments are provided. As a consequence, the following declarations of p1 and p2 are both valid and class template argument deduction occurs; for p2, the deduced type is std::pair<int, std::string> (assuming the previously user-defined guides are available). However, the declarations of p3 and p4 produce an error because class template argument deduction does not occur, since a template argument list is present (<> and <int>) but does not contain all required arguments:

std::pair<int, std::string> p1{ 1, "one" };  // OK
std::pair p2{ 2, "two" };                    // OK
std::pair<> p3{ 3, "three" };                // error
std::pair<int> p4{ 4, "four" };              // error

Class template argument deduction may not always produce the expected results. Let’s consider the following example:

std::vector v1{ 42 };
std::vector v2{ v1, v1 };
std::vector v3{ v1 };

The deduced type for v1 is std::vector<int> and the deduced type for v2 is std::vector<std::vector<int>>. However, what should the compiler deduce for the type of v3? There are two options: std::vector<std::vector<int>> and std::vector<int>. If your expectation is the former, you will be disappointed to learn that the compiler actually deduces the latter. This is because deduction depends on both the number of arguments and their type.

When the number of arguments is greater than one, it will use the constructor that takes an initializer list. For the v2 variable, that is std::initializer_list<std::vector<int>>. When the number of arguments is one, then the type of the arguments is considered. If the type of the argument is a (specialization of) std::vector – considering this explicit case – then the copy-constructor is used and the deduced type is the declared type of the argument. This is the case of variable v3, where the deduced type is std::vector<int>. Otherwise, the constructor that takes an initializer list (with a single element) is used, as in the case of variable v1, for which the deduced type is std::vector<int>. These could be better visualized with the help of the cppinsights.io tool, which shows the following generated code (for the previous snippet). Notice that the allocator argument has been removed for brevity:

std::vector<int> v1 = 
   std::vector<int>{std::initializer_list<int>{42}};
std::vector<vector<int>> v2 = 
   std::vector<vector<int>>{
      std::initializer_list<std::vector<int>>{
         std::vector<int>(v1), 
         std::vector<int>(v1)
      }
   };
std::vector<int> v3 = std::vector<int>{v1};

Class template argument deduction is a useful feature of C++17 with improvements for aggregate types in C++20. It helps avoid writing unnecessary explicit template arguments when the compiler is able to deduce them, even though, in some cases, the compiler may require user-defined deduction guides for the deduction to work. It also avoids the need for creating factory functions, such as std::make_pair or std::make_tuple, that were a workaround for benefiting from template argument deduction before it was available for class templates.

There is more to template argument deduction than what we have discussed so far. There is a special case of function template argument deduction known as forwarding references. This will be addressed next.

Forwarding references

One of the most important features that were added to the language in C++11 is move semantics, which helps improve performance by avoiding making unnecessary copies. Move semantics are supported by another C++11 feature called rvalue references. Before discussing these, it is worth mentioning that, in C++, we have two kinds of values:

  • lvalues are values that refer to a memory location and, therefore, we can take their address with the & operator. lvalues can appear both on the left and right sides of an assignment expression.
  • rvalues are values that are not lvalues. They are defined by exclusion. rvalues do not refer to a memory location and you can’t take their address with the & operator. rvalues are literals and temporary objects and can only appear on the right side of an assignment expression.

    Note

    In C++11, there are a few other value categories, glvalue, prvalue, and xvalue. Discussing them here would not benefit the current topic. However, you can read more about them at https://en.cppreference.com/w/cpp/language/value_category.

References are aliases to already existing objects or functions. Just as we have two kinds of values, in C++11 we have two kinds of references:

  • lvalue references, denoted with a &, such as in &x, are references to lvalues.
  • rvalue references, denoted with &&, such as in &&x, are references to rvalues.

Let’s look at some examples to understand these concepts better:

struct foo
{
   int data;
};
void f(foo& v)
{ std::cout << "f(foo&)
"; }
void g(foo& v)
{ std::cout << "g(foo&)
"; }
void g(foo&& v)
{ std::cout << "g(foo&&)
"; }
void h(foo&& v)
{ std::cout << "h(foo&&)
"; }
foo x = { 42 };   //  x is lvalue
foo& rx = x;      // rx is lvalue

We have three functions here: f, which takes an lvalue reference (that is, int&); g, which has two overloads, one for an lvalue reference, and one for an rvalue reference (that is, int&&); and h, which takes an rvalue reference. We also have two variables, x and rx. Here, x is an lvalue, whose type is foo. We can take its address with &x. An lvalue is also rx, which is an lvalue reference, whose type is foo&. Now, let’s see how we can call each of the f, g, and h functions:

f(x);       // f(foo&)
f(rx);      // f(foo&)
f(foo{42}); // error: a non-const reference
            // may only be bound to an lvalue

Because x and rx are both lvalues, passing them to f is OK since this function takes an lvalue reference. However, foo{42} is a temporary object, as it does not exist outside the context of the call to f. That means, it is an rvalue, and passing it to f will result in a compiler error, because the parameter of the function is of the type foo& and non-constant references may only be bound to lvalues. This would work if the signature of the function f was changed to f(int const &v). Next, let’s discuss the g function:

g(x);             // g(foo&)
g(rx);            // g(foo&)
g(foo{ 42 });     // g(foo&&)

In the preceding snippet, calling g with either x or rx will resolve to the first overload, which takes an lvalue reference. However, calling it with foo{42}, which is a temporary object, therefore an rvalue, will resolve to the second overload, which takes an rvalue reference. Let’s see what happens when we want to make the same calls to the h function:

h(x);      // error, cannot bind an lvalue to an rvalue ref
h(rx);           // error
h(foo{ 42 });    // h(foo&&)
h(std::move(x)); // h(foo&&)

This function takes an rvalue reference. The attempts to pass either x or rx to it result in compiler errors because lvalues cannot be bound to rvalue references. The expression foo{42}, being an rvalue, can be passed as an argument. We can also pass the lvalue x to the function h if we change its semantic from an lvalue to an rvalue. That is done with the help of std::move. This function does not really move anything; it only makes a sort of a cast from an lvalue to an rvalue.

However, it is important to understand that passing rvalues to a function has two purposes: either the object is temporary and does not exist outside the call and the function can do anything with it, or the function is supposed to take ownership of the object that is received. This is the purpose of the move constructor and the move assignment operator and it’s rare that you will see other functions taking rvalue references. In our last example, within the function h, the parameter v is an lvalue but it is bound to an rvalue. The variable x existed outside the call to h but passing it to std::move transformed it into an rvalue. It still exists as an lvalue after the call to h returns but you should assume the function h did something with it and its state can be anything.

One purpose of rvalue references is, therefore, to enable move semantics. But it has yet another one and that is to enable perfect forwarding. To understand this, let’s consider the following modified scenario of the previous functions g and h:

void g(foo& v)  { std::cout << "g(foo&)
"; }
void g(foo&& v) { std::cout << "g(foo&&)
"; }
void h(foo& v)  { g(v); }
void h(foo&& v) { g(v); }

In this snippet, the implementation of g is identical to the one seen earlier. However, h also has two overloads, one that takes an lvalue reference and calls g and another one that takes an rvalue reference and also calls g. In other words, the function h is just forwarding the argument to g. Now, let’s consider the following calls:

foo x{ 42 };
h(x);          // g(foo&)
h(foo{ 42 });  // g(foo&)

From this, you would expect that the call h(x) will result in a call to the g overload taking an lvalue reference and the call to h(foo{42}) will result in a call to the g overload taking an rvalue reference. However, in fact, both of them will call the first overload of g, therefore printing g(foo&) to the console. The explanation is actually simple once you understand how references work: in the context h(foo&& v), the parameter v is actually an lvalue (it has a name and you can take its address) so calling g with it invokes the overload that takes an lvalue reference. To make it work as intended, we need to change the implementation of the h functions as follows:

void h(foo& v)  { g(std::forward<foo&>(v)); }
void h(foo&& v) { g(std::forward<foo&&>(v)); }

The std::forward is a function that enables the correct forwarding of values. What the function does is as follows:

  • If the argument is an lvalue reference, then the function behaves just as a call to std::move (changing the semantics from an lvalue to an rvalue).
  • If the argument is an rvalue reference, then it does nothing.

Everything that we discussed so far is unrelated to templates, which are the subject of this book. However, function templates can also be used to take lvalue and rvalue references and it’s important to understand first how these work in non-templates scenarios. This is because, in templates, rvalue references work slightly differently, and sometimes they are rvalue references, but other times they are actually lvalue references.

References that exhibit this behavior are called forwarding references. However, they are often referred to as universal references. This was a term coined by Scott Meyers shortly after C++11 when there was no term in the standard for this type of reference. In order to address this shortcoming, and because it didn’t feel the term universal references properly described their semantics, the C++ standard committee called these forwarding references in C++14. Yet, both terms are equally present in literature. For the sake of being true to the standard terminology, we’ll call them forwarding references in this book.

To begin the discussion on forwarding references, let’s consider the following overloaded function templates and class templates:

template <typename T>
void f(T&& arg)               // forwarding reference
{ std::cout << "f(T&&)
"; }
template <typename T>
void f(T const&& arg)         // rvalue reference
{ std::cout << "f(T const&&)
"; }
template <typename T>
void f(std::vector<T>&& arg)  // rvalue reference
{ std::cout << "f(vector<T>&&)
"; }
template <typename T>
struct S
{
   void f(T&& arg)            // rvalue reference
   { std::cout << "S.f(T&&)
"; }
};

We can make calls to these functions as follows:

int x = 42;
f(x);                   // [1] f(T&&)
f(42);                  // [2] f(T&&)
int const cx = 100;
f(cx);                  // [3] f(T&&)
f(std::move(cx));       // [4] f(T const&&)
std::vector<int> v{ 42 };
f(v);                   // [5] f(T&&)
f(std::vector<int>{42});// [6] f(vector<T>&&)
S<int> s;
s.f(x);                 // [7] error
s.f(42);                // [8] S.f(T&&)

From this snippet, we can notice that:

  • Calling f with an lvalue or rvalue at [1] and [2] resolves to the first overload, f(T&&).
  • Calling f with a constant lvalue at [3] also resolves to the first overload, but calling f with a constant rvalue at [4] resolves to the second overload, f(T const&&), because it’s a better match.
  • Calling f with an lvalue std::vector object at [5] resolves to the first overload, but calling f with an rvalue std::vector object at [6] resolves to the third overload, f(vector<T>&&), because it’s a better match.
  • Calling S::f with an lvalue at [7] is an error because lvalues cannot be bound to rvalue references, but calling it with an rvalue at [8] is correct.

All the f function overloads in this example take an rvalue reference. However, the && in the first overload does not necessarily mean an rvalue reference. It means an rvalue reference if an rvalue was passed or an lvalue reference if an lvalue was passed. Such a reference is called a forwarding reference. However, forwarding references are only present in the context of an rvalue reference to a template parameter. It has to have the form T&& and nothing else. T const&& or std::vector<T>&& are not forwarding references, but normal rvalue references. Similarly, the T&& in the f function member of the class template S is also an rvalue reference because f is not a template but a non-template member function of a class template, so this rule for forwarding references does not apply.

Forwarding references are a special case of function template argument deduction, a topic that we previously discussed in this chapter. Their purpose is to enable perfect forwarding with templates and they are made possible by a new C++11 feature called reference collapsing. Let’s look at this first, before showing how they solve the perfect forwarding problem.

Prior to C++11, it was not possible to take a reference to a reference. However, that is now possible in C++11 for typedefs and templates. Here is an example:

using lrefint = int&;
using rrefint = int&&;
int x = 42;
lrefint&  r1 = x; // type of r1 is int&
lrefint&& r2 = x; // type of r2 is int&
rrefint&  r3 = x; // type of r3 is int&
rrefint&& r4 = 1; // type of r4 is int&&

The rule is pretty simple: an rvalue reference to an rvalue reference collapses to an rvalue reference; all other combinations collapse to an lvalue reference. This can be put in a tabular form as follows:

Table 4.2

Table 4.2

Any other combinations, shown in the following table, do not involve reference collapsing rules. These only apply when both types are references:

Table 4.3

Table 4.3

Forwarding references work not only for templates but also with auto deduction rules. When auto&& is found, it means a forwarding reference. The same does not apply for anything else, such as cv-qualified forms like auto const&&. Here are some examples:

int x = 42;
auto&& rx = x;          // [1] int&
auto&& rc = 42;         // [2] int&&
auto const&& rcx = x;   // [3] error
std::vector<int> v{ 42 };
auto&& rv = v[0];       // [4] int&

In the first two examples, rx and rc are both forwarding references and are bound to an lvalue and an rvalue respectively. However, rcx is an rvalue reference because auto const&& does not denote a forwarding reference. Therefore, trying to bind it to an lvalue is an error. Similarly, rv is a forwarding reference and is bound to an lvalue.

As previously mentioned, the purpose of forwarding references is to enable perfect forwarding. We have seen the concept of perfect forwarding earlier but in a non-template context. It works, however, in a similar manner with templates. To demonstrate this, let’s redefine the function h as a template function. It would look as follows:

void g(foo& v)  { std::cout << "g(foo&)
"; }
void g(foo&& v) { std::cout << "g(foo&&)
"; }
template <typename T> void h(T& v)  { g(v); }
template <typename T> void h(T&& v) { g(v); }
foo x{ 42 };
h(x);          // g(foo&)
h(foo{ 42 });  // g(foo&)

The implementation of the g overloads is the same, but the h overloads are now function templates. However, calling h with an lvalue and an rvalue actually resolves to the same call to g, the first overload taking an lvalue. This is because in the context of the function h, v is an lvalue so passing it to g will call the overload taking an lvalue.

The solution to this problem is the same as what we already saw before discussing templates. However, there is a difference: we no longer need two overloads, but a single one taking a forwarding reference:

template <typename T>
void h(T&& v)
{
   g(std::forward<T>(v));
}

This implementation is using std::forward to pass lvalues as lvalues and rvalues as rvalues. It works similarly for variadic function templates. The following is a conceptual implementation of the std::make_unique function that creates a std::unique_ptr object:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{
   return std::unique_ptr<T>(
           new T(std::forward<Args>(args)...));
}

To summarize this section, remember that forwarding references (also known as universal references) are basically a special deduction rule for function template arguments. They work based on the rules of reference collapsing and their purpose is to enable perfect forwarding. That is passing forward to another function a reference by preserving its valueness semantics: rvalues should be passed as rvalues and lvalues as lvalues.

The next topic that we will address in this chapter is the decltype specifier.

The decltype specifier

This specifier, introduced in C++11, returns the type of an expression. It is usually used in templates together with the auto specifier. Together, they can be used to declare the return type of a function template that depends on its template arguments, or the return type of a function that wraps another function and returns the result from executing the wrapped function.

The decltype specifier is not restricted for use in template code. It can be used with different expressions, and it yields different results based on the expression. The rules are as follows:

  1. If the expression is an identifier or a class member access, then the result is the type of the entity that is named by the expression. If the entity does not exist, or it is a function that has an overload set (more than one function with the same name exists), then the compiler will generate an error.
  2. If the expression is a function call or an overloaded operator function, then the result is the return type of the function. If the overloaded operator is wrapped in parentheses, these are ignored.
  3. If the expression is an lvalue, then the result type is an lvalue reference to the type of expression.
  4. If the expression is something else, then the result type is the type of the expression.

To understand these rules better, we’ll look at a set of examples. For these, we will consider the following functions and variables that we will use in decltype expressions:

int f() { return 42; }
int g() { return 0; }
int g(int a) { return a; }
struct wrapper
{
   int val;
   int get() const { return val; }
};
int a = 42;
int& ra = a;
const double d = 42.99;
long arr[10];
long l = 0;
char* p = nullptr;
char c = 'x';
wrapper w1{ 1 };
wrapper* w2 = new wrapper{ 2 };

The following listing shows multiple uses of the decltype specifier. The rule that applies in each case, as well as the deduced type, is specified on each line in a comment:

decltype(a) e1;             // R1, int
decltype(ra) e2 = a;        // R1, int&
decltype(f) e3;             // R1, int()
decltype(f()) e4;           // R2, int
decltype(g) e5;             // R1, error
decltype(g(1)) e6;          // R2, int
decltype(&f) e7 = nullptr;  // R4, int(*)()
decltype(d) e8 = 1;         // R1, const double
decltype(arr) e9;           // R1, long[10]
decltype(arr[1]) e10 = l;   // R3, long&
decltype(w1.val) e11;       // R1, int
decltype(w1.get()) e12;     // R1, int
decltype(w2->val) e13;      // R1, int
decltype(w2->get()) e14;    // R1, int
decltype(42) e15 = 1;       // R4, int
decltype(1 + 2) e16;        // R4, int
decltype(a + 1) e17;        // R4, int
decltype(a = 0) e18 = a;    // R3, int&
decltype(p) e19 = nullptr;  // R1, char*
decltype(*p) e20 = c;       // R3, char&
decltype(p[0]) e21 = c;     // R3, char&

We will not elaborate on all these declarations. Most of these are relatively easy to follow based on the specified rules. A few notes, however, are worth considering for clarifying some of the deduced types:

  • decltype(f) only names a function with an overloaded set, so rule 1 applies. decltype(g) also names a function but it has an overloaded set. Therefore, rule 1 applies and the compiler generates an error.
  • decltype(f()) and decltype(g(1)) are both using function calls for the expression, so the second rule applies, and even if g has an overload set, the declaration is correct.
  • decltype(&f) uses the address of the function f, so the fourth rule applies, yielding int(*)().
  • decltype(1+2) and decltype(a+1) use the overloaded operator + that returns an rvalue, so the fourth rule applies. The result is int. However, decltype(a = 1) uses the assignment operator that returns an lvalue, so the third rule applies, yielding the lvalue reference int&.

The decltype specifier defines an unevaluated context. This means the expression used with the specifier is not evaluated since this specifier is only querying the properties of its operand. You can see this in the following snippet, where the assignment a=1 is used with the decltype specifier to declare the variable e, but after the declaration, the value of a is the one with which it was initialized:

int a = 42;
decltype(a = 1) e = a;
std::cout << a << '
';  // prints 42

There is an exception to this rule concerning template instantiation. When the expression used with the decltype specifier contains a template, the template is instantiated before the expression is evaluated at compile time:

template <typename T>
struct wrapper
{
   T data;
};
decltype(wrapper<double>::data) e1;  // double
int a = 42;
decltype(wrapper<char>::data, a) e2; // int&

The type of e1 is double, and wrapper<double> is instantiated for this to be deduced. On the other hand, the type of e2 is int& (as the variable a is an lvalue). However, wrapper<char> is instantiated here even if the type is only deduced from the variable a (due to the use of the comma operator).

The preceding rules mentioned are not the only ones used for determining the type. There are several more for data member access. These are as follows:

  • The const or volatile specifiers of the object used in the decltype expression do not contribute to the deduced type.
  • Whether the object or pointer expression is an lvalue or an rvalue does not affect the deduced type.
  • If the data member access expression is parenthesized, such as decltype((expression)), then the previous two rules do not apply. The const or volatile qualifier of the object does affect the deduced type, including the valueness of the object.

The first two rules from this list are demonstrated with the following snippet:

struct foo
{
   int          a = 0;
   volatile int b = 0;
   const int    c = 42;
};
foo f;
foo const cf;
volatile foo* pf = &f;
decltype(f.a) e1 = 0;       // int
decltype(f.b) e2 = 0;       // int volatile
decltype(f.c) e3 = 0;       // int const
decltype(cf.a) e4 = 0;      // int
decltype(cf.b) e5 = 0;      // int volatile
decltype(cf.c) e6 = 0;      // int const
decltype(pf->a) e7 = 0;     // int
decltype(pf->b) e8 = 0;     // int volatile
decltype(pf->c) e9 = 0;     // int const
decltype(foo{}.a) e10 = 0;  // int
decltype(foo{}.b) e11 = 0;  // int volatile
decltype(foo{}.c) e12 = 0;  // int const

The deduced type for each case is mentioned on the right side in a comment. When the expression is parenthesized, these two rules are reversed. Let’s take a look at the following snippet:

foo f;
foo const cf;
volatile foo* pf = &f;
int x = 1;
int volatile y = 2;
int const z = 3;
decltype((f.a)) e1 = x;       // int&
decltype((f.b)) e2 = y;       // int volatile&
decltype((f.c)) e3 = z;       // int const&
decltype((cf.a)) e4 = x;      // int const&
decltype((cf.b)) e5 = y;      // int const volatile&
decltype((cf.c)) e6 = z;      // int const&
decltype((pf->a)) e7 = x;     // int volatile&
decltype((pf->b)) e8 = y;     // int volatile&
decltype((pf->c)) e9 = z;     // int const volatile&
decltype((foo{}.a)) e10 = 0;  // int&&
decltype((foo{}.b)) e11 = 0;  // int volatile&&
decltype((foo{}.c)) e12 = 0;  // int const&&

Here, all the expressions used with decltype for declaring variables e1 to e9 are lvalues, so the deduced type is an lvalue reference. On the other hand, the expression used to declare the variables e10, e11, and e12 is an rvalue; therefore, the deduced type is an rvalue reference. Furthermore, cf is a constant object and foo::a has the type int. Therefore, the result type is const int&. Similarly, foo::b has the type volatile int; therefore, the result type is const volatile int&. These are just a few examples from this snippet, but the others follow the same rules for deduction.

Because decltype is a type specifier, the redundant const and volatile qualifiers and reference specifiers are ignored. This is demonstrated with the following example:

int a = 0;
int& ra = a;
int const c = 42;
int volatile d = 99;
decltype(ra)& e1 = a;          // int&
decltype(c) const e2 = 1;      // int const
decltype(d) volatile e3 = 1;   // int volatile

So far in this section, we have learned how the decltype specifier works. However, its real purpose is to be used in templates, where the return value of a function depends on its template arguments and is not known before instantiation. To understand this scenario, let’s start with the following example of a function template that returns the minimum of two values:

template <typename T>
T minimum(T&& a, T&& b)
{
   return a < b ? a : b;
}

We can use this as follows:

auto m1 = minimum(1, 5);       // OK
auto m2 = minimum(18.49, 9.99);// OK
auto m3 = minimum(1, 9.99);    
                     // error, arguments of different type

The first two calls are both correct, as the supplied arguments are of the same type. The third call, however, will produce a compiler error, because the arguments have different types. For this to work, we need to cast the integer value to a double. However, there is an alternative: we could write a function template that takes two arguments of potentially different types and returns the minimum of the two. This can look as follows:

template <typename T, typename U>
??? minimum(T&& a, U&& b)
{
   return a < b ? a : b;
}

The question is, what is the return type of this function? This can be implemented differently, depending on the standard version you are using.

In C++11, we can use the auto specifier with a trailing return type, where we use the decltype specifier to deduce the return type from an expression. This would look as follows:

template <typename T, typename U>
auto minimum(T&& a, U&& b) -> decltype(a < b ? a : b)
{
   return a < b ? a : b;
}

This syntax can be simplified if you’re using C++14 or a newer version of the standard. The trailing return type is no longer necessary. You can write the same function as follows:

template <typename T, typename U>
decltype(auto) minimum(T&& a, U&& b)
{
   return a < b ? a : b;
}

It is possible to simplify this further and simply use auto for the return type, shown as follows:

template <typename T, typename U>
auto minimum(T&& a, U&& b)
{
   return a < b ? a : b;
}

Although decltype(auto) and auto have the same effect in this example, this is not always the case. Let’s consider the following example where we have a function returning a reference, and another function that calls it perfectly forwarding the argument:

template <typename T>
T const& func(T const& ref)
{
   return ref;
}
template <typename T>
auto func_caller(T&& ref)
{
   return func(std::forward<T>(ref));
}
int a = 42;
decltype(func(a))        r1 = func(a);        // int const&
decltype(func_caller(a)) r2 = func_caller(a); // int

The function func returns a reference, and func_caller is supposed to do a perfect forwarding to this function. By using auto for the return type, it is inferred as int in the preceding snippet (see variable r2). In order to do a perfect forwarding of the return type, we must use decltype(auto) for it, as shown next:

template <typename T>
decltype(auto) func_caller(T&& ref)
{
   return func(std::forward<T>(ref));
}
int a = 42;
decltype(func(a))        r1 = func(a);        // int const&
decltype(func_caller(a)) r2 = func_caller(a); // int const&

This time, the result is as intended, and the type of both r1 and r2 in this snippet is int const&.

As we have seen in this section, decltype is a type specifier used to deduce the type of an expression. It can be used in different contexts, but its purpose is for templates to determine the return type of a function and to ensure the perfect forwarding of it. Another feature that comes together with decltype is std::declval, which we will look at in the following section.

The std::declval type operator

The std::declval is a utility type operation function, available in the <utility> header. It’s in the same category as functions such as std::move and std::forward that we have already seen. What it does is very simple: it adds an rvalue reference to its type template argument. The declaration of this function looks as follows:

template<class T>
typename std::add_rvalue_reference<T>::type declval() noexcept;

This function has no definition and therefore it cannot be called directly. It can only be used in unevaluated contextsdecltype, sizeof, typeid, and noexcept. These are compile-time-only contexts that are not evaluated during runtime. The purpose of std::declval is to aid with dependent type evaluation for types that do not have a default constructor or have one but it cannot be accessed because it’s private or protected.

To understand how this works, let’s consider a class template that does the composition of two values of different types, and we want to create a type alias for the result of applying the plus operator to two values of these types. How could such a type alias be defined? Let’s start with the following form:

template <typename T, typename U>
struct composition
{
   using result_type = decltype(???);
};

We can use the decltype specifier but we need to provide an expression. We cannot say decltype(T + U) because these are types, not values. We could invoke the default constructor and, therefore, use the expression decltype(T{} + U{}). This can work fine for built-in types such as int and double, as shown in the following snippet:

static_assert(
  std::is_same_v<double, 
                 composition<int, double>::result_type>);

It can also work for types that have an (accessible) default constructor. But it cannot work for types that don’t have a default constructor. The following type wrapper is such an example:

struct wrapper
{
   wrapper(int const v) : value(v){}
   int value;
   friend wrapper operator+(int const a, wrapper const& w)
   {
      return wrapper(a + w.value);
   }
   friend wrapper operator+(wrapper const& w, int const a)
   {
      return wrapper(a + w.value);
   }
};
// error, no appropriate default constructor available
static_assert(
  std::is_same_v<wrapper, 
                 composition<int,wrapper>::result_type>);

The solution here is to use std::declval(). The implementation of the class template composition would change as follows:

template <typename T, typename U>
struct composition
{
   using result_type = decltype(std::declval<T>() + 
                                std::declval<U>());
};

With this change, both the static asserts previously shown compile without any error. This function avoids the need to use particular values to determine the type of an expression. It produces a value of a type T without involving a default constructor. The reason it returns an rvalue reference is to enable us to work with types that cannot be returned from a function, such as arrays and abstract types.

The definition of the wrapper class earlier contained two friend operators. Friendship, when templates are involved, has some particularities. We will discuss this in the next section.

Understanding friendship in templates

When you define a class, you can restrict access to its member data and member functions with the protected and private access specifiers. If a member is private, it can only be accessed within the class. If a member is protected, it can be accessed from derived classes with public or protected access. However, a class can grant access to its private or protected members to other functions or classes with the help of the friend keyword. These functions or classes, to which special access has been granted, are called friends. Let’s take a look at a simple example:

struct wrapper
{   
   wrapper(int const v) :value(v) {}
private:
   int value;
   friend void print(wrapper const & w);
};
void print(wrapper const& w)
{ std::cout << w.value << '
'; }
wrapper w{ 42 };
print(w);

The wrapper class has a private data member called value. There is a free function called print that takes an argument of the type wrapper and prints the wrapped value to the console. However, in order to be able to access it, the function is declared a friend of the wrapper class.

We will not focus on the way friendship works for non-templates. You should be familiar with this feature to proceed to discuss it in the context of templates. When it comes to templates, things get a bit complicated. We will look into this with the help of several examples. Let’s start with the following:

struct wrapper
{
   wrapper(int const v) :value(v) {}
private:
   int value;
   template <typename T>
   friend void print(wrapper const&);
   template <typename T>
   friend struct printer;
};   
template <typename T>
void print(wrapper const& w)
{ std::cout << w.value << '
'; }
template <typename T>
struct printer
{
   void operator()(wrapper const& w)
   { std::cout << w.value << '
'; }
};
wrapper w{ 42 };
print<int>(w);
print<char>(w);
printer<int>()(w);
printer<double>()(w);

The print function is now a function template. It has a type template parameter, but that’s not really used anywhere. That may look a bit odd, but it’s a valid code, and we need to invoke it by specifying the template argument. However, it helps us make a point: any template instantiation of print, regardless of the template argument, can access the private members of the wrapper class. Notice the syntax used to declare it as a friend of the wrapper class: it uses the template syntax. The same applies to the class template printer. It’s declared as a friend of the wrapper class and any template instantiation, regardless of the template argument, can access its private parts.

What if we wanted to restrict access to only some instances of these templates? Such as only the specializations for the int type? Then, we can declare these specializations as friends, as shown here:

struct wrapper;
template <typename T>
void print(wrapper const& w);
template <typename T>
struct printer;
struct wrapper
{
   wrapper(int const v) :value(v) {}
private:
   int value;
   friend void print<int>(wrapper const&);
   friend struct printer<int>;
};
template <typename T>
void print(wrapper const& w)
{ std::cout << w.value << '
'; /* error */ }
template <>
void print<int>(wrapper const& w)
{ std::cout << w.value << '
'; }
template <typename T>
struct printer
{
   void operator()(wrapper const& w)
   { std::cout << w.value << '
'; /* error*/ }
};
template <>
struct printer<int>
{
   void operator()(wrapper const& w)
   { std::cout << w.value << '
'; }
};
wrapper w{ 43 };
print<int>(w);
print<char>(w);
printer<int>()(w);
printer<double>()(w);

In this snippet, the wrapper class is the same as previously. For both the print function template and the printer class template, we have a primary template and a full specialization for the int type. Only the int instantiations are declared friends of the wrapper class. Attempting to access the private parts of the wrapper class in the primary templates generates compiler errors.

In these examples, the class that granted friendship to its private parts was a non-template class. But class templates can also declare friends. Let’s see how it works in this case. We will start with the case of a class template and a non-template function:

template <typename T>
struct wrapper
{
   wrapper(T const v) :value(v) {}
private:
   T value;
   friend void print(wrapper<int> const&);
};
void print(wrapper<int> const& w)
{ std::cout << w.value << '
'; }
void print(wrapper<char> const& w)
{ std::cout << w.value << '
'; /* error */ }

In this implementation, the wrapper class template declares the overload of print that takes a wrapper<int> as a parameter as being a friend. Therefore, in this overloaded function, we can access the private data member value, but not in any other overload. A similar case occurs when the friend function or class is a template and we want only one specialization to access the private parts. Let’s see the following snippet:

template <typename T>
struct printer;
template <typename T>
struct wrapper
{
   wrapper(T const v) :value(v) {}
private:
   T value;
   friend void print<int>(wrapper<int> const&);
   friend struct printer<int>;
};
template <typename T>
void print(wrapper<T> const& w)
{ std::cout << w.value << '
'; /* error */ }
template<>
void print(wrapper<int> const& w)
{ std::cout << w.value << '
'; }
template <typename T>
struct printer
{
   void operator()(wrapper<T> const& w)
   { std::cout << w.value << '
'; /* error */ }
};
template <>
struct printer<int>
{
   void operator()(wrapper<int> const& w)
   { std::cout << w.value << '
'; }
};

This implementation of the wrapper class template grants friendship to the int specializations of the print function template and printer class template. The attempt to access the private data member value in the primary templates (or any other specialization) would generate a compiler error.

If the intention is that the wrapper class template gives friend access to any instantiation of the print function template or printer class template, then the syntax to do so is as follows:

template <typename T>
struct printer;
template <typename T>
struct wrapper
{
   wrapper(T const v) :value(v) {}
private:
   T value;
   template <typename U>
   friend void print(wrapper<U> const&);
   template <typename U>
   friend struct printer;
};
template <typename T>
void print(wrapper<T> const& w)
{  std::cout << w.value << '
'; }
template <typename T>
struct printer
{
   void operator()(wrapper<T> const& w)
   {  std::cout << w.value << '
';  }
};

Notice that in declaring the friends, the syntax is template <typename U> and not template <typename T>. The name of the template parameter, U, can be anything except for T. That would shadow the name of the template parameter of the wrapper class template and that is an error. Keep in mind though that with this syntax, any specialization of print or printer has access to the private members of any specialization of the wrapper class template. If you want that only the specializations of the friends that meet the template argument of the wrapper class have access to its private parts, then you must use the following syntax:

template <typename T>
struct wrapper
{
   wrapper(T const v) :value(v) {}
private:
   T value;
   friend void print<T>(wrapper<T> const&);
   friend struct printer<T>;
};

This is similar to what we have seen previously when access was granted only to the int specializations, except that now it’s for any specialization that matches T.

Apart from these cases, it’s also possible for a class template to grant friendship to a type template parameter. This is demonstrated with the following example:

template <typename T>
struct connection
{
   connection(std::string const& host, int const port) 
      :ConnectionString(host + ":" + std::to_string(port)) 
   {}
private:
   std::string ConnectionString;
   friend T;
};
struct executor
{
   void run()
   {
      connection<executor> c("localhost", 1234);
      std::cout << c.ConnectionString << '
';
   }
};

The connection class template has a private data member called ConnectionString. The type template parameter T is a friend of the class. The executor class uses the instantiation connection<executor>, which means the executor type is the template argument and benefits from the friendship with the connection class so that it can access the private data member ConnectionString.

As can be seen from all these examples, friendship with templates is slightly different than friendship among non-template entities. Remember that friends have access to all the non-public members of a class. Therefore, friendship should be granted with care. On the other hand, if you need to grant access to some private members but not all, this is possible with the help of the client-attorney pattern. This pattern allows you to control the granularity of access to the private parts of a class. You can learn more about this pattern at this URL: https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Friendship_and_the_Attorney-Client.

Summary

In this chapter, we went through a series of advanced topics. We started with name binding and dependent names and learned how to use the typename and the template keywords to tell the compiler what kind of dependent names we are referring to. Then, we learned about recursive templates and how to implement compile-time versions, using different approaches, for a recursive function.

We also learned about argument deduction for both function templates and class templates and how to help the compiler to do the latter with the help of user-defined deduction guides. An important topic covered in this chapter was the forwarding references and how they help us to implement perfect forwarding. Toward the end of the chapter, we learned about the decltype type specifier, the std::declvalue type utility, and, lastly, how friendship works in the context of class templates.

In the next chapter, we begin utilizing the knowledge accumulated so far about templates to do template metaprogramming, which is, basically, writing code that is evaluated at compile-time.

Questions

  1. When is name lookup performed?
  2. What are deduction guides?
  3. What are forwarding references?
  4. What does decltype do?
  5. What does std::declval do?

Further readings

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset