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.
template<std::size_t N>
struct FixedString {
    char str[N];
    constexpr FixedString(const char (&input)[N]) {
        for (std::size_t i = 0; i < N; i++) {
            str[i] = input[i];
        }
    }
};

template<FixedString InputStr>
void test() {
    std::println(InputStr.str);
}

int main() {
    test<"Hello World">();
}
cpp
  • 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
template <typename T>
void func1(T a, T b) {}

template <typename T>
void func2(T& a, T& b) {}

template <typename T>
void func3(const T a, const T b) {}

int main() {
    const int a { 1 }, b { 2 };
    func1(a, b); // T -> int
    func2(a, b); // T -> const int
    func3(a, b); // T -> int

    int c[8], d[10];
    func1(c, d); // T -> int*
    // func2(c, d); // a: T -> int[8]; b: T -> int[10] conflict!
}
cpp
  • 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.
// declaration of the template
template<class T>
struct container
{
    container(T t) {}
 
    template<class Iter>
    container(Iter beg, Iter end);
};
 
// additional deduction guide
template<class Iter>
container(Iter b, Iter e) -> container<typename std::iterator_traits<Iter>::value_type>;
 
// uses
container c(7); // OK: deduces T=int using an implicitly-generated guide
[std::vector](http://en.cppreference.com/w/cpp/container/vector)<double> v = {/* ... */};
auto d = container(v.begin(), v.end()); // OK: deduces T=double
container e{5, 6}; // Error: there is no std::iterator_traits<int>::value_type
cpp
  • 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.
template <class T>
struct X {
    X(T) {}
    template <class Iter>
    X(Iter b, Iter e) {}

    template <class Iter>
    auto foo(Iter b, Iter e) {
        return X(b, e); // no deduction: X is the current X<T>
    }
    template <class Iter>
    auto bar(Iter b, Iter e) {
        return X<typename Iter::value_type>(b, e); // must specify what we want
    }
    auto baz() {
        return ::X(0); // not the injected-class-name; deduced to be X<int>
    }
};
cpp
  • 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
template <typename... Funcs>
struct Overloaded : public Funcs... {
    using Funcs::operator()...;
};

template <typename... Args>
Overloaded(Args...) -> Overloaded<Args...>; // optional since C++20

int main() {
    auto overloads = Overloaded {
        [](int i) { return std::to_string(i); },
        [](const std::string& s) { return s; },
        [](auto&& val) { return std::string{}; },
    };
    std::variant<int, std::string> v { 1 };
    std::visit(overloads, v);
}
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.
class Student {
public:
    float getGpa(this auto&& self) {
        return self.getGpaCoeff() * 4.0f;
    }
};

class Tom : public Student {
    friend Student;
    float getGpaCoeff() const {
        return 0.8f;
    }
};

template <typename T>
concept StudentBased = std::derived_from<T, Student>;

void func(StudentBased auto& student) {
    std::println("{}", student.getGpa());
}
cpp

Type Erasure#

class StudentProxyBase {
public:
    virtual float getGpaCoeff() = 0;
    virtual StudentProxyBase* clone() const = 0;
    virtual ~StudentProxyBase() = default;
};

template <typename ConcreteStudentType>
class StudentProxy : public StudentProxyBase {
    ConcreteStudentType student;

public:
    // You can also add another overload for ConcreteStudentType&&.
    StudentProxy(const ConcreteStudentType& init_student) : student { init_student } {}
    float getGpaCoeff() override {
        return student.getGpaCoeff();
    }
    StudentProxy* clone() const override {
        return new StudentProxy { student };
    }
};

class StudentBase {
    StudentProxyBase* studentProxy = nullptr;

public:
    StudentBase() = default;

    template <typename T>
    StudentBase(T&& student) {
        studentProxy = new StudentProxy { std::forward<T>(student) };
    }

    StudentBase(const StudentBase& another) : studentProxy { another.studentProxy->clone() } {}

    StudentBase(StudentBase&& another) :
        studentProxy { std::exchange(another.studentProxy, nullptr) } {}

    float getGpa() {
        if (studentProxy == nullptr) {
            throw std::runtime_error { "Cannot deference without underlying student" };
        }
        return studentProxy->getGpaCoeff() * 4.0f;
    }
    ~StudentBase() {
        delete studentProxy;
    }
};

class Student {
public:
    float getGpaCoeff() {
        return 0.8f;
    }
};

class JuanWang {
public:
    float getGpaCoeff() {
        return 1.0f;
    }
};

void printGpa(StudentBase student) {
    std::println("{}", student.getGpa());
}

int main() {
    printGpa(Student {});
    printGpa(JuanWang {});
}
cpp
Modern C++ Basics - Advanced Template
https://astro-pure.js.org/blog/c/modern-c-basics/advanced-template
Author dokee
Published at March 16, 2025
Comment seems to stuck. Try to refresh?✨