《C++ Templates》1. 基础知识

https://github.com/Walton1128/CPP-Templates-2nd–

C++ 11、14 和 17 中的新特性

这一部分列出来了需要学习的新特性

C++ 11 引入了非常多的特性:

  • 变参模板(Variadic templates)
  • 模板别名(Alias templates)
  • 移动语义(Move semantics)、右值引用(RValue references)和完美转发(Perfect forwarding),这三个需要之后去着重看一下
  • 标准类型萃取(Standard type traits)

C++ 14 与 C++ 17 也引入了一些新的特性,但没有 C++ 11 那么多:

  • 变量模板(Variable templates,C++ 14)
  • 泛型 lambda(Generic Lambdas,C++ 14)
  • 类模板参数推断(Class template argument deduction,C++ 17)
  • 折叠表达式(Fold expression,C++ 17)

C++ 20 中,引入了:

  • 模板接口(Concept)

函数模板

两阶段编译检查(Two-Phase Translation)

在编译阶段,模板并不是被编译成一个实体,而是对于每一个使用该模板的类型都生成实体

生成实体的过程被称作实例化

类型可以被推断为 void

两阶段编译检查:

  • 模板定义阶段:
    • 检查语法(如少不少分号)
    • 检查是否有未定义的名称(如模板参数中没有 T 却使用了它)
    • 检查不依赖于模板参数的静态断言(此时没有装填具体的类型进去,只能进行这种断言的检查)
  • 模板实例化阶段:检查其他东西,尤其是依赖于模板参数的部分

类型推断

类型推断中的类型转换有限制:

  • 引用传递的参数不能进行任何类型的类型转换
  • 按值传递只允许进行退化(decay):如忽略(顶层) constvolatile、去掉引用、数组和函数转换为指针

缺省参数不参与类型推断,如:

template <typename T>
void f(T value = "") {}

f(); // 报错,因为无法推断 T 的类型
template <typename T = std::string>
void f(T value = "") {}

f(); // 正常

多个模板参数

当使用了多个模板参数后,这么写的话,返回值的类型可能会与传参顺序有关:

template <typename T1, typename T2>
T1 max(T1 a, T2 b) { ... }

auto m1 = max(4, 7.2); // 返回 int
auto m2 = max(7.2, 4); // 返回 double

为了避免这种情况,有三种方法:

第一种是引入第三个模板参数作为返回类型:

template <typename RT, typename T1, typename T2>
RT max (T1 a, T2 b);

max<double>(4, 7.2); // 不需指定所有类型,只需要指定返回值类型,T1 和 T2 会根据传入参数自动推断

第二种是返回类型推断:

template <typename T1, typename T2>
auto max(T1 a, T2 b) {
    return b < a ? a : b;
}

如果不使用尾置返回类型,那么必须能够通过函数体内的语句推断出来,并且多个返回语句的推断结果必须一致

C++ 11 中,尾置返回类型允许我们使用函数的参数来进行推断:

template <typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(b < a ? a : b);

这里 decltype 里面的表达式并不会被计算,所以与 decltype(true ? a : b) 等价

但是当输入是引用时,推断出的类型也可能是引用(decltype 推断时会保留引用),我们需要进行一个退化(decay):

#include <type_traits>

template <typename T1, typename T2>
auto max(T1 a, T2 b) -> typename std::decay<decltype(b < a ? a : b)>::type;

这个 std::decay<> 会进行左值到右值、数组到指针、函数到函数指针的退化,并且会移除 cv 限定符,auto 默认就是退化的类型

第三种是将返回类型声明为公共类型:

#include <type_traits>

template <typename T1, typename T2>
std::common_type_t<T1, T2> max(T1 a, T2 b);

这里的 std::common_type_t<> 也是一个类型萃取,它返回两个类型的公共类型,并且是退化的

函数模板的重载

一个非模板函数可以和一个与其同名的函数模板共存,并且这个同名的函数模板可以被实例化为与非模板函数具有相同类型的调用参数

