Skip to content

Latest commit

 

History

History
1442 lines (1072 loc) · 65.9 KB

ch12.md

File metadata and controls

1442 lines (1072 loc) · 65.9 KB

12. 深入了解基础知识

在本章中,我们深入回顾了本书第一部分中介绍的一些基本内容:模板的声明、模板参数的限制、模板参数的约束等等。

12.1 参数化声明

C++ 目前支持四种基本类型的模板:类模板、函数模板、变量模板和别名模板。这些模板可以出现在命名空间作用域中,也可以在类作用域中。在类作用域中,它们成为嵌套类模板、成员函数模板、静态数据成员模板和成员别名模板。这些模板的声明方式与普通类、函数、变量和类型别名相似,只是通过参数化子句引入,形式为template<parameters here>

注意,C++17 引入了另一种通过此类参数化子句引入的构造:推导指南(参见第 2.9 节和第 15.12.1 节)。在本书中,这些不称为模板(例如,它们不是实例化),但其语法被设计为与函数模板相似。

我们将在后面的部分回到实际的模板参数声明。首先,一些例子将说明四种类型的模板。它们可以在命名空间作用域中(全局或在某个命名空间)出现,如下所示:definitions1.hpp

template<typename T>        // 一个命名空间范围内的类模板
class Data {
public:
    static constexpr bool copyable = true;
    ....
};

template<typename T>        // 一个命名空间范围内的函数模板
void log(T x) {
    ...
}

template<typename T>        // 一个命名空间范围内的变量模板(从C++14开始支持)
T zero = 0;


template<typename T>        // 一个命名空间范围内的变量模板(从C++14开始支持)
bool dataCopyable = Data<T>::copyable;

template<typename T>        // 一个命名空间范围内的别名模板
using DataList = Data<T*>;

注意,在这个例子中,静态数据成员 Data<T>::copyable 不是变量模板,即使它是通过类模板 Data 的参数化间接参数化的。然而,变量模板可以出现在类作用域中(如下一个例子所示),在这种情况下,它是静态数据成员模板。以下例子展示了作为其父类中定义的四种类型的模板类成员:definitions2.hpp

class Collection {
public:
    template<typename T>    // 类内部成员类模板定义
    class Node {
        ...
    };

    template<typename T>    // 类内部(或者成为隐式 inline)成员函数模板定义
    T *alloc() {
        ...
    }

    template<typename T>    // 一个成员变量模板(自C++14开始支持)
    static T zero = 0;

    template<typename T>    // 一个类内部的成员别名模板
    using NodePtr = Node<T>*; 
};

注意,在 C++17 中,变量(包括静态数据成员)和变量模板可以是内联的(inline),这意味着它们的定义可以在多个翻译单元中重复。对于变量模板而言,这是多余的,因为它们总是可以在多个翻译单元中定义。然而,与成员函数不同,静态数据成员在其封闭类中定义并不意味着它是内联的:在所有情况下必须指定关键字 inline

最后,以下代码展示了如何在类外定义不属于别名模板的成员模板:definitions3.hpp

template<typename T>        // 一个命名空间范围内的类模板
class List {
public:
    List() = default;       // 模板构造函数定义

    template<typename U>    // 一个成员类模版,没有定义
    class Handle;

    template<typename U>    // 一个成员函数模板(构造函数)
    List(List<U> const&);

    template<typename U>    // 一个成员变量模板(自C++14开始支持)
    static U zero;
};

template<typename T>        // 在类外面的成员类模板定义
    template<typename U>
class List<T>::Handle {
    ...
};

template<typename T>        // 在类外面的成员函数模板定义
    template<typename T2>
List<T>::List(List<T2> const& b) {
    ...
};

template<typename T>        // 在类外面的静态数据成员模板定义
    template<typename U>
U List<T>::zero = 0;

在其封闭类外定义的成员模板可能需要多个 template<...> 参数化子句:每个外层类模板一个,以及成员模板自身一个。这些子句从最外层的类模板开始列出。

还要注意,构造函数模板(一种特殊的成员函数模板)会禁用默认构造函数的隐式声明(因为只有在没有声明其他构造函数时才会隐式声明)。添加一个默认的声明 List() = default;,确保 List<T> 的实例可以用隐式声明构造函数的语义进行默认构造。

union 模板

union模板也是可能的(并且被视为一种类模板):

template<typename T>
union AllocChunk {
    T object;
    unsigned char bytes[sizeof(T)];
};

默认调用参数

函数模板可以像普通函数声明一样具有默认调用参数:

template<typename T>
void report_top(Stack<T> const&, int number = 10);

template<typename T>
void fill(Array<T>&, T const& = T{}); // T{} 对于内置类型为零

后面的声明显示默认调用参数可以依赖于模板参数。它也可以定义为(在 C++11 之前唯一的方式,参见第 5.2 节):

template<typename T>
void fill(Array<T>&, T const& = T()); // T() 对于内置类型为零

当调用 fill() 函数时,如果提供了第二个函数调用参数,则不会实例化默认参数。这确保了在特定的 T 无法实例化默认调用参数时不会产生错误。例如:

class Value {
public:
    explicit Value(int); // 无默认构造函数
};

void init(Array<Value>& array) {
    Value zero(0);
    fill(array, zero);  // OK: 不使用默认构造函数
    fill(array);        // ERROR: 使用了未定义的默认构造函数
}

类模板的非模板成员

除了在类内部声明的四种基本类型的模板外,您还可以有通过作为类模板一部分进行参数化的普通类成员。这些成员偶尔(错误地)被称为成员模板。尽管它们可以被参数化,但这种定义并不完全是第一类模板。它们的参数完全由它们所属的模板决定。例如:

template<int I>
class CupBoard {
    class Shelf;                // 类模板中的普通类
    void open();                // 类模板中的普通函数
    enum Wood : unsigned char;  // 类模板中的普通枚举类型
    static double totalWeight;  // 类模板中的普通静态数据成员
};

相应的定义只为父类模板指定参数化子句,而不为成员本身指定,因为它不是模板(即,名称后面没有与最后一个 :: 关联的参数化子句(template<>)):

template<int I> // 类模板中普通类的定义
class CupBoard<I>::Shelf {
    ...
};
template<int I> // 类模板中普通函数的定义
void CupBoard<I>::open() {
    ...
}
template<int I> // 类模板中普通枚举类型类的定义
enum CupBoard<I>::Wood {
    Maple, Cherry, Oak
};
template<int I> // 类模板中普通静态成员的定义
double CupBoard<I>::totalWeight = 0.0;

自 C++17 以来,静态成员 totalWeight 可以在类模板内使用 inline 进行初始化:

template<int I>
class CupBoard {
    ...
    inline static double totalWeight = 0.0;
};

尽管这种参数化定义通常被称为模板,但这个术语并不完全适用于它们。一个偶尔被建议用于这些实体的术语是 temploid。自 C++17 以来,C++ 标准确实定义了模板化实体的概念,包括模板和 temploids,以及递归地,包括在模板化实体中定义或创建的任何实体(例如,在类模板中定义的友元函数(参见第 2.4 节)或出现在模板中的 Lambda 表达式的闭包类型)。到目前为止,temploid 和模板化实体都没有得到广泛接受,但它们可能是将来更精确地沟通 C++ 模板的有用术语。

12.1.1 虚成员函数

成员函数模板不能被声明为虚函数。这一限制是因为虚函数调用机制的常规实现使用固定大小的表,每个虚函数一个条目。然而,成员函数模板的实例化数量在整个程序翻译之前并不固定。因此,支持虚成员函数模板需要 C++ 编译器和链接器支持全新的机制。相比之下,类模板的普通成员可以是虚拟的,因为它们的数量在类实例化时已确定

