模板形参与模板实参
模板形参
每个模板都会由一个或多个模板形参参数化,它们在模板声明语法中的形参列表 中指定:
template < 形参列表 > 声明
|
(1) | ||||||||
template < 形参列表 > requires 约束 声明
|
(2) | (C++20 起) | |||||||
形参列表 中的每个形参可以是:
- 非类型模板形参;
- 类型模板形参;
- 模板模板形参。
非类型模板形参
| 类型 名字 (可选) | (1) | ||||||||
类型 名字 (可选) = 默认值
|
(2) | ||||||||
类型 ... 名字 (可选)
|
(3) | (C++11 起) | |||||||
| 类型 | - | 以下类型之一:
| ||||
| 名字 | - | 非类型模板形参的名字 | ||||
| 默认值 | - | 默认模板实参 |
结构化类型是下列类型之一(可以有 cv 限定,忽略限定符):
|
(C++11 起) |
|
(C++20 起) |
数组与函数类型可以写在模板声明中,但它们会被自动替换为适合的对象指针和函数指针。
在类模板体内使用的非类型模板形参的名字是不可修改的纯右值,除非它的类型是左值引用类型或类类型(C++20 起)。
形式为 class Foo 的模板形参不是类型为 Foo 的无名非类型模板形参,虽然 class Foo 还能是详述类型说明符且 class Foo x; 声明 x 为 Foo 类型的对象。
|
指名类类型 struct A
{
friend bool operator==(const A&, const A&) = default;
};
template<A a>
void f()
{
&a; // OK
const A& ra = a, &rb = a; // 都绑定到同一个模板形参对象
assert(&ra == &rb); // 通过
}
|
(C++20 起) |
类型模板形参
| 类型形参关键词 名字 (可选) | (1) | ||||||||
类型形参关键词 名字 (可选) = 默认值
|
(2) | ||||||||
类型形参关键词 ... 名字 (可选)
|
(3) | (C++11 起) | |||||||
| 类型约束 名字 (可选) | (4) | (C++20 起) | |||||||
类型约束 名字 (可选) = 默认值
|
(5) | (C++20 起) | |||||||
类型约束 ... 名字 (可选)
|
(6) | (C++20 起) | |||||||
| 类型形参关键词 | - | typename 或 class 之一。这两个关键词在类型模板形参声明中没有区别
|
| 类型约束 | - | 概念的名字或概念名后随模板实参列表(在角括号中)。两种方式种的概念名都可以有限定 |
| 名字 | - | 类型模板形参的名字 |
| 默认值 | - | 默认模板实参 |
template<class T>
class My_vector { /* ... */ };
template<class T = void>
struct My_op_functor { /* ... */ };
template <My_concept T>
class My_constrained_vector { /* ... */ };
template <My_concept T = void>
class My_constrained_op_functor { /* ... */ };
形参的名字是可选的:
// 对上面所示模板的声明:
template<class>
class My_vector;
template<class = void>
struct My_op_functor;
template<typename...>
class My_tuple;
在模板声明体内,类型形参的名字是 typedef 名字,它是当模板被实例化时所提供的类型的别名。
|
对于每个受约束形参
template<typename T>
concept C1 = true;
template<typename... Ts> // 变参概念
concept C2 = true;
template<typename T, typename U>
concept C3 = true;
template<C1 T> struct s1; // 约束表达式是 C1<T>
template<C1... T> struct s2; // 约束表达式是 (C1<T> && ...)
template<C2... T> struct s3; // 约束表达式是 (C2<T> && ...)
template<C3<int> T> struct s4; // 约束表达式是 C3<T, int>
template<C3<int>... T> struct s5; // 约束表达式是 (C3<T, int> && ...)
|
(C++20 起) |
模板模板形参
template < 形参列表 > 类型形参关键词 名字 (可选)
|
(1) | ||||||||
template < 形参列表 > 类型形参关键词 名字 (可选) = 默认值
|
(2) | ||||||||
template < 形参列表 > 类型形参关键词 ... 名字 (可选)
|
(3) | (C++11 起) | |||||||
| 类型形参关键词 | - | class 或 typename 之一(C++17 起)
|
在模板声明体内,此形参的名字是一个模板名(且需要实参以实例化)。
template<typename T>
class my_array {};
// 两个类型模板形参和一个模板模板形参:
template<typename K, typename V, template<typename> typename C = my_array>
class Map
{
C<K> key;
C<V> value;
};
模板形参的名字决议
模板形参的名字不能在它的作用域(包括内嵌作用域)内重声明。模板形参的名字不能与模板的名字相同。
template<class T, int N>
class Y
{
int T; // 错误:重声明模板形参
void f()
{
char T; // 错误:重声明模板形参
}
};
template<class X>
class X; // 错误:重声明模板形参
在某个类模板定义外的出现的类模板成员定义中,类模板成员名会隐藏任何外围类模板的模板形参名,但如果该成员是类或函数模板就不会隐藏该成员的模板形参。
template<class T>
struct A
{
struct B {};
typedef void C;
void f();
template<class U>
void g(U);
};
template<class B>
void A<B>::f()
{
B b; // A 的 B,不是模板形参
}
template<class B>
template<class C>
void A<B>::g(C)
{
B b; // A 的 B,不是模板形参
C c; // 模板形参 C,不是 A 的 C
}
在包含某个类模板的定义的命名空间外出现的该类模板的成员定义中,模板形参名隐藏此命名空间的成员名。
namespace N
{
class C {};
template<class T>
class B
{
void f(T);
};
}
template<class C>
void N::B<C>::f(C)
{
C b; // C 是模板形参,不是 N::C
}
在类模板定义中,或类模板某个成员的位于模板定义外的定义中,对于每个非待决基类,如果基类名或基类成员名与模板形参名相同,那么该基类名或成员名隐藏模板形参名。
struct A
{
struct B {};
int C;
int Y;
};
template<class B, class C>
struct X : A
{
B b; // A 的 B
C b; // 错误:A 的 C 不是类型名
};
模板实参
为使模板被实例化,它的每个模板形参(类型、非类型或模板)都必须被一个对应的模板实参替换。对于类模板,实参可以被显式提供,或从初始化式推导,(C++17 起)或为默认。对于函数模板,实参可以被显式提供,或从语境推导,或为默认。
如果实参可以同时被解释为类型标识和表达式,那么它始终会被解释为类型标识,即使它对应的是非类型模板形参:
template<class T>
void f(); // #1
template<int I>
void f(); // #2
void g()
{
f<int()>(); // "int()" 既是类型又是表达式,
// 因为它被解释成类型,所以调用 #1
}
非类型模板实参
|
可以用在非类型模板形参上的模板实参,可以是任何明显常量求值的表达式。 |
(C++11 前) |
|
可以用在非类型模板形参上的模板实参,可以是任何初始化式子句。如果此初始化式子句是表达式,则它必须是明显常量求值的表达式。 |
(C++11 起) |
给定非类型模板形参声明的类型 为 T,提供给该形参的模板实参为 E。
|
虚设声明 |
(C++26 起) |
|
如果 如果被推导的类型不是结构化类型,那么程序非良构。 对于类型中使用了占位符类型的非类型模板形参包,每个模板实参的类型会独立进行推导,而且不需要互相匹配。 |
(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 前) |
|
(C++11 起) (C++20 前) |
|
(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; // 错误:模板形参对象与引入的临时量并不模板实参等价
|
在实例化拥有非类型模板形参的模板时应用下列限制:
特别是,这意味着字符串字面量、数组元素的地址和非静态成员的地址,不能被用作模板实参,来实例化它对应的非类型模板形参是对象指针的模板形参的模板。 |
(C++17 前) |
|
引用或指针类型的非类型模板形参以及类类型的非类型模板形参和它的子对象之中的引用或指针类型的非静态数据成员(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 与模板模板形参 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; // 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 起)。
如果为主类模板、主变量模板(C++14 起)或别名模版的模板形参指定默认实参,那么它后面的所有模板形参都必须有默认实参,但最后一个可以是模板形参包(C++11 起)。在函数模板中,对跟在默认实参之后的形参没有限制,而只有在类型形参具有默认实参,或可以从函数实参推导时,才能跟在形参包之后(C++11 起)。
以下情况不允许默认形参:
- 在类模板的成员的类外定义中(必须在类体内的声明中提供它们)。注意非模板类的成员模板可以在它的类外定义中使用默认形参(见 GCC 漏洞 53856)
- 在友元类模板声明中
|
(C++11 前) |
|
在友元函数模板的声明上,仅当声明是定义,且此翻译单元不出现此函数的其他声明时,才允许默认模板实参。 |
(C++11 起) |
各个声明中所出现的默认模板实参,以类似默认函数实参的方式合并:
template<typename T1, typename T2 = int> class A;
template<typename T1 = int, typename T2> class A;
// 如上与如下相同:
template<typename T1 = int, typename T2 = int> class A;
但在同一作用域中不能两次为同一形参指定默认实参:
template<typename T = int> class X;
template<typename T = int> class X {}; // 错误
当解析非类型模板形参的默认模板实参时,第一个非嵌套的 > 被当做模板形参列表的结尾而非大于运算符:
template<int i = 3 > 4> // 语法错误
class X { /* ... */ };
template<int i = (3 > 4)> // OK
class Y { /* ... */ };
模板模板形参的模板形参列表可拥有它自己的默认实参,它只会在模板模板实参自身处于作用域中时有效:
// 类模板,带有默认实参的类型模板形参
template<typename T = float>
struct B {};
// 模板模板形参 T 有形参列表,
// 它由一个带默认实参的类型模板形参组成
template<template<typename = float> typename T>
struct A
{
void f();
void g();
};
// 类体外的成员函数模板定义
template<template<typename TT> class T>
void A<T>::f()
{
T<> t; // 错误:TT 在作用域中没有默认实参
}
template<template<typename TT = char> class T>
void A<T>::g()
{
T<> t; // OK:t 是 T<char>
}
默认模板形参中所用的名字的成员访问,在声明中,而非在使用点检查:
class B {};
template<typename T>
class C
{
protected:
typedef T TT;
};
template<typename U, typename V = typename U::TT>
class D: public U {};
D<C<B>>* d; // 错误:C::TT 是受保护的|
默认模板实参在需要该默认实参的值时被隐式实例化,除非模板用于指名函数: template<typename T, typename U = int>
struct S {};
S<bool>* p; // 默认模板实参 U 在此点实例化
// p 的类型是 S<bool, int>* |
(C++14 起) |
模板实参等价性
模板实参等价性用来确定两个模板标识是否相同。
如果两个值拥有相同的类型,且满足以下条件之一,那么它们模板实参等价:
- 它们拥有整数或枚举类型且它们的值相同。
- 它们拥有指针类型且它们拥有同一指针值。
- 它们拥有成员指针类型且它们指代同一类成员或都是空成员指针值。
- 它们拥有左值引用类型且它们指代同一对象或函数。
|
(C++11 起) |
|
(C++20 起) |
注解
|
模板形参中,对类型和非类型形参都可以使用类型约束,这取决于是否有 template<typename>
concept C = true;
template<C, // 类型形参
C auto // 非类型形参
>
struct S{};
S<int, 0> s;
|
(C++20 起) |
| 功能特性测试宏 | 值 | 标准 | 功能特性 |
|---|---|---|---|
__cpp_nontype_template_parameter_auto |
201606L |
(C++17) | 声明带有 auto 的非类型模板形参
|
__cpp_template_template_args |
201611L |
(c++17) (DR) |
模板模板实参的匹配 |
__cpp_nontype_template_args |
201411L |
(C++17) | 允许所有非类型模板实参的常量求值 |
201911L |
(C++20) | 非类型模板形参中的类类型和浮点数类型 |
示例
#include <array>
#include <iostream>
#include <numeric>
// 简单的非类型模板形参
template<int N>
struct S { int a[N]; };
template<const char*>
struct S2 {};
// 复杂的非类型形参的例子
template
<
char c, // 整型类型
int (&ra)[5], // 到(数组类型)对象的左值引用
int (*pf)(int), // 函数指针
int (S<10>::*a)[10] // 指向(int[10] 类型的)成员对象的指针
>
struct Complicated
{
// 调用编译时所选择的函数
// 并在编译时将它的结果存储在数组中
void foo(char base)
{
ra[4] = pf(c - base);
}
};
// S2<"fail"> s2; // 错误:不能用字符串字面量
char okay[] = "okay"; // 有连接的静态对象
// S2<&okay[0] > s3; // 错误:数组元素无连接
S2<okay> s4; // 能用
int a[5];
int f(int n) { return n; }
// C++20:非类型模板形参可以具有字面量类类型
template<std::array arr>
constexpr
auto sum() { return std::accumulate(arr.cbegin(), arr.cend(), 0); }
// C++20:可以在调用处推导出类模板实参
static_assert(sum<std::array<double, 8>{3, 1, 4, 1, 5, 9, 2, 6}>() == 31.0);
// C++20:非类型模板形参的实参推导和类模板实参推导
static_assert(sum<std::array{2, 7, 1, 8, 2, 8}>() == 28);
int main()
{
S<10> s; // s.a 是含有 10 个 int 的数组
s.a[9] = 4;
Complicated<'2', a, f, &S<10>::a> c;
c.foo('0');
std::cout << s.a[9] << a[4] << '\n';
}输出:
42| 本节未完成 原因:更多示例 |
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
| 缺陷报告 | 应用于 | 出版时的行为 | 正确行为 |
|---|---|---|---|
| CWG 150 (P0522R0) |
C++98 | 模板模板实参必须准确匹配模板模板形参的形参列表 | 模板形参可以比模板实参更特殊 |
| CWG 184 | C++98 | 未指明模板模板形参中的模板形参是否可以有默认实参 | 添加相应说明 |
| CWG 354 | C++98 | 非类型模板实参不能是空指针值 | 可以是空指针值 |
| CWG 1398 | C++11 | 非类型模板实参不能具有 std::nullptr_t 类型
|
可以具有该类型 |
| CWG 1570 | C++98 | 非类型模板实参可以表示子对象的地址 | 只能表示完整对象的地址 |
| CWG 1922 | C++98 | 不明确名字是注入类名的类模板是否可以使用之前的声明中的默认模板实参 | 可以使用 |
| CWG 2032 | C++14 | 对于变量模板,有默认实参的模板形参后的模板形参没有任何限制 | 应用与类模板和别名模板相同的限制 |
| CWG 2542 | C++20 | 不明确闭包类型是不是结构化类型 | 不是结构化类型 |
| CWG 2845 | C++20 | 闭包类型不是结构化类型 | 无捕获时是结构化类型 |
| P2308R1 | C++11 C++20 |
1. 非类型模板实参不允许列表初始化(C++11) 2. 不明确类类型的非类型模板形参如何初始化(C++20) |
1. 允许 2. 使之明确 |