int max(int, int);

template <typename T>
T max(T, T);

int main() {
    ::max(7, 42);            // 非模板函数
    ::max(7.0, 42.0);        // 模板函数
    ::max('a', 'b');         // 模板函数
    ::max<>(7, 42);          // 模板函数
    ::max<double>(7, 42);    // 模板函数
    ::max('a', 42);          // 非模板函数
}

有一些规则:

  • 在所有其他因素都相同的情况下,模板解析优先选择非模板函数,而不是从模板实例化出来的函数
  • 如果模板可以实例化出一个更优秀的函数,那么就会选择这个函数

在模板参数推断时不允许自动类型转换

如果两个模板都匹配到了,那么会有二义性

在重载模板时要尽可能少做改动,应该只改变模板参数的个数或者显式指定某些模板参数。否则可能遇到意想不到的问题:

#include <cstring>

template <typename T>
T const & max(const T & a, const T & b) {
    return b < a ? a : b;
}

const char * max(const char * a, const char * b) {
    return std::strcmp(b, a) < 0 ? a : b;
}

template <typename T>
T const & max(const T & a, const T & b, const T & c) {
    return max(max(a, b), c); // 可能会返回一个局部变量的引用
}

int main() {
    auto m1 = ::max(7, 42, 68); // OK
    const char * s1 = "frederic";
    const char * s2 = "anica";
    const char * s3 = "lucas";
    auto m2 = ::max(s1, s2, s3) // 运行时错误
    
}

需要确保函数模板在被调用时,其已经在前方某处定义

一些共识

在模板中,按值传递好于按引用传递:

  • 语法简单
  • 编译器能够更好地进行优化
  • 移动语义通常使得拷贝成本比较低
  • 某些情况下可能没有拷贝或者移动
  • 模板既可用于简单类型,也可用于复杂类型,如果使用引用传递,那么可能对简单类型产生不利影响
  • 可以使用 std::ref()std::cref() 传递引用参数
  • 引用传递对 C 风格字符串和数组有着非常大的问题

模板不需要被声明成 inline

类模板

模板函数和模板类只有在被调用时才会实例化

如果一个类模板有 static 成员,对每一个用到这个类模板的类型,相应的静态成员也只会被实例化一次

模板参数唯一的要求是:它要支持模板中被用到的各种操作(运算符)

Concept

我们如何才能知道为了实例化一个模板需要哪些操作?答案就是 Concept,它通常被用来表示一组反复被模板库要求的限制条件

这个概念很早就出现了,但是直到 C++ 20 才被写进 C++ 标准。这本书只到了 C++ 17,所以不会讨论 C++ 20 中的 Concept

从 C++ 11 开始,可以通过关键字 static_assert 和一些预定义的类型萃取来做一些简单的检查:

template <typename T>
class C {
    static_assert(std::is_default_constructible<T>::value,
                  "Class C requires default-constructible elements");
    ...
};

模板类的全特化与部分特化

函数模板不支持偏特化,但是支持全特化

原来的模板类是这样的:

template <typename T>
class Stack<T> {
    ...
};

可以对类模板的某一个模板参数进行特化,和函数模板的重载类似,类模板的特化允许我们对某一特定类型做优化,或者去修正类模板针对某一特定类型实例化之后的行为

语法如下:

template <>
class Stack<std::string> {
    ...
};

类模板可以只被部分的特化,这样就可以为某些特殊情况提供特殊的实现

语法如下:

template <typename T>
class Stack<T *> {
    ...
};

这样就是被特化专门用来处理指针的类模板

全特化后也不会立即实例化,而是在使用时才会实例化

类型别名

从 C++ 14 开始,标准库使用了 using 类型别名的技术,定义了一种”快捷方式“,可以使用 _t 后缀的类型萃取

标准库中的定义如下:

namespace std {
    template <typename T>
    using add_const_t = typename add_const<T>::type;
}

类模板的类型推导

直到 C++ 17,使用类模板时都必须显式指出所有的模板参数的类型(除非它们有默认值)