template<typename T>
class Dynamic {
public:
    virtual ~Dynamic(); // OK: 每个 Dynamic<T> 实例一个析构函数
    template<typename T2>
    virtual void copy(T2 const&);
                        // ERROR: copy() 的实例数量不确定
};

12.1.2 模板的链接性

每个模板必须有一个名称,并且该名称在其作用域内必须是唯一的,函数模板可以重载(见第 16 章)。特别注意,与类类型不同,类模板不能与不同类型的实体共享名称:

int C;
...
class C; // OK: 类名和非类名在不同的“空间”中
int X;
...
template<typename T>
class X; // ERROR: 与变量 X 冲突
struct S;
...
template<typename T>
class S; // ERROR: 与结构 S 冲突

模板名称具有链接性,但它们不能具有 C 链接性。非标准链接可能具有实现相关的意义(然而,我们不知道有任何实现支持模板的非标准名称链接):

extern "C++" template<typename T>
void normal();      // 默认:链接规范可以省略
extern "C" template<typename T>
void invalid();     // ERROR: 模板不能具有 C 链接
extern "Java" template<typename T>
void javaLink();    // 非标准,但也许将来某些编译器会支持与 Java 泛型兼容的链接

模板通常具有外部链接。唯一的例外是具有 static 修饰符的命名空间作用域函数模板、直接或间接属于未命名命名空间的模板(它们具有内部链接),以及未命名类的成员模板(没有链接)。例如:

template<typename T>    // 指向在另一文件中同名(和作用域)的实体
void external();

template<typename T>    // 与另一文件中同名模板无关
static void internal();

template<typename T>    // 先前声明的重新声明
static void internal();

namespace {
template<typename>      // 与另一文件中同名模板无关
void otherInternal();
}                       // 在未命名命名空间中

namespace {
template<typename>      // 先前模板声明的重新声明
void otherInternal();
}

struct {
    template<typename T> void f(T) {} // 无链接:无法重新声明
} x;

注意,由于后者的成员模板没有链接,它必须在未命名类内定义,因为无法在类外提供定义。当前,模板不能在函数作用域或局部类作用域中声明,但泛型 Lambda(见第 15.10.6 节)可以出现在局部作用域中,这有效地意味着一种局部成员函数模板。

模板实例的链接性与模板的链接性相同。例如,从上述声明的模板 internal 实例化的函数 internal<void>() 将具有内部链接。这在变量模板的情况下产生了有趣的结果。确实,考虑以下示例:

template<typename T> T zero = T{};

所有 zero 的实例都具有外部链接,即使是像 zero<int const> 这样的实例。这可能有些违反直觉,因为:

int const zero_int = int{};

具有内部链接,因为它被声明为常量类型。类似地,所有模板实例化的:

template<typename T> int const max_volume = 11;

也具有外部链接,尽管所有这些实例化的类型都是 int const

12.1.3 主模板

模板的普通声明声明了主模板。这种模板声明在模板名称后不添加尖括号中的模板参数:

template<typename T> class Box;                 // OK: 主模板
template<typename T> class Box<T>;              // ERROR: 不是特化
template<typename T> void translate(T);         // OK: 主模板
template<typename T> void translate<T>(T);      // ERROR: 函数不允许这样
template<typename T> constexpr T zero = T{};    // OK: 主模板
template<typename T> constexpr T zero<T> = T{}; // ERROR: 不是特化

非主模板发生在声明类或变量模板的部分特化时。关于这一点将在第 16 章中讨论。函数模板必须始终是主模板。

12.2 模板参数(Template Parameters)

模板参数主要有三种基本类型:

  1. 类型参数(这是最常见的)
  2. 非类型参数
  3. 模板模板参数

这些基本类型的模板参数中的任何一种,都可以用作模板参数包的基础(见第 12.2.4 节)。模板参数在模板声明的引入参数化子句中声明[1],这种声明不一定需要命名

template<typename, int>
class X; // X<> 由一个类型和一个整数参数化

当然,如果后续在模板中引用该参数,则需要参数名称。还要注意,模板参数名称可以在后续参数声明中引用(但不能在之前):

template<typename T,            // 第一个参数被使用
      T Root,                   // 在第二个参数声明中
      template<T> class Buf>    // 在第三个参数声明中
class Structure;

12.2.1 类型参数

类型参数通过关键字 typenameclass 引入:这两者完全等价[2]。关键字后必须跟一个简单的标识符,该标识符后面必须跟一个逗号以表示下一个参数声明的开始、一个右括号(>)以表示参数化子句的结束,或一个等号(=)以表示默认模板参数的开始。

在模板声明中,类型参数的作用类似于类型别名(见第 2.8 节)。例如,当 T 是模板参数时,无法使用形式为 class T 的详细名称,即使 T 被替换为类类型:

template<typename Allocator>
class List {
    class Allocator* allocptr;  // ERROR: 应使用 Allocator* allocptr
    friend class Allocator;     // ERROR: 应使用 friend Allocator
    ...
};

12.2.2 非类型参数

非类型模板参数代表在编译或链接时3可以确定的常量值。这种参数的类型(换句话说,它代表的值的类型)必须是以下之一:

  • 整数类型或枚举类型
  • 指针类型[4]
  • 成员指针类型
  • 左值引用类型(对对象和函数的引用均可接受)
  • std::nullptr_t
  • 包含 autodecltype(auto) 的类型(仅自 C++17 起;见第 15.10.1 节)

目前其他类型被排除(尽管将来可能会添加浮点类型;见第 17.2 节)。

令人惊讶的是,非类型模板参数的声明在某些情况下也可以以关键字 typename 开始:

template<typename T, // 类型参数
         typename T::Allocator* Allocator> // 非类型参数
class List;

或者以关键字 class 开始:

template<class X*> // 指针类型的非类型参数
class Y;

这两种情况容易区分,因为第一种后面跟着一个简单标识符,然后是一小组符号(= 表示默认参数,,表示另有模板参数跟随,或一个关闭尖括号 > 结束模板参数列表)。第 5.1 节和第 13.3.2 节解释了第一个非类型参数中使用关键字 typename 的必要性。

函数和数组类型可以指定,但它们隐式调整为其衰减后的指针类型:

template<int buf[5]> class Lexer;       // buf 实际上是 int*
template<int* buf> class Lexer;         // OK: 这是重新声明
template<int fun()> struct FuncWrap;    // fun 实际上是指向
                                        // 函数类型的指针
template<int (*)()> struct FuncWrap;    // OK: 这是重新声明

非类型模板参数的声明类似于变量,但不能具有 staticmutable 等非类型修饰符。它们可以具有 constvolatile 限定符,但如果这样的限定符出现在参数类型的最外层,它将被忽略

template<int const length> class Buffer;    // const 在这里无效
template<int length> class Buffer;          // 与前面的声明相同

最后,非引用的非类型参数在表达式中使用时总是 prvalue。它们不能被取地址且不能被赋值。另一方面,左值引用类型的非类型参数可以用来表示左值:

template<int& Counter>
struct LocalIncrement {
    LocalIncrement() { Counter = Counter + 1; } // OK: 引用整数
    ~LocalIncrement() { Counter = Counter - 1; }
};

不允许使用右值引用。

12.2.3 模板模板参数

模板模板参数是类或别名模板的占位符。它们的声明与类模板类似,但不能使用 structunion 关键字:

template<template<typename X> class C>  // OK
void f(C<int>* p);
template<template<typename X> struct C> // ERROR: struct 在这里无效
void f(C<int>* p);
template<template<typename X> union C>  // ERROR: union 在这里无效
void f(C<int>* p);

C++17 允许使用 typename 替代 class:这一变化是由于模板模板参数不仅可以由类模板替换,还可以由别名模板替换(别名模板实例化为任意类型)。因此,在 C++17 中,我们的示例可以改写为:

