C++ 模板03--基本技巧

C++ template 中关于实践的一些技巧和基础机制

typename 关键字

当一个类型名依赖于一个模板参数时,该类型名之前必须使用 typename:

1
2
3
4
5
6
7
8
9
10
template<typename T>
class C {
void fun() {
// without the typename keywork,
// the compiler might think
// some_type is a member of T
// instead of a type.
typename T::some_type* ptr;
}
}

zero-initialization

对于 C++ 中的基本类型如 int, double, 和 pointer 等类型,它们没有默认构造函数因此不会自动将自己初始化为默认值。在非全局作用域下,未显式初始化的局部变量具有未定义的值:

1
2
3
4
void func () {
int x; // x has undefined value
int* ptr; // ptr might point to anywhere instead of nowhere
}

因此在模板中,如果需要对模板参数提供的类型构造一个对象,必须显式指定为其默认初始化( 大括号初始化 ):

1
2
3
4
5
template<typename T>
void func () {
T t1; // not good, t1 has undefined value
T t2{}; // good, t2 is default initialized
}

大括号初始化意味着:

  1. 如果对象类型定义有默认构造函数,则调用该默认构造函数,如果没有默认构造函数,则会尝试使用初始化列表构造函数
  2. 对于内置类型,执行零初始化( zero initialization )。即: 对象的值被初始化为 0 (数值类型,指针) 或 false (bool 类型)

使用 this->

对于继承体系中的类模板,如果基类也是类模板且基类模板依赖于派生类模板的模板参数,那么对于在基类中定义的符号,应该总是使用 this->Base<T>:: 标识:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename T>
class Base {
public:
void fun();
};

template <typename T>
class Derived : Base<T> { // Base relies on T of Derived
public:
void foo(){
fun(); // Error, Base<T>::fun() is never considered in symbol resolving process
this->func(); // Ok
Base<T>::func(); // Ok
}
}

原始数组或字符串字面量作参数的模板

当原始数组或字符串字面量作参数时,如果模板的参数类型被声明为引用类型,那么原始数组或字符串字面量不会在传参过程中发生类型退化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<typename T, size_t N, size_t M>
bool less(T(&a)[N], T(&b)[M]) {
for(size_t i = 0; i < N && i < M; ++i){
if(a[i] < b[i])return true;
if(a[i] > b[i])return false;
}
return N < M;
}

int x[] = {1, 2, 3};
int y[] = {1, 2, 3, 4, 5};

// both type and length are reserved when passing arguments
less(x, y);

数组也可以以不完整类型的形式出现,即一个数组的声明符号也可以用作模板参数。由于数组声明符号可以不提供长度信息,因此一个支持所有数组类型的模板必须为不完整数组(无边界数组)提供特化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
extern int x[]; // an incomplete type of int []

// undefined template
template <typename T>
class TemplateForArray;

// template 1: for array type
template <typename T, std::size_t N>
class TemplateForArray<T[N]> {
public:
    static void func() { std::cout << "for array T[" << N << "]"; }
};

// template 2: for reference to array type
template <typename T, std::size_t N>
class TemplateForArray<T (&)[N]> {
public:
    static void func() { std::cout << "for array T(&)[" << N << "]"; }
};

// template 3: for incomplete array
template <typename T>
class TemplateForArray<T[]> {
public:
static void func() { std::cout << "for incomplete array T[]"; }
};

int main() {
int x[] = {1, 2, 3}; // x is of type int[3]
int (&y)[3] = x; // y is of type int(&)[3]
extern int z[]; // z in of type int[], which is incomplete
TemplateForArray<decltype(x)>::func(); // ok
TemplateForArray<decltype(y)>::func(); // ok
TemplateForArray<decltype(z)>::func(); // error: incomplete type 'TemplateForArray<int []>' used in nested name specifier if without template 3
}

成员模板

  1. 类/类模板的成员也可以被定义成模板

    对于类模板,只有被用到的成员函数才会被实例化

  2. 类/类模板的成员模板也可以被特化(全特化, 偏特化)
  3. 类/类模板的特殊成员函数也可以被定义成模板,但是需要注意:
    1. 构造函数模板或赋值运算符模板不会替换预定义的构造函数或赋值运算符
    2. 成员模板不算做复制或移动对象的特殊成员函数
  4. .template 关键字
    1. 当调用成员模板的对象其自身依赖于模板参数时,编译器无法将 < 解析成模板参数列表的开始,因此需要显式地使用 .template 表示模板构造。例如:

      1
      2
      3
      4
      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>>();
      }

      上例中由于 bs 的类型依赖于模板参数 N, 因此必须用 .template 才可以调用成员模板。

    2. 类似的操作还有 ->template::template, 应该仅在模板中使用

  5. 泛型 lambda 表达式是成员模板的 wrapper
    C++ 14 中引入的泛型 lambda 表达式例如:
    1
    2
    3
    [] (auto a, auto b) {
    return a + b;
    };
    实际上相当于:
    1
    2
    3
    4
    5
    6
    7
    8
    class [InnerCompilerID] { // compiler given name
    public:
    [InnerCompilerID]();
    template <typename T1, typename T2>
    auto operator()(T1 a, T2 b) const {
    return a + b;
    }
    };