从 C++ 17 开始,这一要求不在那么严格了,如果构造函数能够推导出来,那么就可以不用显式指明类型

和函数模板不同,类模板可能无法部分的推断模板类型参数

类模板对字符串常量参数的类型推断:

template <typename T>
class Stack<T> {
public:
    Stack(const T &);
    ...
};

Stack stringStack = "bottom";  // 它会推断成 Stack<const char[7]>

它不会被退化,这样会导致一些问题

但是如果改成值传递的话就会被退化:

template <typename T>
class Stack<T> {
public:
    Stack(T);
    ...
};

Stack stringStack = "bottom";  // 它会推断成 Stack<const char *>

除了这种方法外,还可以使用推断指引(deduction guides)来提供额外的模板参数推断规则

它的语法是在类外部(需要在和模板类的定义相同的作用域或者命名空间内)写这样的语句:

Stack(const char *) -> Stack<std::string>;

但是这样也不行,因为隐式转换最多被转换一步!这里转换了两步:

  • const char * 转换成 std::string
  • std::string 转换成 Stack<std::string>

我们需要显式调用构造函数:

Stack stringStack{"bottom"};

聚合类的模板化

聚合类就是:

  • 没有用户定义的显式的,或者继承而来的构造函数
  • 没有 private 或者 protected 的非静态成员
  • 没有虚函数
  • 没有 virtualprivate 或者 protected 的基类

的类或者结构体。它也可以是模板:

template <typename T>
struct ValueWithComment {
    T value;
    std::string comment;
};

从 C++ 17 开始,对于聚合类的类模板甚至可以使用类型推断指引:

ValueWithComment(char const*, char const*) -> ValueWithComment<std::string>;
ValueWithComment vc2 = {"hello", "initial value"};

注意,如果没有推断指引的话,就不能使用上述初始化方法,因为 ValueWithComment 没有相应的构造函数来完成相关类型推断

标准库的 std::array<> 类也是一个聚合类,其元素类型和尺寸都是被参数化的。C++ 17 也给它定义了推断指引

非类型模板参数

函数模板和类模板的模板参数不一定非得是某种具体的类型,也可以是常规数值

使用非类型模板参数是有限制的,通常它们只能是:

  • 整形常量(包含枚举)
  • 指向对象、函数或对象成员的指针
  • 对象或者函数的左值引用
  • 或者 std::nullptr_t 类型

浮点型数值或者类类型的对象都不能作为非类型模板参数使用,如:

template <double VAT>
double process (double v);

template <std::string name>
class C { ... };

上面两个都不行

当传递对象的指针或者引用作为模板参数时,对象不能是字符串常量、临时变量或者数据成员以及其它子对象,C++ 的每次版本都会放宽限制

下面写法不对:

template<const char * name>
class C { ... };

C<"hello"> x;

不过根据 C++ 版本不同有以下变通方法:

extern const char s1[] = "hi";    // 外部链接性
const char s2[] = "hi";           // 内部链接性

int main() {
    C<s1> c1;   // 外部链接性,所有版本都可以
    C<s2> c2;   // 内部链接性,C++ 11 和 C++ 14 可以
    
    static const char s3[] = "hi"; // 没有链接性
    
    C<s3> c3;   // 没有链接性,C++ 17 以上可以
}

如果在表达式中使用了 operator >,就必须将相应表达式放在括号里面:

C<42, sizeof(int) > 4> c;   // 错误,第一个 > 会被当成结尾
C<42, (sizeof(int) > 4)> c; // 正确

auto 作为非模板类型参数的类型

从 C++ 17 开始,可以不指定非类型模板参数的具体类型(代之以 auto),从而使其可以用于任意有效的非类型模板参数的类型,如:

template<typename T, auto Maxsize>
class Stack {
public:
    using size_type = decltype(Maxsize);
    ...
};

其中 Maxsize 的类型可以是任意非类型参数所允许的类型

decltype(auto):使用 decltype 来进行类型推导,如:

template <decltype(auto) N>
class C { ... };

int i;
C<(i)> x;

由于 decltype 不会进行退化,所以可以保留引用

变参模板

从 C++ 11 开始,模板可以接受一组数量可变的参数,这样就可以在参数数量和参数类型都不确定的情况下使用模板

使用方法如下:

#include <iostream>

void print() {}

template <typename T, typename... Types>
void print(T firstArg, Types... args) {
    std::cout << firstArg << endl;
    print(args...);
}

上面的 args 被称为函数参数包(function parameter pack)

使用递归的方式

它在泛型库的开发中有重要作用,比如 C++ 标准库

一个重要的作用是转发任意类型和数量的参数,通常是使用移动语义对参数进行完美转发(perfectly forwarded)

sizeof... 运算符

C++ 11 为变参模板引入了新的 sizeof 运算符:sizeof...,它会被扩展成参数包中所包含的参数数目:

template <typename T, typename... Types>
void print(T firstArg, Types... args) {
    std::cout << firstArg << endl;
    std::cout << sizeof...(Types) << endl;  // 打印剩余的类型数量
    std::cout << sizeof...(args) << endl;   // 打印剩余的类型数量
    print(args...);
}

上面两个 sizeof... 等价

注意,下面的写法不对(不写无参的 print 来结束递归):

#include <iostream>
template <typename T, typename... Types>
void print(T firstArg, Types... args) {
    std::cout << firstArg << endl;
    if (sizeof...(args) > 0) {
        print(args...);
    }
}

这是因为函数模板中 if 语句的两个分支都会被实例化,是否使用被实例化出来的代码是在运行期间决定的,而是否实例化代码是在编译期间决定的

由于没有定义无参版本的函数,所以会编译错误

