Namespaces
Variants

Template arguments

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

为了使模板能够实例化,每个 模板参数 都必须被对应的模板实参所替代。这些实参可以显式提供、通过推导获得或使用默认值。

模板参数列表(参见 模板标识符语法 )中的每个参数属于以下类别之一:

  • 常量模板实参
  • 类型模板实参
  • 模板模板实参

目录

常量模板参数

也称为 非类型模板参数 (参见 下文 )。

可用于常量模板参数的模板实参可以是任何 显式常量求值表达式

(C++11 前)

可用于常量模板参数的模板实参可以是任何 初始化子句 。若该初始化子句为表达式,则必须是 显式常量求值的

(C++11 起)

给定常量模板参数声明的 类型 T ,且为该参数提供的模板实参为 E

虚构声明 T x = E ; 必须满足具有 静态存储期 constexpr 变量 定义的语义约束。

(C++20 起)

T 包含 占位类型 ,或是 待推导类类型的占位符 ,则模板参数的类型是为虚构声明 T x = E ; 中变量 x 推导出的类型。

若推导出的参数类型不是 结构类型 ,则程序非良构。

对于类型使用占位类型的常量模板参数包,每个模板参数的类型均独立推导且无需匹配。

(C++17 起)
template<auto n>
struct B { /* ... */ };
B<5> b1;   // 正确:常量模板参数类型为 int
B<'a'> b2; // 正确:常量模板参数类型为 char
B<2.5> b3; // 错误(C++20 前):常量模板参数类型不能为 double
// C++20 推导的类类型占位符,类模板参数在调用点推导
template<std::array arr>
void f();
f<std::array<double, 8>{}>();
template<auto...>
struct C {};
C<'C', 0, 2L, nullptr> x; // 正确

常量模板参数 P 的值(其类型为 T (可能被推导) (C++17 起) )通过其模板实参 A 按以下方式确定:

(C++11 前)
  • A 是表达式:
  • 否则( A 是花括号环绕的初始化列表),引入临时变量 constexpr T v = A ; P 的值为 v 的值。
(C++11 起)
(C++20 前)
  • T 不是类类型且 A 是表达式:
  • 否则( T 是类类型或 A 是花括号环绕的初始化列表),引入临时变量 constexpr T v = A ;
  • v 生存期 在初始化 v P 后立即结束。
  • P 的初始化满足以下任意条件,则程序非良构:
  • 否则, P 的值为 v 的值。
(C++20 起)
template<int i>
struct C { /* ... */ };
C<{42}> c1; // 正确
template<auto n>
struct B { /* ... */ };
struct J1
{
    J1* self = this;
};
B<J1{}> j1; // 错误:模板参数对象的初始化
            //        不是常量表达式
struct J2
{
    J2 *self = this;
    constexpr J2() {}
    constexpr J2(const J2&) {}
};
B<J2{}> j2; // 错误:模板参数对象与引入的临时对象
            //        不是模板参数等价的

实例化具有常量模板参数的模板时需遵循以下限制:

  • 对于整型和算术类型,实例化时提供的模板实参必须是模板参数类型的 转换后常量表达式 (因此适用某些隐式转换)。
  • 对于对象指针,模板实参必须指定具有静态 存储期 链接 (内部或外部)的完整对象地址,或是计算结果为适当空指针值 std::nullptr_t (C++11 起) 的常量表达式。
  • 对于函数指针,有效实参必须是具有链接的函数指针(或计算结果为空指针值的常量表达式)。
  • 对于左值引用参数,实例化时提供的实参不能是临时对象、无名左值或无链接的具名左值(即实参必须具有链接)。
  • 对于成员指针,实参必须是以 & Class :: Member 形式表示的成员指针,或是计算结果为空指针值 std::nullptr_t (C++11 起) 的常量表达式。

特别地,这意味着字符串字面量、数组元素地址和非静态成员地址不能用作模板实参来实例化对应常量模板参数为对象指针的模板。

(C++17 前)

引用或指针类型的常量模板参数 ,以及类类型常量模板参数及其子对象中引用或指针类型的非静态数据成员 (C++20 起) 不得引用/为以下对象的地址:

  • 临时对象(包括在 引用初始化 期间创建的对象);
  • 字符串字面量
  • typeid 的运算结果;
  • 预定义变量 __func__
  • 或上述对象的子对象(包括非静态类成员、基类子对象或数组元素) (C++20 起)