template<template<typename X> typename C> // OK 自 C++17 起
void f(C<int>* p);

在其声明的范围内,模板模板参数的用法与其他类或别名模板相同。

模板模板参数的参数可以具有默认模板参数。当在使用模板模板参数时未指定相应参数时,这些默认参数将生效:

template<template<typename T, typename A = MyAllocator> class Container>
class Adaptation {
    Container<int> storage; // 隐式等同于 Container<int, MyAllocator>
    ...
};

这里,TA 是模板模板参数 Container 的模板参数名称。这些名称只能在该模板模板参数的其他参数声明中使用。以下示例演示了这一概念:

template<template<typename T, T*> class Buf> // OK
class Lexer {
    static T* storage;      // ERROR: 这里不能使用模板模板参数
    ...
};

然而,通常模板模板参数的模板参数名称在其他模板参数的声明中并不需要,因此往往完全不命名。例如,我们之前的 Adaptation 模板可以这样声明:

template<template<typename, typename = MyAllocator> class Container>
class Adaptation {
    Container<int> storage; // 隐式等同于 Container<int, MyAllocator>
    ...
};

12.2.4 模板参数包

自 C++11 起,任何类型的模板参数都可以通过在模板参数名称前引入省略号 (...),或在模板参数未命名时放置在模板参数名称应该出现的地方,变为模板参数包:

template<typename... Types> // 声明一个名为 Types 的模板参数包
class Tuple;

模板参数包的行为类似于其底层模板参数,但有一个关键的区别:正常模板参数必须精确匹配一个模板参数,而模板参数包可以匹配任意数量的模板参数。这意味着上面声明的 Tuple 类模板可以接受任意数量(可能不同)的类型作为模板参数:

using IntTuple = Tuple<int>;            // OK: 一个模板参数
using IntCharTuple = Tuple<int, char>;  // OK: 两个模板参数
using IntTriple = Tuple<int, int, int>; // OK: 三个模板参数
using EmptyTuple = Tuple<>;             // OK: 零个模板参数

同样,非类型和模板模板参数的模板参数包可以接受任意数量的非类型或模板模板参数:

template<typename T, unsigned... Dimensions>
class MultiArray;       // OK: 声明一个非类型模板参数包

using TransformMatrix = MultiArray<double, 3, 3>; // OK: 3x3 矩阵

template<typename T, template<typename, typename>... Containers>
void testContainers(); // OK: 声明一个模板模板参数包

MultiArray 示例要求所有非类型模板参数必须为相同类型 unsigned。C++17 引入了推导非类型模板参数的可能性,从某种程度上使我们能够绕过这一限制,参考第 15.10.1 节的详细信息。

主类模板、变量模板和别名模板最多可以有一个模板参数包,且如果存在,模板参数包必须是最后一个模板参数。函数模板有更弱的限制:允许多个模板参数包,只要每个跟随模板参数包的模板参数要么具有默认值(见下一节),要么可以推导(见第 15 章):

template<typename... Types, typename Last>
class LastType;         // ERROR: 模板参数包不是最后一个模板参数

template<typename... TestTypes, typename T>
void runTests(T value); // OK: 模板参数包后跟一个可推导的模板参数

template<unsigned...> struct Tensor;

template<unsigned... Dims1, unsigned... Dims2>
auto compose(Tensor<Dims1...>, Tensor<Dims2...>); // OK: 张量维度可以被推导

最后一个示例是一个具有推导返回类型的函数声明—这是 C++14 的特性。另请参见第 15.10.1 节。

类和变量模板的部分特化的声明(见第 16 章)可以有多个参数包,这与它们的主模板对应物不同。这是因为部分特化通过一种与函数模板几乎相同的推导过程进行选择。

template<typename...> Typelist;
template<typename X, typename Y> struct Zip;

template<typename... Xs, typename... Ys>
struct Zip<Typelist<Xs...>, Typelist<Ys...>>; // OK: 部分特化使用推导确定 Xs 和 Ys 的替代

也许并不奇怪,类型参数包不能在其自身的参数列表中展开。例如:

template<typename... Ts, Ts... vals> struct StaticValues {}; // ERROR: Ts 不能在其自身的参数列表中展开

然而,嵌套模板可以创建类似的有效情况:

template<typename... Ts> struct ArgList {
    template<Ts... vals> struct Vals {};
};
ArgList<int, char, char>::Vals<3, 'x', 'y'> tada;

包含模板参数包的模板称为可变模板,因为它接受可变数量的模板参数。第 4 章和第 12.4 节描述了可变模板的使用。

12.2.5 默认模板参数

任何类型的模板参数,只要不是模板参数包,都可以配备默认参数,尽管必须在类型上与相应的参数匹配(例如,类型参数不能有非类型的默认参数)。默认参数不能依赖于其自身的参数,因为该参数的名称在默认参数之后才在作用域内。然而,它可以依赖于之前的参数

template<typename T, typename Allocator = allocator<T>>
class List;

类模板、变量模板或别名模板的模板参数只有在后续参数也提供了默认参数时,才能具有默认模板参数。(默认函数调用参数也有类似的约束。)后续的默认值通常在同一个模板声明中提供,但也可以在该模板之前的声明中声明。以下示例阐明了这一点:

template<typename T1, typename T2, typename T3,
typename T4 = char, typename T5 = char>
class Quintuple;    // OK

template<typename T1, typename T2, typename T3 = char,
typename T4, typename T5>
class Quintuple;    // OK: T4 和 T5 已经有默认值

template<typename T1 = char, typename T2, typename T3,
typename T4, typename T5>
class Quintuple;    // ERROR: T1 不能有默认参数
                    // 因为 T2 没有默认值

函数模板的模板参数的默认模板参数不要求后续模板参数必须有默认模板参数5

template<typename R = void, typename T>
R* addressof(T& value); // OK: 如果未显式指定,R 将为 void

默认模板参数不能重复:

template<typename T = void>
class Value;
template<typename T = void>
class Value; // ERROR: 重复的默认参数

有许多上下文不允许使用默认模板参数:

  • 部分特化:
template<typename T>
class C;
...
template<typename T = int>
class C<T*>; // ERROR
  • 参数包:
template<typename... Ts = int> struct X; // ERROR
  • 类模板成员的类外定义:
template<typename T> struct X {
    T f();
};
template<typename T = int> T X<T>::f() { // ERROR
    ...
}
  • 友元类模板声明:
struct S {
    template<typename = void> friend struct F;
};
  • 友元函数模板声明,除非它是定义且在翻译单元中没有出现其他声明:
struct S {
    template<typename = void> friend void f(); // ERROR: 不是定义
    template<typename = void> friend void g() { // 到目前为止 OK
    }
};
template<typename> void g(); // ERROR: g() 在定义时给了默认模板参数
// 这里不能存在其他声明

12.3 模板实参(Template Arguments)

在实例化模板时,模板参数由模板实参替代。可以通过几种不同机制确定这些参数:

  • 显式模板实参:模板名称后可以跟随用尖括号括起来的显式模板参数。得到的名称称为模板标识符(template-id)。
  • 注入类名称:在类模板 X 的作用域内,具有模板参数 P1P2 等,该模板的名称 X 可以等同于模板标识符 X<P1, P2, ...>。有关详细信息,请参见第 13.2.3 节。
  • 默认模板参数:如果有默认模板参数,则可以省略模板实例中的显式模板参数。然而,对于类模板或别名模板,即使所有模板参数都有默认值,仍必须提供(可能为空的)尖括号。
  • 参数推导:未显式指定的函数模板参数可以根据函数调用参数的类型进行推导。详细描述见第 15 章。推导也适用于其他几种情况。如果所有模板参数都可以推导,则在函数模板名称后不需要指定尖括号。C++17 还引入了从变量声明的初始化器或函数符号类型转换中推导类模板参数的能力;有关讨论,请参见第 15.12 节。

