Templates: A 101 Reference

Reading time ~18 minutes

Overview

Templates are a massive topic in C++ - one that is quite intimidating to beginners and novice programmers alike. This 101 Reference Guide attempts to briefly cover a number of relevant subtopics with code samples.


Template Categories

The summarizing visual shows the different template categories available as of C++14:

  1. Function Templates(Ref, Try Online!):
    template <typename T>
    void print(T elem) {
     // __PRETTY_FUNCTION__: Compiler provided pretty printer extension
     std::cout << __PRETTY_FUNCTION__ << std::endl;
    }
    // Calling Templated Fns
    print(3); // void print(T) [with T = int]
    print("abc"); // void print(T) [with T = const char*]
    
  2. Class Templates(Ref, Try Online!):
    template <typename T>
    struct Print{
     static void print_pretty_same(T elem) { // simple member function
         std::cout << __PRETTY_FUNCTION__ << std::endl;
         std::cout << elem << std::endl;
     }
     template <typename U>
     static void print_pretty_diff(U elem) { // templated member function
         std::cout << __PRETTY_FUNCTION__ << std::endl;
         std::cout << elem << std::endl;
     }
    };
    // Calling Template class Member Fns
    Print<const char*>::print_pretty_same("123"); // static void Print<T>::print_pretty_same(T) [with T = const char*]
    Print<int>::print_pretty_diff("123"); // static void Print<T>::print_pretty_diff(U) [with U = const char*; T = int]
    

    Note that the case of class templates can get especially complicated due to further templated member functions. This is very commonly done when attempting to overload template member functions(discussed here).

  3. Variable Templates(Ref, Try Online!):
    template <typename T1, typename T2>
    constexpr bool is_same_v = std::is_same<T1, T2>::value;
    std::cout << is_same_v<int, int> << std::endl; // 1
    
  4. Alias Templates(Ref, Try Online!):
    template <typename T>
    using MyType = T;
    MyType<int> x = 3;
     std::cout << x << std::endl; // 3
    

Default Template Parameters

Templates allow providing a default type to use if none is provided or deducible(Ref). Here is an example of a function template(Try Online!)

template <typename T, typename ToType = int>
void typePrinter(T elem) {
    std::cout << (ToType)(5) << std::endl;
}
typePrinter(5.23); // 5

Defaults are convenient and allow mixing with non-defaults as can be seen above.
Default template parameters are also allowed for Class Templates(Try Online), Variable Templates(Try Online) and Alias Templates(Try Online). Note though, that for these 3, while calling the template one must append <> eg. TemplateClass<> obj_;.
Additionally, when attempting to mix defaults with non-defaults for these 3, one must also be aware of certain restrictions which can be found at the start of the reference link above.


Template Type Specification/Deduction

Templates can either attempt to implicitly deduce the types of its parameters, or be be explicitly provided those types.

  1. Implicit Deduction(Ref, Try Online):
    template <typename T>
    void adder(T elem1, T elem2) {
     std::cout << __PRETTY_FUNCTION__ << std::endl;
     std::cout << (elem1 + elem2) << std::endl;
    }
    adder(10, 15); // implicitly deduce T=int
    

    The template can in the above example use the parameters passed to the function to deduce the template parameters. For example passing (10, 15) both of type int, helps the template deduce that T=int.

  2. Explicit Specification(Try Online):
    template <typename T>
    void castNumTo(int num) {
     std::cout << __PRETTY_FUNCTION__ << std::endl;
     std::cout << "Casted: " << static_cast<T>(num) << std::endl;
    }
    castNumTo<float>(23);
    

    In the example here, not explicitly specifying the types by <float> will have the compiler unsure which type to cast to - it must be specified. Examples of Non-deducible cases are: template_fn(var_typ1, var_type2) or template_fn().

  3. Partial Deduction(Try Online):
     // any types needing explicit specification come first
     // what can be deduced automatically can come second
    template <typename ToType, typename T>
    void adder(T elem1, T elem2) {
     std::cout << __PRETTY_FUNCTION__ << std::endl;
     std::cout << (ToType)(elem1 + elem2) << std::endl;
    }
    adder<int>(23.5, 0.23); // void adder(T, T) [with ToType = int; T = double]
    

    In the above example, the type T can be deduced, but ToType must be explicitly specified. When needing to write a signature such that non-deducible/explicitly specified types come first and anything that is to be/can be deduced comes at the end. eg. Template <typename NonDedType, typename DedType> fn(DedType);.
    Template type deduction is supported for Function Templates(Try Online) and Class Templates(post C++17, Try Online). Not for Variable/Alias Templates.