(C++17 起)
template<const int* pci>
struct X {};
int ai[10];
X<ai> xi; // 正确:数组到指针转换和cv限定符转换
struct Y {};
template<const Y& b>
struct Z {};
Y y;
Z<y> z;   // 正确:无需转换
template<int (&pa)[5]>
struct W {};
int b[5];
W<b> w;   // 正确:无需转换
void f(char);
void f(int);
template<void (* pf)(int)>
struct A {};
A<&f> a;  // 正确:重载决议选择f(int)
template<class T, const char* p>
class X {};
X<int, "Studebaker"> x1; // 错误:字符串字面量不能作为模板参数
template<int* p>
class X {};
int a[10];
struct S
{
    int m;
    static int s;
} s;
X<&a[2]> x3; // 错误(C++20前):数组元素的地址
X<&s.m> x4;  // 错误(C++20前):非静态成员的地址
X<&s.s> x5;  // 正确:静态成员的地址
X<&S::s> x6; // 正确:静态成员的地址
template<const int& CRI>
struct B {};
B<1> b2;     // 错误:模板参数需要临时变量
int c = 1;
B<c> b1;     // 正确

类型模板实参

类型模板参数的模板实参必须是 类型标识 ,该标识可以命名不完整类型:

template<typename T>
class X {}; // 类模板
struct A;            // 不完整类型
typedef struct {} B; // 对无名类型的类型别名
int main()
{
    X<A> x1;  // 正确:'A' 表示一个类型
    X<A*> x2; // 正确:'A*' 表示一个类型
    X<B> x3;  // 正确:'B' 表示一个类型
}

模板模板参数

模板模板参数的模板实参必须是命名类模板或模板别名的 标识表达式

当参数是类模板时,匹配参数时仅考虑主模板。部分特化(如果存在)仅当基于此模板模板参数的特化被实例化时才会被考虑。

template<typename T> // 主模板
class A { int x; };
template<typename T> // 部分特化
class A<T*> { long x; };
// 带有模板模板参数 V 的类模板
template<template<typename> class V>
class C
{
    V<int> y;  // 使用主模板
    V<int*> z; // 使用部分特化
};
C<A> c; // c.y.x 的类型为 int,c.z.x 的类型为 long

要使模板模板实参 A 与模板模板形参 P 匹配, P 必须 至少与 A 一样特化 (详见下文)。 P 的参数列表包含 参数包 ,则 A 的模板参数列表中的零个或多个模板参数(或参数包)将与之匹配。 (C++11 起)

形式上,当模板模板形参 P 满足以下重写条件时,称其 至少与 模板模板实参 A 同样特化:若将以下内容重写为两个函数模板,根据 函数模板 的偏序规则,对应于 P 的函数模板至少与对应于 A 的函数模板同样特化。假设存在一个虚构类模板 X ,其模板形参列表与 A 相同(包含默认实参):

  • 两个函数模板各自拥有与 P A 相同的模板参数。
  • 每个函数模板具有单个函数形参,其类型是 X 的特化形式,该特化的模板实参分别对应函数模板的模板形参。对于函数模板模板形参列表中的每个模板形参 PP ,会生成对应的模板实参 AA PP 声明了形参包,则 AA 为包展开 PP... ;否则 (C++11 起) AA 为标识表达式 PP

如果重写产生无效类型,则 P 不至少与 A 同样特化。

template<typename T>
struct eval;                     // 主模板
template<template<typename, typename...> class TT, typename T1, typename... Rest>
struct eval<TT<T1, Rest...>> {}; // eval 的部分特化
template<typename T1> struct A;
template<typename T1, typename T2> struct B;
template<int N> struct C;
template<typename T1, int N> struct D;
template<typename T1, typename T2, int N = 17> struct E;
eval<A<int>> eA;        // 正确:匹配 eval 的部分特化
eval<B<int, float>> eB; // 正确:匹配 eval 的部分特化
eval<C<17>> eC;         // 错误:C 不匹配部分特化中的 TT
                        // 因为 TT 的第一个参数是类型模板参数,而 17 不是类型名称
eval<D<int, 17>> eD;    // 错误:D 不匹配部分特化中的 TT
                        // 因为 TT 的第二个参数是类型参数包,而 17 不是类型名称