12.3.1 函数模板实参

函数模板的模板参数可以显式指定,也可以根据模板的使用方式推导,或提供为默认模板参数。例如:

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

int main()
{
    ::max<double>(1.0, -3.0);   // 显式指定模板参数
    ::max(1.0, -3.0);           // 模板参数被隐式推导为 double
    ::max<int>(1.0, 3.0);       // 显式 <int> 禁止了推导;因此结果类型为 int
}

某些模板参数无法被推导,因为其对应的模板参数未出现在函数参数类型中或出于其他原因(见第 15.2 节)。这些参数通常放在模板参数列表的开头,以便可以显式指定,同时允许其他参数推导。例如:

template<typename DstT, typename SrcT>
DstT implicit_cast(Srct const& x)   // SrcT 可以被推导,但是 DstT 不行
{
    return x;
}

int main()
{
    double value = implicit_cast<double>(-1);
}

如果我们在这个例子中反转模板参数的顺序(换句话说,如果我们写 template<typename SrcT, typename DstT>),则隐式转换的调用必须显式指定两个模板参数。

此外,这些参数无法有用地放置在模板参数包之后或出现在部分特化中,因为没有方法可以显式指定或推导它们。

template<typename ... Ts, int N>
void f(double (&)[N+1], Ts ... ps); // 无用的声明,因为 N
// 不能被指定或推导

由于函数模板可以重载,因此显式提供函数模板的所有参数可能不足以唯一标识单个函数:在某些情况下,它会标识一组函数。以下示例说明了这一观察的一个结果:

template<typename Func, typename T>
void apply (Func funcPtr, T x)
{
    funcPtr(x);
}

template<typename T> void single(T);
template<typename T> void multi(T);
template<typename T> void multi(T*);

int main()
{
    apply(&single<int>, 3); // OK
    apply(&multi<int>, 7);  // ERROR: 没有单一的 multi<int>
}

在这个例子中,第一次调用 apply() 可以工作,因为表达式 &single<int> 的类型是明确的。因此,Func 参数的模板参数值很容易推导。在第二次调用中,&multi<int> 可能是两种不同类型中的一种,因此在这种情况下无法推导 Func。

此外,替换函数模板中的模板参数可能会导致尝试构造一个无效的 C++ 类型或表达式。考虑以下重载函数模板(RT1 和 RT2 是未指定的类型):

template<typename T> RT1 test(typename T::X const*);
template<typename T> RT2 test(...);

对于这两个函数模板中的第一个,表达式 test<int> 是没有意义的,因为类型 int 没有成员类型 X。然而,第二个模板没有这样的问题。因此,表达式 &test<int> 确定了单个函数的地址。将 int 替换到第一个模板中失败并不使表达式无效。这一 SFINAE(替换失败不是错误)原则是使函数模板重载变得实用的重要组成部分,详细讨论见第 8.4 节和第 15.7 节。

12.3.2 类型实参

模板类型参数的“值”是为模板类型参数指定的内容。一般来说,任何类型(包括 void、函数类型、引用类型等)都可以用作模板参数,但它们替代模板参数时必须导致有效的构造:

template<typename T>
void clear (T p)
{
    *p = 0; // 需要一元 * 可以适用于 T
}

int main()
{
    int a;
    clear(a); // 错误:int 不支持一元 *
}

12.3.3 非类型实参

非类型模板实参是为非类型参数替代的值。这种值必须是以下之一:

  • 另一个具有正确类型的非类型模板参数。
  • 整数(或枚举)类型的编译时常量值。只有当对应的参数具有与该值匹配的类型或能够在不收窄的情况下隐式转换为该值的类型时,这种情况才是可接受的。例如,char 值可以用于 int 参数,但 500 对于 8 位 char 参数来说是无效的。
  • 以内置一元 &(“地址”)运算符为前缀的外部变量或函数的名称。对于函数和数组变量,可以省略 &。这样的模板参数与指针类型的非类型参数相匹配。C++17 放宽了这一要求,允许产生指向函数或变量的指针的任何常量表达式。
  • 上一种参数,但没有前导 & 运算符,这对于引用类型的非类型参数是有效参数。同样,C++17 放宽了限制,允许任何常量表达式的 glvalue 用于函数或变量。
  • 指向成员的常量;换句话说,形如 &C::m 的表达式,其中 C 是类类型,m 是非静态成员(数据或函数)。这仅与指向成员类型的非类型参数相匹配。同样,在 C++17 中,实际的语法形式不再受限:允许任何常量表达式评估为匹配的指向成员的常量。
  • 空指针常量是指向指针或指向成员类型的非类型参数的有效参数。

对于整型的非类型实参 —— 可能是最常见的非类型参数 —— 考虑隐式转换为参数类型。随着 C++11 中引入 constexpr 转换函数,这意味着转换前的参数可以具有类类型。

在 C++17 之前,当将参数与指针或引用的参数匹配时,不考虑用户定义的转换(单参数构造函数和转换运算符)以及派生到基类的转换,尽管在其他情况下它们会被视为有效的隐式转换。使参数变得更常量和/或更易变的隐式转换是可以的。

以下是一些有效的非类型模板参数示例:

template<typename T, T nontypeParam>
class C;

C<int, 33>* c1;             // 整数类型
int a;
C<int*, &a>* c2;            // 外部变量的地址
void f();
void f(int);
C<void (*)(int), f>* c3;    // 函数的名称:重载解析在这种情况下选择 f(int);& 是隐含的
template<typename T> void templ_func();
C<void(), &templ_func<double>>* c4;     // 函数模板实例化是函数
struct X {
    static bool b;
    int n;
    constexpr operator int() const { return 42; }
};
C<bool&, X::b>* c5;         // 静态类成员是可接受的变量/函数名称
C<int X::*, &X::n>* c6;     // 指向成员常量的示例
C<long, X{}>* c7;           // OK: X 首先通过 constexpr 转换函数转换为 int,然后通过标准整数转换转换为 long

模板参数的一般约束是编译器或链接器必须能够在构建程序时表达其值。在程序运行之前不知道的值(例如局部变量的地址)与模板在程序构建时实例化的概念不兼容。

即便如此,仍然有一些常量值,或许令人惊讶地,当前不被允许:

  • 浮点数
  • 字符串字面量

(在 C++11 之前,空指针常量也不被允许。)

字符串字面量的一个问题是两个相同的字面量可以存储在两个不同的地址。表示在常量字符串上实例化模板的一种替代(但繁琐)方法是引入一个额外的变量来保存字符串:

template<char const* str>
class Message {
    ...
};

extern char const hello[] = "Hello World!";
char const hello11[] = "Hello World!";
void foo()
{
    static char const hello17[] = "Hello World!";
    Message<hello> msg03;   // 在所有版本中都是 OK
    Message<hello11> msg11; // 自 C++11 以来 OK
    Message<hello17> msg17; // 自 C++17 以来 OK
}

要求是,声明为引用或指针的非类型模板参数可以是所有 C++ 版本中的具有外部链接的常量表达式,自 C++11 以来具有内部链接,或自 C++17 以来的任何链接。

有关此领域可能的未来更改的讨论,请参见第 17.2 节。

以下是一些其他(不那么惊讶的)无效示例:

template<typename T, T nontypeParam>
class C;

struct Base {
    int i;
} base;

struct Derived : public Base {
} derived;

C<Base*, &derived>* err1;   // 错误:不考虑派生到基类的转换
C<int&, base.i>* err2;      // 错误:变量的字段不被视为变量
int a[10];
C<int*, &a[0]>* err3;       // 错误:数组元素的地址也不被接受