Template Full/Partial Specialization

This and the next section go into the rules of specifying custom implementations of a template based on certain criteria. Basically the implementations can go from generalized -> specialized. The most general form of the template class/function/etc is known as the primary template.
Specializations can be full or partial. The order of preference when matching invocations to signatures is Full Specialization -> Partial Specialization -> Primary Template. We’ll return to this topic in a later section. That said, let’s dive into template specialization.

  1. Template Full Specialization(Ref):

    Basically, when invoking a template function/class/variable, the compiler will first try to match with fully specialized versions only falling back to the primary template when no match is found. In addition to the categories shown above, it is also possible to fully specialize template class member functions(covered here already).
    Note how full specialization definitions must alwayes start with template <>. Let’s see some more code samples now:

    • Function Template Full Specialization(Try Online)
      template <typename T> // primary template
      void print_if_int32(T elem) {
       std::cout << "No int32 No print!" << std::endl;
      }
      template <>
      void print_if_int32(int32_t elem) {
       std::cout << "Good type Me print: " << elem << std::endl;
      }
      print_if_int32(23); // Good Type
      print_if_int32(23L); // No int32
      print_if_int32("abc"); // No int32
      
    • Class Template Full Specialization(Try Online)
      template <typename T> // primary
      struct MyClassImpl{
       T member_;
       MyClassImpl(T member): member_(member) {}
       void print() { std::cout << "Non Int Type: " << member_ << std::endl; }
      };
      template <>
      struct MyClassImpl<int>{
       int m1_;
       int m2_;
       MyClassImpl(int m1, int m2): m1_(m1), m2_(m2) {}
       void print() { std::cout << "Int Type!: addition is " << (m1_ + m2_) << std::endl; }
      };
      
    • Variable Template Full Specialization(Try Online)
      template<class T>
      constexpr auto pi = T(3.1415926535897932385L);  // primary template
      template<>
      constexpr auto pi<double> = 3.1415926535897932385;  // FS
      template<>
      constexpr auto pi< const char*> = "Hello Pi!";  // FS
      pi<double> // 3.14...
      pi<int> // 3
      pi<const char*> // Hello Pi
      
  2. Template Partial Specialization(Ref):
    • Note that it will always have <> after template name.
    • Class Template Partial Specialization:(Try Online)
      template <typename T>
      struct MyClassImpl{ // Primary/Base template
       T member_;
       MyClassImpl(T member): member_(member) {}
       void print() { std::cout << member_ << std::endl; }
      };
      template <typename T>
      struct MyClassImpl<T*>{ // Partial specialization: angle brackets after name
       T* member_;
       MyClassImpl(T* member): member_(member) {}
       void print() { std::cout << *member_ << std::endl; }
      };
      
    • Variable Template Partial Specialization:(Try Online)
      // Ref: https://stackoverflow.com/questions/41418432/variable-template-partial-specialization-and-constexpr
      template<int M, int N>
      const int gcd = gcd<N, M % N>;
      template<int M>
      const int gcd<M, 0> = M; // Partial specialization
      gcd<9, 6> // 3
      
    • Not required for function templates because overloading does the trick!
    • No partially specialized member functions allowed too - Can only be done by partially specializing classes.(Ref)

Function Template Overloading