不过从 C++ 17 开始,可以使用编译阶段的 if 语句(应该是 constexpr

折叠表达式

从 C++ 17 开始,提供了一种可以用来计算参数包(可以有初始值)中所有参数运算结果的二元运算符

比如,下面的函数会返回 s 中所有参数的和:

template<typename... T>
auto foldSum(T... s) {
    return (... + s);
}

这个叫做折叠表达式

一些折叠表达式:

  • (... op pack)(((pack1 op pack2) op pack3) op pack4)
  • (pack op ...)(pack1 op ((pack2 op pack3) op pack4))
  • (init op ... op pack)(init op (pack1 op ((pack2 op pack3) op pack4)))
  • (pack op ... op init) :((pack1 op ((pack2 op pack3) op pack4)) op init)

几乎所有的二元运算符都可以用于折叠表达式,比如 ->*(用于成员指针)、<<(可以简化输出)、, 等,这一个东西后面章节会有更深入的解释

变参表达式

变参表达式是 C++ 11 提供的,例如:

template <typename... Args>
void printDouble(Args const &... args) {
    print(args + args...);
    // 等价于 print(args[0] + args[0], args[1] + args[1], ..., args[n] + args[n])
}

上面的代码中,... 优先级最低,等价于 (args + args)...,在含有参数包的表达式后面加上 ... 就可以做到:用参数包中的每个具体参数替换表达式中的参数包,并且产生一个新的参数包

当后面是个数字时,并不能直接加 ...,必须加上括号:(args + 1)...

constexpr 编译阶段表达式同样可以包含模板参数包,如:

template <typename T1, typename... TN>
constexpr bool isHomogeneous(T1, TN...) {
    return (std::is_same<T1, TN>::value && ...);
    // 等价于 std::is_same<T1, TN[0]>::value && std::is_same<T1, TN[1]>::value && ... && std::is_same<T1, TN[n]>::value
}

上面的代码将 C++ 11 的变参表达式和 C++ 17 的折叠表达式结合起来了

变参下标(variadic indices)是变参表达式的一个应用:

template <typename T, typename... Idx>
void printElems(const T & coll, Idx... idx) {
    print (coll[idx]...);
}

变参类模板、变参推断指引与变参基类

类模板也可以是变参的

一个重要的例子是,通过任意多个模板参数指定了类相应数据成员的类型,例如元组类型

另一个例子是指定对象可能包含的类型,例如 Variant 类型

这是迈向元编程(meta-programming)的第一步

推断指引也可以是变参的,例如标准库中的 array 类型:

namespace std {
    template <typename T, typename... U> array(T, U...) ->
    array<enable_if_t<(is_same_v<T, U> && ...), T>, (1 + sizeof...(U))>;
}

它的意思是:判断首元素和剩下的所有元素是否相同,如果相同,那么基于首元素类型和参数包中参数个数构造 array,如果不同则编译错误

变参基类,例子:

template <typename... Bases>
class Overloader : Bases... {
    using Bases::operator()...;
};

它从个数不定的基类派生出了一个新的类,并且从其每个基类中引入了 operator() 的声明

后面会有具体的使用了该技术的例子

基础技巧

使用 typename 关键字拿到依赖于模板参数的类型

template <typename T>
class C {
public:
    ...
    void foo() {
        typename T::SubType* ptr;
    }
};

上面的代码中,如果没有 typename,那么 SubType 可能会被认为是一个静态成员或者枚举变量,而不是一个类型,会有一些问题

通常而言,当一个依赖于模板参数的名称代表的是某种类型的时候,就必须使用 typename

但是对于 C++ 20,在某些常规情况下可能不再需要 typename

内置类型的零初始化

对于内置类型如 intdouble 等,它们作为局部变量时是不会被初始化的

下面的代码会出现问题:

template <typename T>
void foo() {
    T x; // 如果 T 是个内置类型,那么这就是个未定义行为
}

我们最好显式调用默认构造函数来将它们初始化成 0:

T x{};

或者可以使用拷贝初始化:

T x = T();

在 C++ 17 之前,这种方法要求拷贝构造函数不能被声明为 explicit

从 C++ 17 开始,引入了强制拷贝省略(mandatory copy elision),通过返回值优化(RVO)等操作避免拷贝。这时两种方法都可用

不管大括号有个优点,它还可以使用列表初始化构造函数

依赖名称(dependent name)问题

当基类本身是一个模板参数时,在派生类中调用基类的方法可能会遇到**依赖名称(dependent name)**的问题,例如:

template <typename T>
struct Base {
    void func() {
        std::cout << "Base<T>::func()" << std::endl;
    }
};

template <typename T>
struct Derived : public Base<T> {
    void callBaseFunc() {
        func();
    }
};

上面的代码会编译错误,由于 Base<T> 是一个依赖模板参数,编译器不会自动查找基类的成员,导致 func(); 语句无法解析

我们可以显式使用 this->func() 来调用基类的方法

也可以使用 Base<T>::func() 或者 using Base<T>::func

使用裸数组或者字符串常量的模板

当向模板传递裸数组或者字符串常量时,需要格外注意以下内容:

  • 如果参数是按引用传递的,那么参数类型不会退化。也就是说当传递 "hello" 作为参数时,模板类型会被推断为 char const[6],不注意的话可能出问题
  • 只有当按值传递参数时,模板类型才会退化

可以这样定义模板:

template <typename T, int N, int M>
bool less(T (&a)[N], T (&b)[M]) { ... }

其中,T (&a)[N] 叫做数组的引用,可以帮助我们防止数组退化为指针。可以与 T (*a)[N] 相区分

成员模板

类的成员也可以是模板,对嵌套类和成员函数都是这样

成员模板也可以被特化:

class BoolString {
public:
    template <typename T = std::string>
    T get() const;
};

template <>
inline bool BoolString::get<bool>() const { ... }

有时,在调用成员模板的时候需要显式地指定其模板参数的类型。这时需要使用 template 关键字来让编译器理解 < 后面是模板参数列表的开始,而不是一个比较运算符

泛型 lambda

C++ 14 中引入了泛型 lambda,是一种成员模板的简化。例如下面的 lambda:

[] (auto x, auto y) {
    return x + y;
}

会被编译器解释为:

class SomeCompilerSpecificName {
public:
    SomeCompilerSpecificName();
    template <typename T1, typename T2>
    auto operator()(T1 x, T2 y) const {
        return x + y;
    }
};

变量模板

C++ 14 开始,变量也可以被类型参数化,称为变量模板。例如:

template <typename T>
constexpr T pi{3.1415926535897932385};

它可以有默认的模板参数,但是调用时必须加上尖括号,如 pi<>,不能只写一个 pi

模板参数模板

模板参数也可以是一个类模板,例如:

template <typename T, template <typename Elem> class Cont = std::vector>
class Stack {
private:
    Cont<T> elems;
public:
    ...
};

第二个参数就是类模板

在 C++ 11 之前,只能使用 class 关键字,不可以使用模板别名,而且只能模板参数完全匹配(不能智能地使用默认模板参数),例如如果用 std::vector 会报下面的错误:

**.cc:86:27: error: type/value mismatch at argument 2 in template parameter list for 'template<class T, template<class Elem> class Cont> class Stack'
   86 |     Stack<int, std::vector> t2;
      |                           ^
**.cc:86:27: note:   expected a template of type 'template<class Elem> class Cont', got 'template<class _Tp, class _Alloc> class std::vector'
ninja: build stopped: subcommand failed.

C++ 11 之后,第二个参数可以使用模板别名,但是模板参数仍然需要完全匹配,仍不能使用 typename

template <typename T, typename U>
struct A;

template <typename T>
using AInt = A<T, int>;

template <typename T, template <typename Elem> class Cont>
class Stack { ... };

Stack<int, AInt> s;

C++ 17 之后现在可以使用 typename 了,并且也可以智能使用默认模板参数了:

template <typename T, template <typename Elem> typename Cont = std::vector>
class Stack { ... };

C++ 17 之前虽然需要模板参数完全匹配,但也可以变通:

template <typename T, template<typename Elem, typename Alloc = std::allocator<Elem>> class Cont = std::deque>
class Stack { ... };

移动语义和 enable_if<>

完美转发

C++ 中的引用与值类型在使用上没有区别

在函数中,如果某个形参被定义为右值引用,一旦绑定了实参,那么这个实参就有了“名字”,不再是右值了,它变成了左值

这时如果需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下我们需要保持被转发实参的所有性质,包括 cv、左右值

我们就可以使用完美转发

特殊成员函数模板

特殊成员函数也可以是模板,比如构造函数,但是有时候这可能会带来令人意外的结果

使用 std::enable_if<>

从 C++ 11 开始,通过 C++ 标准库提供的辅助模板 std::enable_if<>,可以在某些编译期条件下忽略掉函数模板

std::enable_if<> 是一种类型萃取(type trait),它会根据第一个 bool 参数做下面的事情:

  • 如果这个表达式结果为 true,它的 type 成员会返回一个类型(第二个参数,默认是 void
  • 如果表达式结果 false,则它没有 type 成员

常见的方法是使用一个额外的、有默认值的模板参数:

template <typename T, typename = std::enable_if_t<(sizeof(T) > 4)>>
void foo() { ... }

std::enable_if<> 基于 SFINAE,可以用来禁用模板函数

在 C++ 20 中,引入了 concept,可以用来简化 std::enable_if<> 表达式

按值传递还是按引用传递

当按值传递参数时,原则上所有的参数都会被拷贝。因此每一个参数都会是被传递实参的一份拷贝

调用拷贝构造函数的成本可能很高。但是有多种方法可以避免按值传递的高昂成本:事实上编译器可以通过移动语义(move semantics)来优化掉对象的拷贝,这样即使是对复杂类型的拷贝,其成本也不会很高

推荐在默认情况下,将参数声明为按值传递

按值传递会导致类型退化按引用传递不会做类型退化

如何禁用非 const 引用 T& 传递 const 引用:

  • 使用 static_assert 触发一个编译期错误,使用 std::is_const<>
  • 通过使用 std::enable_if<> 禁用该情况下的模板
  • 使用对应的 concept

引用转发

如果模板参数被声明成按值传递的,调用者可以使用定义在头文件 functional 中的 std::ref()std::cref() 将参数按引用传递给函数模板

实际上,标准库做了很简单的事情:在 std::reference_wrapper<> 中定义一个到引用的转换函数,它没有被声明为 explicit,让它可以支持到引用的隐式转换

#include <functional>
#include <string>
#include <iostream>

void printString(const std::string& s) {
    std::cout << s << ’\n;
}

template <typename T>
void printT(T arg) {
    printString(arg); // might convert arg back to std::string
}

int main() {
    std::string s = "hello";
    printT(s);
    printT(std::cref(s));
}

由于引用本身不支持任何形式的隐式转换,所以使用 std::ref 和使用引用没有任何区别

但是有一个问题,编译器可能不知道要将 std::reference_wrapper<> 解释为引用:

template <typename T>
void printT(T arg) {
    std::cout << arg << std::endl;
}

template <typename T>
bool compareT(T arg1, T arg2) {
    return arg1 < arg2;
}

std::string s = "hello";
printV(s);                // OK
printV(std::cref(s));     // ERROR,因为编译器不知道要如何解释对 std::reference_wrapper<> 的 << 运算符

std::string s2 = "hello2";
compareT(s, s2);                         // OK
compareT(std::cref(s), std::cref(s2));   // ERROR

编译期编程

SFINAE、constexpr(C++ 11)、编译期 if(compile-time if,C++ 17)

C++ 11 引入了 constexpr 的新特性,大大简化了各种类型的编译期计算,在 C++ 14 中,constexpr 的各种限制很多被解除了

可以通过部分特例化进行路径选择

编译期 if:通过使用 if constexpr (...) 语法,编译器会使用编译期表达式来决定是使用 if 语句的 then 对应的部分还是 else 对应的部分

此时被跳过的部分不进行实例化,只进行第一阶段编译检查

编译相关

唯一定义法则(one-definition rule, ODR):

  • 常规(比如非模板)非 inline 函数和成员函数,以及非 inline 的全局变量和静态数据成员,在整个程序中只能被定义一次
  • class 类型(包含 structunion),模板(包含部分特化,但不能是全特化),以及 inline 函数和变量,在一个编译单元中只能被定义一次

泛型库

std::invoke():可以实现能够处理所有类型的、可调用对象(包含成员函数)的代码

类型萃取:可以检查类型的属性和功能的类型函数

std::addressoff<>():返回一个对象或者函数的准确地址,即使一个对象重载了运算符 &

std::declval():返回某个类型 T 的右值引用,生成了一个假的对象

使用 decltype(auto) 来完美转发返回值

推迟计算(Defer Evaluation)

在实现模板的过程中,有时候需要面对是否需要考虑不完整类型的问题,如:

template <typename T>
class Cont {
private:
    T* elems;
public:
    ...
};


struct Node {
    std::string value;
    Cont<Node> next;    // 在这里 Node 是不完整类型,此时 Cont 可以接受不完整类型
};

上面的代码是可以编译通过的

但是如果使用了类型萃取的话,可能就不能将其用于不完整类型了,如:

template <typename T>
class Cont {
private:
    T* elems;
public:
    typename std::conditional<std::is_move_constructible<T>::value, T&&, T& >::type foo();
    ...
};

由于 std::is_move_constructible 要求其参数必须是完整类型,所以如果再创建上面的 Node 的话,会编译错误

为了解决这个问题,需要使用一个成员模板代替现有 foo() 的定义:

template <typename T>
class Cont {
private:
    T* elems;
public:
    template <typename D>
    typename std::conditional<std::is_move_constructible<D>::value, T&&, T& >::type foo();
    ...
};

这样的话,编译器会一直等到 foo() 被以完整类型(比如 Node)为参数调用时,才会对类型萃取部分进行计算