12.3.4 模板模板实参

模板模板实参通常必须是一个类模板或别名模板,其参数必须与所替换的模板模板参数的参数完全匹配。在 C++17 之前,模板模板参数的默认模板参数会被忽略(但如果模板模板参数有默认参数,在模板实例化时会考虑它们)。C++17 放宽了匹配规则,只要求模板模板参数至少与对应的模板模板参数匹配得更加特化(详见第 16.2.2 节)。

在 C++17 之前,以下示例是无效的:

#include <list>
// 在命名空间 std 中声明:
// template<typename T, typename Allocator = allocator<T>>
// class list;

template<typename T1, typename T2,
template<typename> class Cont> // Cont 期望一个参数
class Rel {
    ...
};

Rel<int, double, std::list> rel; // C++17 之前的错误:std::list 有多个模板参数

在这个例子中,问题在于标准库的 std::list 模板有多个参数。第二个参数(描述分配器)有一个默认值,但在 C++17 之前,这在将 std::list 与容器参数匹配时并未被考虑。

可变模板模板参数是上述 C++17 之前“完全匹配”规则的一个例外,提供了对此限制的解决方案:它们支持对模板模板参数的更一般的匹配。一个模板模板参数包可以匹配模板模板参数中的零个或多个同类模板参数:

#include <list>
template<typename T1, typename T2,
template<typename... > class Cont> // Cont 期望任意数量的类型参数
class Rel {
    ...
};

Rel<int, double, std::list> rel; // OK:std::list 有两个模板参数,但可以使用一个参数

模板参数包只能匹配同类的模板参数。例如,以下类模板可以与任何仅具有模板类型参数的类模板或别名模板实例化,因为传递的模板类型参数包 TT 可以匹配零个或多个模板类型参数:

#include <list>
#include <map>
// 在命名空间 std 中声明:
// template<typename Key, typename T,
// typename Compare = less<Key>,
// typename Allocator = allocator<pair<Key const, T>>>
// class map;

#include <array>
// 在命名空间 std 中声明:
// template<typename T, size_t N>
// class array;

template<template<typename... > class TT>
class AlmostAnyTmpl {
};

AlmostAnyTmpl<std::vector> withVector;  // 两个类型参数
AlmostAnyTmpl<std::map> withMap;        // 四个类型参数
AlmostAnyTmpl<std::array> withArray;    // 错误:模板类型参数包不匹配非类型模板参数

在 C++17 之前,只有关键字 class 可以用来声明模板模板参数,并不意味着只有使用关键字 class 声明的类模板才被允许作为替代参数。实际上,结构体、联合体和别名模板都是有效的模板模板参数(自 C++11 引入别名模板以来)。这类似于观察到任何类型都可以作为声明为关键字 class 的模板类型参数的参数。

12.3.5 等价性

当两个模板参数集的值一一相同时,这两个参数集是等价的。对于类型参数,类型别名并不重要:比较的是最终基础的类型声明。对于整数非类型参数,比较的是参数的值;如何表达这个值并不重要。以下示例说明了这一概念:

template<typename T, int I>
class Mix;
using Int = int;
Mix<int, 3*3>* p1;
Mix<Int, 4+5>* p2; // p2 的类型与 p1 相同

(从这个例子可以看出,建立模板参数列表的等价性并不需要模板定义。)

然而,在模板依赖的上下文中,模板参数的“值”并不总是能够明确确定,因此等价性的规则变得有些复杂。考虑以下示例:

template<int N> struct I {};
template<int M, int N> void f(I<M+N>); // #1
template<int N, int M> void f(I<N+M>); // #2
template<int M, int N> void f(I<N+M>); // #3 错误

仔细研究声明 #1 和 #2,你会发现通过将 M 和 N 分别重命名为 N 和 M,可以得到相同的声明:因此它们是等价的,并声明了相同的函数模板 f。两个声明中的表达式 M+N 和 N+M 被称为等价。

然而,声明 #3 则微妙不同:操作数的顺序被颠倒了。这使得声明 #3 中的表达式 N+M 与其他两个表达式不等价。然而,由于该表达式对于涉及的模板参数的任何值将产生相同的结果,因此这些表达式被称为功能等价。模板以仅因为声明包含功能等价,而实际上不等价表达式的方式进行的声明是错误的。然而,这种错误不一定会被编译器诊断。这是因为一些编译器可能会将 N+1+1 内部表示为与 N+2 完全相同,而其他编译器则可能不会。标准允许两者,并要求程序员在这方面小心。

从函数模板生成的函数与普通函数永远不等价,即使它们可能具有相同的类型和相同的名称。这对类成员有两个重要的影响

  1. 从成员函数模板生成的函数永远不会覆盖虚函数。
  2. 从构造函数模板生成的构造函数永远不会是复制构造函数或移动构造函数6。同样,从赋值模板生成的赋值操作符也永远不会是复制赋值或移动赋值操作符。(然而,这种情况较少出问题,因为隐式调用复制赋值或移动赋值操作符的情况较少。)

这既有好处也有坏处。详细信息请参见第 6.2 节和第 6.4 节。

12.4 可变参数模板

可变参数模板是在第 4.1 节中引入的,包含至少一个模板参数包的模板(详见第 12.2.4 节)[7]。可变参数模板在模板的行为可以推广到任意数量的参数时非常有用。第 12.2.4 节中介绍的 Tuple 类模板就是这样一个类型,因为元组可以有任意数量的元素,且所有元素都以相同方式处理。我们还可以想象一个简单的 print() 函数,它可以接受任意数量的参数并按顺序显示它们。

当为可变参数模板确定模板参数时,模板中的每个参数包将匹配零个或多个模板参数的序列。我们将这一序列称为参数包。以下示例说明了模板参数包 Types 如何根据提供给 Tuple 的模板参数匹配不同的参数包:

template<typename... Types>
class Tuple {
// 提供对 Types 中类型列表的操作
};

int main() {
    Tuple<> t0; // Types 包含一个空列表
    Tuple<int> t1; // Types 包含 int
    Tuple<int, float> t2; // Types 包含 int 和 float
}

由于模板参数包表示一组模板参数而不是单个模板参数,因此必须在一个上下文中使用,其中相同的语言结构适用于参数包中的所有参数。其中一个这样的结构是 sizeof... 操作符,它计算参数包中参数的数量:

template<typename... Types>
class Tuple {
public:
    static constexpr std::size_t length = sizeof...(Types);
};

int a1[Tuple<int>::length]; // 一整型数组
int a3[Tuple<short, int, long>::length]; // 三整型数组

12.4.1 参数扩展

sizeof... 表达式是参数扩展的一个示例。参数扩展是一种构造,它将参数包扩展为独立的参数。虽然 sizeof... 只是执行这种扩展以计数独立参数的数量,但其他形式的参数包 —— 即在 C++ 期望列表的位置出现的那些——可以在该列表中扩展为多个元素。这种参数扩展通过位于列表元素右侧的省略号 (...) 来标识。以下是一个简单示例,我们创建一个新的类模板 MyTuple,继承自 Tuple,并传递其参数:

template<typename... Types>
class MyTuple : public Tuple<Types...> {
// 仅为 MyTuple 提供的额外操作
};

MyTuple<int, float> t2; // 从 Tuple<int, float> 继承

模板参数 Types... 是一个参数扩展,生成一系列模板参数,每个参数对应于参数包中替换的参数。正如示例所示,类型 MyTuple<int, float> 的实例化用参数包 int, float 替换模板类型参数包 Types。当在参数扩展 Types... 中发生这种替换时,我们得到了一个 int 的模板参数和一个 float 的模板参数,因此 MyTuple<int, float>Tuple<int, float> 继承。

