

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
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.
- 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">();
}
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;
}
cppType 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.
- 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
cpp- In class context, injected template name is preferred over CTAD.
X
isX<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>
}
};
cppT&&
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;
};
cppLazy 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;
};
cppVariadic 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.
- 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), ...)
}
cpptemplate <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>>
cpptemplate <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 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
}
cpptemplate <typename... Args>
void print(const Args&... args) {
((std::cout << args << " "), ...);
std::cout << std::endl;
}
cppCommon 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());
}
cppType 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