eval<E<int, float>> eE; // 错误:E 不匹配部分特化中的 TT
                        // 因为 E 的第三个(默认)参数是常量

在采纳 P0522R0 之前, A 的每个模板参数都必须与 P 的对应模板参数完全匹配。这导致许多合理的模板参数无法被接受。

尽管这个问题很早就被指出( CWG#150 ),但当它得到解决时,相关修改已被应用到 C++17 工作草案中,该解决方案实际上已成为 C++17 的特性。许多编译器默认禁用它:

  • GCC 在 C++17 之前的所有语言模式中默认禁用此功能,只能通过设置编译器标志启用。
  • Clang 在所有语言模式中默认禁用此功能,只能通过设置编译器标志启用。
  • Microsoft Visual Studio 将其视为常规 C++17 功能,仅在 C++17 及更高语言模式中启用(即在默认的 C++14 语言模式下不支持)。
template<class T> class A { /* ... */ };
template<class T, class U = T> class B { /* ... */ };
template<class... Types> class C { /* ... */ };
template<template<class> class P> class X { /* ... */ };
X<A> xa; // 正确
X<B> xb; // P0522R0 后正确
         // 早期错误:不完全匹配
X<C> xc; // P0522R0 后正确
         // 早期错误:不完全匹配
template<template<class...> class Q> class Y { /* ... */ };
Y<A> ya; // 正确
Y<B> yb; // 正确
Y<C> yc; // 正确
template<auto n> class D { /* ... */ };   // 注意:C++17
template<template<int> class R> class Z { /* ... */ };
Z<D> zd; // P0522R0 后正确:模板参数
         // 比模板实参更特化
template<int> struct SI { /* ... */ };
template<template<auto> class> void FA(); // 注意:C++17
FA<SI>(); // 错误

模板实参等价性

模板实参等价性用于判断两个 模板标识符 是否相同。

两个值在满足以下任一条件时是 模板实参等价 的:

  • 它们是整数或枚举类型,且它们的值相同。
  • 它们是指针类型,且具有相同的指针值。
  • 它们是指向成员的指针类型,且指向同一个类成员或都是空成员指针值。
  • 它们是左值引用类型,且引用同一个对象或函数。
(since C++11)
  • 它们是浮点类型且其值完全相同。
  • 它们是数组类型(此时数组必须是某个类/联合体的成员对象)且它们的对应元素是模板实参等价的。
  • 它们是联合体类型且要么都没有活跃成员,要么具有相同的活跃成员且它们的活跃成员是模板实参等价的。
  • 它们是lambda闭包类型。
  • 它们是非联合体类类型且它们的对应直接子对象和引用成员是模板实参等价的。
(since C++20)

歧义解析

如果模板实参既可以解释为 类型标识 也可以解释为表达式,它总是被解释为类型标识,即使对应的模板参数是常量:

template<class T>
void f(); // #1
template<int I>
void f(); // #2
void g()
{
    f<int()>(); // “int()” 既是类型也是表达式,
                // 调用 #1 因为它被解析为类型
}

注释

在C++26之前,标准措辞中将常量模板实参称为非类型模板实参。该术语已通过 P2841R6 / PR #7587 进行了更改。

功能测试宏 标准 功能特性
__cpp_template_template_args 201611L (C++17)
(DR)
模板模板参数 的匹配
__cpp_nontype_template_args 201411L (C++17) 允许对所有 常量模板参数 进行常量求值
201911L (C++20) 常量模板参数 中的类类型和浮点类型

示例

缺陷报告

以下行为变更缺陷报告被追溯应用于先前发布的C++标准。

缺陷报告 应用于 发布时的行为 正确行为
CWG 150
( P0522R0 )
C++98 模板模板实参必须与模板模板形参的
参数列表完全匹配
也允许更特化的
匹配方式
CWG 354 C++98 空指针值不能作为常量模板实参 允许
CWG 1398 C++11 常量模板实参不能具有 std::nullptr_t 类型 允许
CWG 1570 C++98 常量模板实参可以指向子对象的地址 不允许
P2308R1 C++11
C++20
1. 不允许对常量模板实参使用
列表初始化(C++11)
2. 未明确说明类类型的常量模板
形参如何初始化(C++20)
1. 允许
2. 已明确说明