理解参数扩展的一种直观方式是将其视为语法扩展,其中模板参数包被替换为恰当数量的(非包)模板参数,并且参数扩展作为独立参数逐一写出,针对每个非包模板参数。例如,MyTuple 如果为两个参数扩展会是这样[8]:

template<typename T1, typename T2>
class MyTuple : public Tuple<T1, T2> {
// 仅为 MyTuple 提供的额外操作
};

对于三个参数:

template<typename T1, typename T2, typename T3>
class MyTuple : public Tuple<T1, T2, T3> {
// 仅为 MyTuple 提供的额外操作
};

然而,请注意,你无法通过名称直接访问参数包的单个元素,因为像 T1T2 等名称在可变参数模板中并未定义。如果你需要这些类型,唯一的方法是将它们(递归地)传递给另一个类或函数。

每个参数扩展都有一个模式,即将为参数包中每个参数重复的类型或表达式,通常出现在表示参数扩展的省略号之前。我们之前的示例中只有简单的模式——参数包的名称——但模式可以复杂得多。例如,我们可以定义一个新的类型 PtrTuple,它继承自指向其参数类型的 Tuple

template<typename... Types>
class PtrTuple : public Tuple<Types*...> {
// 仅为 PtrTuple 提供的额外操作
};

PtrTuple<int, float> t3; // 从 Tuple<int*, float*> 继承

上面示例中参数扩展 Types*... 的模式是 Types*。对该模式的重复替换生成一系列模板类型参数,它们都是指向替换为 Types 的参数包中的类型的指针。在语法解释下,如果 PtrTuple 为三个参数扩展,它将如下所示:

template<typename T1, typename T2, typename T3>
class PtrTuple : public Tuple<T1*, T2*, T3*> {
// 仅为 PtrTuple 提供的额外操作
};

12.4.2 参数扩展可以出现的地方

到目前为止,我们的例子主要集中在使用参数扩展生成模板参数序列上。实际上,参数扩展几乎可以在语言的任何地方使用,只要语法提供了以逗号分隔的列表,包括:

  • 在基类列表中。
  • 在构造函数的基类初始化列表中。
  • 在调用参数列表中(模式是参数表达式)。
  • 在初始化列表中(例如,在花括号初始化列表中)。
  • 在类、函数或别名模板的模板参数列表中。
  • 在函数可以抛出的异常列表中(在 C++11 和 C++14 中被弃用,并在 C++17 中被禁止)。
  • 在属性中,如果该属性本身支持参数扩展(尽管 C++ 标准没有定义这样的属性)。
  • 在声明的对齐指定中。
  • 在 lambda 的捕获列表中。
  • 在函数类型的参数列表中。
  • using 声明中(自 C++17 起;见第 4.4.5 节)。

我们已经提到 sizeof... 作为一种参数扩展机制,实际上并不生成一个列表。C++17 还增加了折叠表达式,这是一种不会生成以逗号分隔的列表的另一种机制(见第 12.4.6 节)。

其中一些参数扩展上下文仅是为了完整性而列出,因此我们将专注于在实践中更有用的参数扩展上下文。由于所有上下文中的参数扩展遵循相同的原则和语法,因此如果你发现需要更冷门的参数扩展上下文,可以根据这里给出的示例进行推导。

在基类列表中的参数扩展扩展为一定数量的直接基类。这种扩展对于通过 mixins 聚合外部提供的数据和功能非常有用,mixins 是指打算“混入”类层次结构中以提供新行为的类。例如,以下 Point 类在多个不同上下文中使用参数扩展,以允许任意的 mixins:

template<typename... Mixins>
class Point : public Mixins... { // 基类参数扩展
    double x, y, z;
public:
    Point() : Mixins()... { } // 基类初始化参数扩展
    template<typename Visitor>
    void visitMixins(Visitor visitor) {
        visitor(static_cast<Mixins&>(*this)...); // 调用参数包扩展
    }
};

struct Color { char red, green, blue; };
struct Label { std::string name; };
Point<Color, Label> p; // 同时继承自 Color 和 Label

Point 类使用参数扩展将每个提供的 mixin 扩展为一个公共基类。Point 的默认构造函数在基类初始化列表中应用参数扩展,以值初始化通过 mixin 机制引入的每个基类。

成员函数模板 visitMixins 是最有趣的,因为它将参数扩展的结果作为调用的参数。通过将 *this 转换为每个 mixin 类型,参数扩展生成的调用参数引用对应于 mixins 的每个基类。实际编写用于 visitMixins 的访问者,可以使用任意数量的函数调用参数,详见第 12.4.3 节。

参数扩展还可以在模板参数列表中使用,以创建非类型或模板参数包:

template<typename... Ts>
struct Values {
    template<Ts... Vs>
    struct Holder {
    };
};

int i;
Values<char, int, int*>::Holder<'a', 17, &i> valueHolder;

请注意,一旦为 Values<...> 指定了类型参数,Values<...>::Holder 的非类型参数列表就具有固定长度;因此,参数包 Vs 不是可变长度参数包

Values 是一个非类型模板参数包,其中每个实际模板参数可以具有不同的类型,这些类型由提供给模板类型参数包 Types 的类型指定。注意,Values 声明中的省略号扮演了双重角色,既声明模板参数为模板参数包,又将该模板参数包的类型声明为参数扩展。虽然这种模板参数包在实践中较为少见,但同样的原则在更普遍的上下文中适用:函数参数。

12.4.3 函数参数包

函数参数包是一个函数参数,可以匹配零个或多个函数调用参数。与模板参数包类似,函数参数包通过在函数参数名称之前(或替代它)使用省略号 (...) 引入,并且每当使用时,函数参数包必须通过参数扩展进行展开。模板参数包和函数参数包统称为参数包

与模板参数包不同,函数参数包始终是参数扩展,因此它们的声明类型必须至少包含一个参数包。在以下示例中,我们引入了一个新的 Point 构造函数,它从提供的构造函数参数中逐个复制初始化每个 mixin:

template<typename... Mixins>
class Point : public Mixins...
{
    double x, y, z;
public:
    // 默认构造函数、访问器函数等省略
    Point(Mixins... mixins) // mixins 是一个函数参数包
    : Mixins(mixins)... { } // 用提供的 mixin 值初始化每个基类
};
struct Color { char red, green, blue; };
struct Label { std::string name; };
Point<Color, Label> p({0x7F, 0, 0x7F}, {"center"});

函数模板的函数参数包可以依赖于在该模板中声明的模板参数包,这使得函数模板能够接受任意数量的调用参数,而不丢失类型信息:

template<typename... Types>
void print(Types... values);
int main
{
    std::string welcome("Welcome to ");
    print(welcome, "C++ ", 2011, '\n'); // 调用 print<std::string, char const*, int, char>
}

当使用一些参数调用函数模板 print() 时,参数的类型将被放入参数包中,以替代模板类型参数包 Types,而实际的参数值将被放入一个参数包中,以替代函数参数包 values。参数确定的过程在第 15 章中详细描述。现在,只需注意,Types 中的第 i 种类型是 Values 中第 i 个值的类型,而这两个参数包在函数模板 print() 的主体中都是可用的。

print() 的实际实现使用递归模板实例化,这是一种在第 8.1 节和第 23 章中描述的模板元编程技术。

在参数列表末尾出现的无名函数参数包与 C 风格的可变参数之间存在语法模糊性。例如:

template<typename T> void c_style(int, T...);
template<typename... T> void pack(int, T...);

在第一个情况下,T... 被视为 T, ...:一个类型为 T 的无名参数后跟一个 C 风格的可变参数。在第二个情况下,T... 构造被视为函数参数包,因为 T 是一个有效的扩展模式。通过在省略号之前添加逗号,可以强制消歧(这确保省略号被视为 C 风格的可变参数),或者通过在省略号后跟一个标识符,使其成为一个命名的函数参数包。注意,在通用 lambda 中,如果紧接着它的类型(没有中间逗号)包含 auto,则尾随的 ... 将被视为表示参数包。

