dokee's site

Back

Modern C++ Basics - Advanced TemplateBlur image

Supplementary#

Template parameter that’s not a type#

Template template parameter#

template<typename T> // primary template
class A { int x; };
 
template<typename T> // partial specialization
class A<T*> { long x; };
 
// class template with a template template parameter V
template<template<typename> class V>
class C
{
    V<int> y;  // uses the primary template
    V<int*> z; // uses the partial specialization
};
 
C<A> c; // c.y.x has type int, c.z.x has type long
cpp
  • You can also use default parameter inside template template parameter.

NTTP#

  • Non-type template parameter (structural types):

    • Enumerations (special kind of integer);
    • Pointers (and nullptr_t), pointer to member; (with restrictions)
    • Lvalue reference;
    • Floating points; (pay attention to floating point rounding error)
    • Some “simple” classes;
    • constexpr Lambda.
  • Class NTTP:

    • Be aliteral types:
      • has a constexpr destructor
      • all of its non-static non-variant data members and base classes are of non-volatile literal types, and is one of
        • a non-union aggregate type
        • a type with at least one constexpr (possibly template) constructor that is not a copy or move constructor.
    • All base classes and non-static data members are public, non-mutable and structural types (or array of structural types).
    • Particularly, for pointer and reference member, it has the same restrictions as NTTP.
  • Since C++17, it’s also allowed to accept NTTP of any type.
  • It’s then easy to accept lambda as NTTP.
    • Notice that each lambda has its unique type even if they have same closure body, so the instantiation is unique theoretically.
template <std::invocable auto Callable>
class Foo {
public:
    constexpr decltype(auto) operator()() {
        return Callable();
    }
};

int main() {
    Foo<[]() { return 0; }> a;
    Foo<[]() { std::cout << "Hello World"; }> b;
}
cpp

Type Deduction#

Function parameter type deduction#

  • Each function parameter will deduce template parameter independently.
    • For non-reference parameter -> decay
    • For reference parameter -> original
  • In most cases, conversion is forbidden in deduction. Three special cases:
    • reference: convert to more cv-qualified
    • pointer: the deduced pointer can have qualification conversion
    • derived class (pointer): convert to base class (pointer)
template <typename T> void func1(const T&) {}
template <typename T> void func2(const T*) {}

int main() {
    int a { 1 };
    func1(a);  // int* -> const int*, T = int
    func2(&a); // int* -> const int* (not int* const), T = int
}
cpp
  • There also exists non-deduced context.

CTAD#

  • CATD: class template argument deduction

    • It deduces class template argument from constructor.
  • It implicitly generates deduction guides.

    • copy/move ctor: as deduction will strip reference, they’re combined together as a single hypothetic function.
template <class T>
struct UniquePtr {
    UniquePtr(T* t);
};
// Set of implicitly-generated deduction guides:
// F1: template<class T>
//     UniquePtr<T> F(T* p);
// F2: template<class T>
//     UniquePtr<T> F(UniquePtr<T>); // copy deduction candidate
UniquePtr dp { new auto(2.0) }; // -> UniquePtr<double>
cpp
  • It allows user-defined deduction guides.
    • It’s OK to add explicit and requires clause.
    • CTAD doesn’t have to be same as some ctor; deduction and overload resolution are separate.
    • The class name cannot use qualified name in deduction guide.
  • In class context, injected template name is preferred over CTAD.
    • X is X<T> by default; You need specify or use ::X to enable CTAD.
  • T&& is still universal reference,
    • Typically deduction guides use value type since it is not really the ctor itself.

Friend in class template#

Friend function#

  • Solution 1: make template method as friend instead of normal method.
    • Limitation: not exactly same as our previous intention.
template <typename T>
class Foo {
    template <typename U>
    friend void func(Foo<U>& arg);
};

template <typename U>
void func(Foo<U>& arg) {
    // ...
}
cpp
  • Solution 2: specify template instantiation as friend.
    • Limitation: hard to code
template <typename T>
class Foo;
template <typename T>
void func(Foo<T>& arg);

template <typename T>
class Foo {
    friend void func<T>(Foo<T>& arg);
};

