0%

2023 年 5 月 7 日

两个 type traits

std::decay_t 可以去引用、去限定符、函数/数组变指针;std::common_type_t 用三元运算符获得更“宽泛”类型。

函数模板默认参数

模板默认参数可以放在最前面,不像普通函数只能把带有默认值的参数放在最后。而且函数模板可以明确指定开头几个参数,让后面的参数由推导规则生成。

函数重载参数匹配优先级

优先级排序

  1. perfect match
  2. decay,或者修改指针的内外层 const 属性(volatile 应该也算吧?) 比如 char *char const *
  3. promotion,比 int 小的转到 int 或更大的整数,或者 float 到 double。 举例:bool 到 char 不是 promotion,而是 standard conversion
  4. standard conversion,包括 int to float子类(值/引用/指针)向基类(值/引用/指针)转换
  5. 用户定义的转换,包括标准库的类
  6. 与可变长参数列表匹配,也就是 (...) 例外:f(...) f(void) 参数类型是同级别的,因为没有提供参数,编译器不知道哪一个匹配更合适。

以上参数匹配不涉及到值类型、左引用、右引用的区别。只要引用属性能够匹配,就不区分优先级。比如同时匹配到 f(int)f(int &) 编译器就会抱怨函数调用 ambiguous。

2023 年 5 月 7 日

定义位置

类模板只能在全局或者类声明中定义,不能在块或者函数中定义。

类模板外函数定义的写法

类模板中定义函数,T 类型参数可以省略;在类模板中声明,但在外提供函数定义,就需要在定义处给出完整的类型参数:

template<typename T> void Stack<T /* 不能省略 */>::push (T const& elem) { /* impl */ }

一个例外是析构函数和构造函数的名称处,因为这里不表类型,而是表示函数名。

模板参数也可以不是类型,而是值。

非类型参数推导只对直接调用有效

模板函数的类型推导只对立即调用有效,而对算法模板这类需要提前知道类型信息的场合无效。比如:

std::transform (source.begin(), source.end(), // start and end of source
                dest.begin(),     // start of destination
                addValue<5,int>); // operation

上面代码中尽管 addValue 的参数可能可以推导出来,但由于 std::transform 要求提前知道函数的类型,去掉参数之后代码无法编译。

非类型参数的类型限制

非类型参数不能是浮点数(未来可能可以)。只能是整数、nullptr、指针或左值引用。

可变长表达式可以对参数包逐个处理。与折叠表达式(相当于函数式编程中的 reduce,但如果用上逗号表达式就能表达 foreach)不同,可变长表达式(相当于函数式编程中的 map)不会改变参数数量,允许的形式也比折叠表达式宽松一点。

参数包(Parameter Pack)和可变长模板

模板函数可以指定参数包,用 sizeof…() 运算符可以获得参数包的数量。至少有一个参数包的模板称为可变长模板(variadic template)

0 Pack Expansion:参数传递

参数传递。

Note

2024 年 2 月 20 日:除了作为函数参数之外,还能用作初始化列表(花括号)的参数。

1 递归:逐个处理

#include <iostream>
void print ()
{
}

template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
    std::cout << firstArg << '\n'; // print first argument
    print(args...);                // call print() for remaining arguments
}

1 使用关键字引导编译器了解成员类型

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

上面的的代码中加了 typename 关键字用来表示 SubType 这个成员是一个类型。否则由于缺乏类型信息,编译器会认为该成员是某个数据域,从而构成了乘法表达式。

template<unsigned long N>
void printBitset (std::bitset<N> const& bs) {
    std::cout << bs.template to_string<char, std::char_traits<char>, std::allocator<char>>();
}

上面的 template 关键字在 to_string 前作解释,表示 to_string 是一个函数模板,否则后面的尖括号会被认为是小于运算符,而不是函数模板参数的开始标志。

2 正确初始化模板类的成员

在模板类中为了保证成员被正确初始化,应该使用 {} 初始化的语法,因为无论 T 是什么类型,这种初始化方式都是可行的。此外 {} 也能用在数据成员定义处,或者构造函数初始化列表中:

Perfect forwarding:

template<typename T>
void f (T&& val) {
    g(std::forward<T>(val)); // perfect forward val to g()
}

注意,完美转发只对模板参数有效,对依赖于模板参数的类型也是无效的,比如:typename T::iterator&& 是无效的,仍表示严格的右值引用。

完美转发也不能加 const 属性。添加 const 属性后表示常右值引用(而且这种形式很少被使用)。

尝试用完美转发简化代码

设想一个 Person 类,里面只有一个 std::string 类型的 name 数据。已经实现了复制构造函数和移动构造函数。从数据的构造函数有 std::string &&std::string const & 两个重载版本。使用完美转发可以同时写出匹配这两种参数的函数模板,减少代码量。

传值:T

有 decay。

传左值引用:T &

注意遇到数组时可能需要 decay。

注意 T 类型可能是常量,导致 T & 其实是常引用。这样的参数不能被修改,因而不能作为传出参数。可以使用 concept 限定参数非常量:

传转发引用:T &&

这是 T 唯一可能匹配到引用类型的情况。如果 T 本身就是引用类型,就不能用它创建值类型的同类数据。解决方法有:① 用 type traits 去除引用属性 ② 使用 auto,因为 auto 默认不会匹配到引用。

模板元编程

略。

constexpr 函数

在 C++11 中只能使用一条语句,而在 C++14 之后可以使用的语句变得丰富。constexpr 函数允许编译期优化,但不阻止运行期使用。作为对比,C++20 consteval 只能在编译期使用。

应用:e.g. 常量表达式函数可以计算出一些基本信息,比如判断一个整数是否为素数,以此做为另一个模板类偏特化的依据。

SFINAE

substitution failure is not an error

有一些地方需要注意:寻找函数 candidates 时只会考虑函数模板的函数签名,不会考虑函数体。用 C++14 auto 省略掉返回值类型之后,也不会考虑返回值类型。如果匹配完成之后函数体不能编译,编译器就会报错。

包含模型 - The Inclusion Model

通常模板需要被包含在头文件中工作。

注意:函数模板完全特化之后也需要 inline 才能够在头文件中使用,否则会出现不同的翻译单元有重复定义的情况。

改进一:precompiled headers

可以把 pch 理解为编译器中间工作状态的一个 dump。编译器可以在读取到某些源代码之后保存其状态,包括符号表等。如果两个文件中有公共的前缀代码(比如一个标准库的头文件就包含了数千行公共代码),编译器就可以读取预先保存的状态,从而跳过对于这些代码的处理。

很遗憾的是前缀的判定是必须完全相同,即便改变头文件的包含顺序,其中与宏相关的定义也可能结果不同。由于宏无法从模块中导出,模块将可能解决这一痛点。

改进二:explicit template instantiation

只在头文件中留模板函数的声明,而不是定义。然后在其他的源文件中提供特化的定义。

Class Template v.s. Template Class

前者是生成类的模板,后者既可以指模板,又可以指从模板生成的类。

Substitution, Instantiation, and Specialization

  • 参数替代是模板生成相应对象(即实例化)的一个中间环节。
  • 实例化是从模板生成相应对象的动作。
  • 特化是和模板形式相似但匹配程度更高的对象。特化分为实例化(生成式特化)和显式特化(explicit specialization,即人为提供特化函数)。

Declarations versus Definitions

函数、类或变量等有了名字,但是定义不完整,就算作声明。

对变量初始化,或者缺少 extern 修饰符(specifier)时,声明就变成了定义。

The One-Definition Rule(ODR)

  • Ordinary (i.e., not templates) noninline functions and member functions, as well as (noninline) global variables and static data members should be defined only once across the whole program. 非内联函数和成员函数、(非内联的)全局和(非内联的)静态变量在整个程序中只能定义一次。
  • Class types (including structs and unions), templates (including partial specializations but not full specializations), and inline functions and variables should be defined at most once per translation unit, and all these definitions should be identical. 类和不完整的模板、内联函数和变量每个翻译单元中最多定义一次,整个程序中的多个定义必须完全相等。