12.4.4 多重和嵌套的参数扩展

参数扩展的模式可以是任意复杂的,并且可以包括多个不同的参数包。在实例化包含多个参数包的参数扩展时,所有参数包必须具有相同的长度。生成的类型或值序列将通过逐个替换每个参数包的第一个参数开始,然后是每个参数包的第二个参数,依此类推。例如,以下函数在将所有参数转发给函数对象 f 之前会复制所有参数:

template<typename F, typename... Types>
void forwardCopy(F f, Types const&... values) {
    f(Types(values)...);
}

调用参数包扩展,命名了两个参数包,Typesvalues。在实例化这个模板时,Types 和 values 参数包的逐元素扩展生成了一系列对象构造,这通过将 values 中的第 i 个值转换为 Types 中的第 i 种类型来构建一个副本。在参数扩展的语法解释下,一个三参数的 forwardCopy 看起来如下:

template<typename F, typename T1, typename T2, typename T3>
void forwardCopy(F f, T1 const& v1, T2 const& v2, T3 const& v3) {
    f(T1(v1), T2(v2), T3(v3));
}

参数扩展本身也可以嵌套。在这种情况下,每个参数包的出现都由其最近的封闭参数扩展“扩展”(仅限于该参数扩展)。以下示例说明了一个涉及三个不同参数包的嵌套参数扩展:

template<typename... OuterTypes>
class Nested {
    template<typename... InnerTypes>
    void f(InnerTypes const&... innerValues) {
        g(OuterTypes(InnerTypes(innerValues)...)...);
    }
};

在对 g() 的调用中,模式为 InnerTypes(innerValues) 的参数扩展是最内层的参数扩展,它同时扩展了 InnerTypesinnerValues,并生成了用于初始化由 OuterTypes 表示的对象的函数调用参数序列。外层参数扩展的模式包括内层参数扩展,为函数 g() 生成一组调用参数,这些参数由内层扩展生成的函数调用参数序列初始化 OuterTypes 中的每个类型。在这种参数扩展的语法解释下,如果 OuterTypes 有两个参数,而 InnerTypesinnerValues 各有三个参数,则嵌套关系变得更加明显:

template<typename O1, typename O2>
class Nested {
    template<typename I1, typename I2, typename I3>
    void f(I1 const& iv1, I2 const& iv2, I3 const& iv3) {
        g(O1(I1(iv1), I2(iv2), I3(iv3)),
          O2(I1(iv1), I2(iv2), I3(iv3)),
          O3(I1(iv1), I2(iv2), I3(iv3)));
    }
};

多重和嵌套的参数扩展是强大的工具(例如,见第 26.2 节)。

12.4.5 零长度参数扩展

参数扩展的语法解释是理解变参模板在不同参数数量下行为的有用工具。然而,在零长度参数包的情况下,语法解释往往会失败。为了说明这一点,考虑第 12.4.2 节中提到的 Point 类模板,用零个参数进行语法替换:

template<>
class Point : {
    Point() : { }
};

上述代码是错误的,因为模板参数列表现在是空的,空的基类和基类初始化列表各自有一个多余的冒号字符。

参数扩展实际上是语义构造,任何大小的参数包替换不会影响参数扩展(或其封闭的变参模板)的解析方式。相反,当参数扩展扩展为一个空列表时,程序在语义上表现得就好像该列表不存在一样。实例化 Point<> 最终没有基类,其默认构造函数没有基类初始化器,但在其他方面是有效的。即使零长度参数扩展的语法解释会产生明确(但不同)代码的情况下,这个语义规则仍然适用。

例如:

template<typename T, typename... Types>
void g(Types... values) {
    T v(values...);
}

变参函数模板 g() 创建一个从给定值序列直接初始化的值 v。如果该值序列为空,则 v 的声明在语法上看起来像是一个函数声明 T v()。然而,由于对参数扩展的替换是语义性的,不能影响解析产生的实体类型,因此 v 被用零个参数初始化,即值初始化9

12.4.6 折叠表达式

编程中的一个常见模式是对值序列进行操作的折叠。例如,一个函数 fn 对序列 x[1], x[2], ..., x[n-1], x[n] 的右折叠表示为:

fn(x[1], fn(x[2], fn(..., fn(x[n-1], x[n])...)))

在探索新的语言特性时,C++ 委员会遇到了需要处理这种构造的情况,特别是对应用于参数扩展的逻辑二元运算符(即 &&||)。如果没有额外特性,我们可能会为 && 运算符写出如下代码:

bool and_all() { return true; }
template<typename T>
bool and_all(T cond) { return cond; }
template<typename T, typename... Ts>
bool and_all(T cond, Ts... conds) {
    return cond && and_all(conds...);
}

在 C++17 中,添加了一种新特性,称为折叠表达式(见第 4.2 节介绍)。它适用于除 .->[] 以外的所有二元运算符。

给定一个未展开的表达式模式包和一个非模式的表达式值,C++17 允许我们为任何这样的运算符 op 编写:

(pack op ... op value) // 右折叠
(value op ... op pack) // 左折叠

注意,这里的括号是必需的。有关一些基本示例,请参见第 4.2 节。

折叠操作适用于从扩展参数包并添加值作为序列的最后一个元素(对于右折叠)或序列的第一个元素(对于左折叠)所产生的序列。

有了这个特性,像下面的代码:

template<typename... T> bool g() {
    return and_all(trait<T>()...);
}

(其中 and_all 如上所定义),可以改写为:

template<typename... T> bool g() {
    return (trait<T>() && ... && true);
}

如你所料,折叠表达式是参数扩展。注意,如果参数包为空,折叠表达式的类型仍然可以从非包操作数(上述形式中的值)中确定。

然而,这个特性的设计者还希望提供一个选择,允许省略值操作数。因此,C++17 中还有两种其他形式:一元右折叠和一元左折叠:

(pack op ... ) // 一元右折叠
(... op pack) // 一元左折叠

同样,括号是必需的。这显然给空扩展带来了问题:我们如何确定它们的类型和值?答案是,一元折叠的空扩展通常是错误的,但有三个例外:

  • 一元折叠 && 的空扩展产生值 true
  • 一元折叠 || 的空扩展产生值 false
  • 一元折叠逗号运算符(,)的空扩展产生一个 void 表达式。

注意,如果你以某种不寻常的方式重载这些特殊运算符中的一个,这可能会导致意外。例如:

struct BooleanSymbol {
    ...
};
BooleanSymbol operator||(BooleanSymbol, BooleanSymbol);
template<typename... BTs> void symbolic(BTs... ps) {
    BooleanSymbol result = (ps || ...);
    ...
}

假设我们用从 BooleanSymbol 派生的类型调用 symbolic。对于所有扩展,结果将产生 BooleanSymbol 值,除了空扩展,它将产生一个 bool 值[10]。因此,我们通常建议谨慎使用一元折叠表达式,并推荐使用二元折叠表达式(带有显式指定的空扩展值)。

12.5 友元

友元声明的基本思想很简单:标识出与声明友元的类有特殊关系的类或函数。然而,有两个因素使事情变得有些复杂:

  1. 友元声明可能是某个实体的唯一声明;
  2. 友元函数声明可以是定义;

12.5.1 类模板的友元类

友元类声明不能作为定义,因此通常不成问题。在模板的上下文中,友元类声明唯一的新特性是可以将类模板的特定实例声明为友元。

template<typename T>
class Node;

template<typename T>
class Tree {
    friend class Node<T>;
    ...
};

