CTTCG 05 Tricky Basics

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 是什么类型,这种初始化方式都是可行的。此外 {} 也能用在数据成员定义处,或者构造函数初始化列表中:

template<typename T>
class MyClass {
private:
    T x;
    T y{};
public:
    MyClass() : x{} {
        // ensures that x is initialized even for built-in types
    }
};

这里 x 是在构造函数中初始化的,y 是在定义处初始化的。如果两处都写了初始化方式,构造函数的初始化会覆盖原来的初始化。个人认为在数据成员定义处给出初始化可以减少遗漏。

3 this 指针失效?

template<typename T>
class Base {
public:
    void bar();
};

template<typename T>
class Derived : Base<T> {
public:
    void foo() {
        // bar(); // calls external bar() or error
        this->bar(); // correct
        // Base<T>::bar(); // also correct
    }
};

上面的例子中,由于 Derived 模板类在定义时不知道 Base<T> 信息,因而不会在其中寻找 bar 方法。即便 Base 有 bar 方法也找不到。如果需要使用基类中的 bar 方法,应该加上 this 指针。

4 为数组提供模板偏特化

数组作为模板的偏特化参数时有下面的可能:

  1. 退化成首元素指针
  2. arrays of known bounds:没错,数组也可以是值,但是值类型的数组是无法作为函数参数的(会退化),因而只能改为引用,或者不经过函数直接使用。
  3. arrays of unknown bounds:用 T[] 语法就可以
  4. reference to arrays of known bounds
  5. reference to arrays of unknown bounds

5 成员模板

可以为类的成员提供模板。比如矩阵模板类的赋值符号,允许从 Matrix<int> 赋值到 Matrix<double> 之类的。但是注意:模板化的构造函数或赋值运算符是不会覆盖默认的构造函数和赋值运算符的。

6 inline 函数模板特化

模板函数的特化相当于普通函数。因而如果被包含在头文件中也需要 inline 关键字,否则有可能会出现重复定义的编译错误。

应该理解 inline 的含义为允许函数多次出现在不同的翻译单元中。inline 并不要求函数内联。

7 变量模板(C++14)

变量也可以是模板!通过指定不同的参数得到的是不同的变量,相同参数则是指的同一变量。下面展示的是一个常量模板(常量是变量的特例):

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

int main() {
    // std::cout << pi<int> << '\n'; // 装不下
    std::cout << pi<double> << '\n';
    std::cout << pi<float> << '\n';
}

注意 int 作为参数会报错,因为花括号初始化不允许类型窄化的转换。

有哪些好处?

  1. 这样能够规避使用者的强制类型转换。
  2. 提供数据成员的 alias,比如 Type Traits Suffix _v 的使用

C++20 的 <numbers> 头文件中就用这样的方式定义了一组数学常量,pipi_v 也包含在其中。不过该头文件中还增加了 inline 限定符。很多变量模板曾经是 constexpr,在 C++17 之后也改成 inline constexpr 了。https://stackoverflow.com/q/75659426/ 是一个相关话题的讨论,目前还没有结果。

2023 年 5 月 11 日: 12 Fundamentals in Depth

不确定变量模板中 inline 是不是必须的,但在非变量模板中 inline 很重要:

An example where inline constexpr makes a difference – Arthur O’Dwyer – Stuff mostly about C++ (quuxplusone.github.io) 巧妙地构造出一个反例,使得”同一“ constexpr 变量的地址不同。原因是 constexpr 是 internal linkage,而 inline constexpr 是 external linkage。

8 模板类模板(模板类作为模板类参数)

class class template 指的是用一个模板类作为模板类参数的行为:

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

注意必须包含头文件 <deque>,否则会报错说 std::deque 并不是模板类,这个报错比较隐晦难懂。如果没有用到 typename 的名字,也可以不写出:

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

在 C++17 之后,作为参数的模板类中的 class 也能写成 typename

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

在 C++17 之前,参数模板类匹配时不会考虑默认参数,因而如果传入的模板类的参数更多(即便是有些参数有默认值),也会出现参数不匹配的问题。上面的代码通过写全所有参数,并为第二个参数提供默认值规避了这个问题。

💡 标准模板库里面应该是没有使用类类模板。可能是有一些比较棘手的地方。