Functions - be they free, member or templated - can all be overloaded(Reference). In fact, overloading is used to obtain an identical effect as partial specialization in functions.

Here is the corresponding code snippet(Try Online):

template <typename T>
bool is_pointer(T x) { // (1)
    return false;
}
template <typename Tp>
bool is_pointer(Tp* x) { // (2)
    return true;
}

Let’s add another layer of complexity - Mixing Overloads with Full specializations. What happens during compilation is visualized below:

Given this information, can you guess which version would be called for this code snippet:

template <typename T>
bool is_pointer(T x) { // (1)
    std::cout << "v1" << std::endl;
    return false;
}
template <>
bool is_pointer(int* x) { // (2)
    std::cout << "v2" << std::endl;
    return true;
}
template <typename Tp>
bool is_pointer(Tp* x) { // (3)
    std::cout << "v3" << std::endl;
    return true;
}
int elem = 3;
is_pointer(elem); // O1: ??
is_pointer(&elem); // O2: ??

The output of O1: v1 and O2: v3 - Try it here. So basically matching against overloads happens first - here (3) is the only available and matching overload. (2) is a viable full specialization of (3), but since it appears before (3), it is treated as a full specialization of (1). In summary: (1) => Primary Template
(2) => Full specialization of (1)
(3) => Overload of (1)
You can read more about this in this amazing answer or this talk.


Template Instantiation

A class template by itself is not a type, or an object, or any other entity. No code is generated from a source file that contains only template definitions. In order for any code to appear, a template must be instantiated: the template arguments must be provided so that the compiler can generate an actual class (or function, from a function template). - CPP Reference

In short, if a template is not instantiated you will not see the compiler output any assembly for it. For example this:

template <typename T>
T add(T num1, T num2) {
    return num1 + num2;
}

does not get compiled to any assembly by itself. Check it out yourself.

There are two ways in which assembly will be generated for this function:

  1. Implicit Instantiation: The compiler will automatically generate an assembly language definition for the template, when it is invoked in some usage context where the complete type/definition is needed. Here is an example.:
    int main() {
     add(5, 3); // complete definition needed here - implicitly instantiate
     return 0;
    }
    

    Here is how it looks on Compiler Explorer:

  2. Explicit Instantiation: An assembly language definition can also be forcefully generated by(Try it here!):
    template int add(int, int);
    // OR
    template int add<>(int, int);
    // OR
    template int add<int>(int, int);
    

    Here is how it looks on Compiler Explorer:

    One might wonder - why go about explicit instantiation, when implicit instantiation would happen anyways. There are two scenarios where it comes up:

    • Separating Template declaration and definition: This is extremely common for organizing declarations and definitions between header and cpp files to keep things readable. Here is a code example.
    • Distributing templates in libraries: Not so common scenario and found from a MSVC Reference - Explicit Instantiation is useful when you are creating library (.lib) files that use templates for distribution.

Template "What else?"

There are a few more topics which I haven’t covered in this post, but make for useful learning:

  1. Variadic Templates: Variadic templates allow for creation/evaluation of templates with a variable number of arguments. Eli Bendersky coverts this topic in a great amount of detail in his blog post.
  2. Template Type Deduction Rules: While I covered a few items about this, I didnt go into a lot of detail about the exact rules behind type deduction. This is covered very well in the first few chapters of Effective Modern C++.
  3. Template Member Function Overload Resolution: Quite a mouthful huh! As we read, template functions(both simple and class member functions) can be customized to work differently based on the parameter type. I’ve written a complete blog post on this topic about a month back.
  4. Template Type Maps: This is a useful trick to create a compile time type mapping which allows something like setting up the correct set of functions/types/etc based on what the template parameter type is. Here is an example.

References


That’s all, thanks for reading :) - You can post a comment or reach out to me at saatvikshah1994@gmail.com.

Paper Summary: Kafka

A short visual summary of the original Kafka paper. Continue reading