

Modern C++ Basics - Advanced Template
deducing this + CRTP: the Metaprogramming Equivalent of a Flex
  views
|  comments
 
 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- 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;
- constexprLambda.
 
- 
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.
 
- Be aliteral types:
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">();
}- 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;
}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!
}- 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
}- 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>- It allows user-defined deduction guides.
- It’s OK to add explicitand 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.
 
- It’s OK to add 
// 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- In class context, injected template name is preferred over CTAD.
- Xis- X<T>by default; You need specify or use- ::Xto 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>
    }
};- 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) {
    // ...
}- 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) {
    // ...
}- 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;
};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;
};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...);
    }
}- Pack expansion:
- Write a pattern, applying pattern on every element and concatenating with ,.
- It’s semantic substitution, not a comma expression.
 
- Write a pattern, applying pattern on every element and concatenating with 
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), ...)
}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>>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);
}- Variadic template is less preferred than normal template in overload resolution.
- Pack can be indexed at compile time since C++26.
- friendcan 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 whensizeof…(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
}template <typename... Args>
void print(const Args&... args) {
    ((std::cout << args << " "), ...);
    std::cout << std::endl;
}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;
    }
};- 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;
    }
};- 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());
}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 {});
}