template <typename T>
void func(Foo<T>& arg) {
    // ...
}
cpp
  • Solution 3: define friend methods inside the class directly, and use ADL to call it.
    • Limitation: ADL is quite obscure.

Friend class#

template <typename T>
class B {};

template <typename T>
class A {
    // Solution 1: all instantiations of B are friend.
    template <typename U>
    friend class B;
    // Solution 2: only one instantiation B<T> is friend.
    friend class B<T>;
    // Solution 3: template parameter as friend; ignored if T is not a class.
    friend T;
};
cpp

Lazy Instantiation#

  • When a class template is instantiated, not all of its members are immediately fully instantiated.
    • Some of them will only be fully instantiated when they’re actually used.

std::conditional_t#

  • You may need defer type evaluation.
template <typename T>
struct TryUnsignedT {
    using type = typename std::conditional_t<
        std::is_integral_v<T> && !std::is_same_v<T, bool>,
        std::make_unsigned<T>,
        std::type_identity<T>>::type;
};
cpp

Variadic Template#

Basics (pack expansion & …)#

template <typename T, typename... Args>
void print(T&& first_arg, const Args&... args) {
    std::cout << first_arg << "\n";
    if constexpr (sizeof...(args) != 0) { // or sizeof...(Args)
        print(args...);
    }
}
cpp
  • Pack expansion:
    • Write a pattern, applying pattern on every element and concatenating with ,.
    • It’s semantic substitution, not a comma expression.
template <typename F, typename... Args>
decltype(auto) invoke(F&& func, Args&&... args) {
    return func(std::forward<Args>(args)...);
    // same as func(std::forward<Arg1>(arg1), std::forward<Arg2>(arg2), ...)
}
cpp
template <typename... Args1>
struct zip {
    template <typename... Args2>
    struct with {
        using type = std::tuple<std::pair<Args1, Args2>...>;
        // Pair<Args1, Args2>... is the pack expansion
        // Pair<Args1, Args2> is the pattern
    };
};

using Type = zip<short, int>::with<unsigned short, unsigned>::type;
// std::tuple<std::pair<short, unsigned short>, std::pair<int, unsigned int>>
cpp
  • Variadic template is less preferred than normal template in overload resolution.
  • Pack can be indexed at compile time since C++26.
  • friend can also be expanded pack since C++26.
  • You can capture variadic arguments in lambda.

Fold expression#

  • Syntax:
    • (pack op ...)
    • (... op pack)
    • (pack op ... op init)
    • (init op ... op pack)
  • Which one is evaluated first depends on the evaluation order.
    • Except comma expression.
  • Without init, this expression is only valid when sizeof…(pack) > 0.
    • Except comma expression and logical expression.
template <typename... Args>
int sum(Args&&... args) {
    // return (args + ... + 1 * 2); // Error: operator with precedence below cast
    return (args + ... + (1 * 2));  // OK
}
cpp
template <typename... Args>
void print(const Args&... args) {
    ((std::cout << args << " "), ...);
    std::cout << std::endl;
}
cpp

Common Techniques#

CRTP#

  • Curiously Recurring Template Pattern (CRTP) is a technique for static polymorphism.
template <typename Derived>
class Student {
public:
    float getGpa() {
        return static_cast<Derived*>(this)->getGpaCoeff() * 4.0f;
    }
};

class Tom : public Student<Tom> {
public:
    float getGpaCoeff() const {
        return 0.8f;
    }
};
cpp
  • You can hide implementation by making base class friend.
template <typename Derived>
class Student {
public:
    using Super = Student<Derived>;
    float getGpa() {
        return static_cast<Derived*>(this)->getGpaCoeff() * 4.0f;
    }
};

class Tom : public Student<Tom> {
    friend Super;
    float getGpaCoeff() const {
        return 0.8f;
    }
};
cpp
  • Since C++23, you can use deducing this to simplify CRTP in some cases.

Type Erasure#

Modern C++ Basics - Advanced Template
https://dokee.moe/blog/c/modern-c-basics/advanced-template
Author dokee
Published at March 16, 2025
Comment seems to stuck. Try to refresh?✨