注意,类模板必须在将其某个实例声明为友元的地方是可见的。对于普通类,没有这样的要求:

template<typename T>
class Tree {
    friend class Factory; // 即使 Factory 是首次声明也可以
    friend class Node<T>; // 若 Node 不可见则报错
};

在 13.2.2 节对此有更多说明。5.5 节中介绍了一种应用,即声明其他类模板实例为友元:

template<typename T>
class Stack {
public:
    ...
    // 为不同类型 T2 的栈赋值
    template<typename T2>
    Stack<T>& operator=(Stack<T2> const&);
    // 以获取 Stack<T2> 的私有成员的访问权限:
    template<typename> friend class Stack;
    ...
};

C++11 还添加了将模板参数设为友元的语法:

template<typename T>
class Wrap {
    friend T;
    ...
};

这对于任何类型 T 都有效,但如果 T 不是类类型,则会被忽略

12.5.2 类模板的友元函数

通过确保友元函数名后跟尖括号,可以将函数模板的实例设为友元。尖括号可以包含模板参数,但如果可以推导出参数,则可以省略尖括号

template<typename T1, typename T2>
void combine(T1, T2);

class Mixer {
    friend void combine<>(int&, int&);          // OK: T1 = int&, T2 = int&
    friend void combine<int, int>(int, int);    // OK: T1 = int, T2 = int
    friend void combine<char>(char, int);       // OK: T1 = char, T2 = int
    friend void combine<char>(char&, int);      // 错误:与 combine() 模板不匹配
    friend void combine<>(long, long) { ... }   // 错误:不允许定义
};

注意,我们不能定义模板实例(最多只能定义特化),因此以实例名的友元声明不能是定义。

如果名称后不跟尖括号,有两种可能性:

  1. 如果名称未限定(即不含 ::),则永远不会指向模板实例。如果在友元声明处没有可见的匹配非模板函数,则该友元声明是该函数的首次声明。该声明也可以是定义。
  2. 如果名称被限定(包含 ::),则该名称必须指向先前声明的函数或函数模板优先匹配普通函数而非函数模板。然而,这种友元声明不能是定义

一个示例可以帮助澄清各种可能性:

void multiply(void*);   // 普通函数

template<typename T>
void multiply(T);       // 函数模板

class Comrades {
    friend void multiply(int) { }   // 定义了新函数 ::multiply(int)
    friend void ::multiply(void*);  // 引用上述普通函数,而非 multiply<void*> 实例
    friend void ::multiply(int);    // 引用模板的一个实例
    friend void ::multiply<double*>(double*); // 限定名也可以带尖括号,但模板必须可见
    friend void ::error() { }       // 错误:限定友元不能是定义
};

在之前的示例中,我们在普通类中声明了友元函数。同样的规则适用于在类模板中声明它们,但模板参数可能用于标识要成为友元的函数:

template<typename T>
class Node {
    Node<T>* allocate();
    ...
};

template<typename T>
class List {
    friend Node<T>* Node<T>::allocate();
    ...
};

友元函数也可以在类模板中定义,这种情况下它仅在实际使用时才实例化。这通常要求友元函数在其类型中使用类模板本身,这使得更容易表达类模板的函数,并且可以在命名空间范围内调用它们:

template<typename T>
class Creator {
    friend void feed(Creator<T>) { // 每个 T 实例化不同的函数 ::feed()
        ...
    }
};

int main() {
    Creator<void> one;
    feed(one); // 实例化 ::feed(Creator<void>)
    Creator<double> two;
    feed(two); // 实例化 ::feed(Creator<double>)
}

在此示例中,每个 Creator 的实例化会生成一个不同的函数。注意,尽管这些函数是作为模板实例化的一部分生成的,但它们本身是普通函数,而不是模板实例。然而,它们被视为模板实体(参见 12.1 节),并且仅在使用时才会实例化其定义。此外,由于这些函数的主体是在类定义中定义的,因此它们隐式为内联函数。因此,在不同的翻译单元中生成相同函数并不会报错

12.5.3 友元模板

通常,在声明一个作为函数或类模板实例的友元时,我们可以明确表示哪个实体是友元。然而,有时表达一个类的所有模板实例都是友元会更为有用。这需要使用友元模板。例如:

class Manager {
    template<typename T>
    friend class Task;
    
    template<typename T>
    friend void Schedule<T>::dispatch(Task<T>*);
    
    template<typename T>
    friend int ticket() {
        return ++Manager::counter;
    }
    
    static int counter;
};

与普通友元声明一样,友元模板仅当它命名未限定的函数名且未跟随尖括号时,才可以是定义。

友元模板只能声明主模板及其主模板的成员。任何与主模板关联的部分特化和显式特化也会自动视为友元

12.6 附记

自从 C++ 模板在 1980 年代后期问世以来,模板的总体概念和语法相对保持稳定。类模板和函数模板、类型参数和非类型参数都在最初的模板功能中。

然而,最初的设计也有一些重要的扩展,主要是由 C++ 标准库的需求推动的。成员模板可能是这些扩展中最重要的。奇怪的是,只有成员函数模板被正式引入 C++ 标准,而成员类模板是由于编辑失误才成为标准的一部分。

在 C++98 标准化期间,友元模板、默认模板参数和模板模板参数相继被添加。模板模板参数的声明能力有时被称为高阶泛型。它们最初是为支持 C++ 标准库中的某种分配器模型而引入的,但该分配器模型后来被一个不依赖模板模板参数的模型所替代。模板模板参数后来几乎被从语言中移除,因为它们的规范直到 1998 标准制定的最后阶段才完成。最终,多数委员会成员投票保留了它们,并完成了相应的规范。

别名模板作为 2011 标准的一部分被引入。别名模板与人们长期请求的typedef 模板功能类似,它们简化了对现有类模板的重命名。编入标准的规范(N2258)由 Gabriel Dos Reis 和 Bjarne Stroustrup 撰写,Mat Marcus 也为该提案的早期草案做出贡献。Gaby 还详细制定了 C++14 中变量模板提案(N3651)的细节。最初的提案仅支持 constexpr 变量,但在被采纳为草案标准时取消了此限制。

变参模板是由 C++11 标准库和 Boost 库的需求推动的(参见 Boost)。C++ 模板库中越来越多地使用复杂技术来提供支持任意数量模板参数的模板。Doug Gregor、Jaakko Järvi、Gary Powell、Jens Maurer 和 Jason Merrill 提供了标准的初始规范(N2242)。Doug 还在规范制定期间开发了该功能的原始实现(在 GNU 的 GCC 中),这大大促进了该功能在标准库中的应用。

折叠表达式由 Andrew Sutton 和 Richard Smith 开发,通过他们的提案 N4191 添加到 C++17。

[1]: 自 C++14 起,一个例外是对于泛型 Lambda 的隐式模板类型参数;见第 15.10.6 节。

[2]: 关键字 class 并不意味着替代参数应该是类类型。它可以是任何可访问的类型。

[4]: 截至目前,仅允许对象指针和函数指针类型,这排除了如 void* 的类型。然而,所有编译器似乎也接受 void*

[7]: 可变的术语源于 C 的可变参数函数,它们接受可变数量的函数参数。可变参数模板也借用了省略号的使用,以表示零个或多个参数,并且旨在作为某些应用中 C 的可变参数函数的类型安全替代品。

[8]: 这种对参数扩展的语法理解是一个有用的工具,但在模板参数包长度为零时会失效。第 12.4.5 节(第 207 页)提供了关于零长度参数扩展解释的更多细节。

[10]: 由于这三个特殊运算符的重载很少见,因此这个问题很罕见(但微妙)。折叠表达式的最初提议包括了常见运算符(如 +*)的空扩展值,这会导致更严重的问题。