模板实参

来自cppreference.com
< cpp‎ | language


 
 
C++ 语言
 
 

为使模板被实例化,它的每个模板形参(类型、常量或模板)都必须被一个对应的模板实参替换。实参可以被显式提供,推导,或为默认。

模板实参列表 (参考模板标识语法)中的每个实参具有以下类别之一:

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

目录

[编辑] 常量模板实参

又称为非类型模板实参(见下文)。

可以用在常量模板形参上的模板实参,可以是任何明显常量求值的表达式

(C++11 前)

可以用在常量模板形参上的模板实参,可以是任何初始化器子句。如果此初始化器子句是表达式,那么它必须是明显常量求值的表达式

(C++11 起)

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

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

(C++20 起)

如果 T 包含占位符类型,或者是被推导类类型的占位符,那么模板形参的类型是对虚设声明 T x = E; 中的变量 x 所推导的类型,其中 E 是该形参所提供的模板实参。

如果被推导的类型不是结构化类型,那么程序非良构。

对于类型中使用了占位符类型的常量模板形参包,每个模板实参的类型会独立进行推导,而且不需要互相匹配。

(C++17 起)
template<auto n>
struct B { /* ... */ };
 
B<5> b1;   // OK:常量模板形参的类型是 int
B<'a'> b2; // OK:常量模板形参的类型是 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; // OK

(可能推导的)(C++17 起)T 类型的常量模板形参 P 的值,按如下方式从它的模板实参 A 确定:

(C++11 前)
  • 如果 A 是表达式:
  • 否则(A 是花括号包围的初始化器列表),引入一个临时变量 constexpr T v = A;P 的值是 v 的值。
  • v生存期在它初始化后立即结束。
(C++11 起)
(C++20 前)
  • 如果 T 不是类类型且 A 是表达式:
  • 否则(T 是类类型,或 A 是花括号包围的初始化器列表),引入一个临时变量 constexpr T v = A;
  • v生存期在它和 P 初始化后立即结束。
  • 如果 P 的初始化满足以下任一条件,则程序非良构:
  • 否则,P 的值是 v 的值。
(C++20 起)
template<int i>
struct C { /* ... */ };
 
C<{42}> c1; // OK
 
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; // OK:数组到指针转换和 cv 限定转换
 
struct Y {};
 
template<const Y& b>
struct Z {};
 
Y y;
Z<y> z;   // OK:没有转换
 
template<int (&pa)[5]>
struct W {};
 
int b[5];
W<b> w;   // OK:没有转换
 
void f(char);
void f(int);
 
template<void (*pf)(int)>
struct A {};
 
A<&f> a;  // OK:重载决议选择 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;  // OK:静态成员的地址
X<&S::s> x6; // OK:静态成员的地址
 
template<const int& CRI>
struct B {};
 
B<1> b2;     // 错误:模板实参要求临时量
int c = 1;
B<c> b1;     // OK

[编辑] 类型模板实参

类型模板形参的模板实参必须是类型标识,它可以指名不完整类型:

template<typename T>
class X {}; // 类模板
 
struct A;            // 不完整类型
typedef struct {} B; // 无名类型的类型别名
 
int main()
{
    X<A> x1;  // OK:'A' 指名类型
    X<A*> x2; // OK:'A*' 指名类型
    X<B> x3;  // OK:'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 与模板模板形参 PP 必须至少和 A 一样特殊。如果 P 的形参列表包含一个形参包,那么来自 A 的模板形参列表中的零或更多模板形参(或形参包)和它匹配。(C++11 起)

正式来说,给定以下对两个函数模板的重写,根据函数模板的偏序规则,如果对应于模板模板形参 P 的函数模板,至少与对应于模板模板实参 A 的函数模板同样特殊,那么 P 至少和 A 一样特殊。给定一个虚设的类模板 X,它拥有 A 的模板形参列表(包含默认实参):

  • 两个函数模板各自分别拥有与 PA 相同的各个模板形参。
  • 每个函数模板均拥有单个函数形参,它的类型是以对应于各自函数模板的模板形参的模板实参对 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;        // OK:匹配 eval 的部分特化
eval<B<int, float>> eB; // OK:匹配 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 的第三个(默认)形参是常量形参

在采纳 P0552R0 前,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; // OK
X<B> xb; // 在 P0552R0 后 OK;之前是错误的:非严格匹配
X<C> xc; // 在 P0552R0 后 OK;之前是错误的:非严格匹配
 
template<template<class...> class Q> class Y { /* ... */ };
Y<A> ya; // OK
Y<B> yb; // OK
Y<C> yc; // OK
 
template<auto n> class D { /* ... */ };   // 注意:C++17
template<template<int> class R> class Z { /* ... */ };
Z<D> zd; // 在 P0552R0 后 OK:模板形参比模板实参更特殊
 
template<int> struct SI { /* ... */ };
template<template<auto> class> void FA(); // 注意:C++17
FA<SI>(); // 错误

[编辑] 模板实参等价性

模板实参等价性用来确定两个模板标识是否相同。

如果两个值拥有相同的类型,且满足以下条件之一,那么它们模板实参等价

  • 它们拥有整数或枚举类型且它们的值相同。
  • 它们拥有指针类型且它们拥有同一指针值。
  • 它们拥有成员指针类型且它们指代同一类成员或都是空成员指针值。
  • 它们拥有左值引用类型且它们指代同一对象或函数。
(C++11 起)
  • 它们拥有浮点数类型且它们的值相同。
  • 它们拥有数组类型(此情况下数组必须是某类/联合体的成员对象)且它们对应的元素模板实参等价。
  • 它们拥有联合体类型且它们均无活跃成员,或它们拥有相同的活跃成员且它们的活跃成员模板实参等价。
  • 它们拥有 lambda 闭包类型。
  • 它们拥有非联合类类型且它们对应的直接子对象和引用成员模板实参等价。
(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. 使之明确