模板形参与模板实参
模板形参
每个模板都会由一个或多个模板形参参数化,它们在模板声明语法中的 形参列表 中指定:
template < 形参列表 > 声明
|
|||||||||
形参列表 中的每个形参可以是:
- 非类型模板形参;
- 类型模板形参;
- 模板模板形参。
非类型模板形参
类型 名字(可选) | (1) | ||||||||
类型 名字(可选) = 默认值
|
(2) | ||||||||
类型 ... 名字(可选)
|
(3) | (C++11 起) | |||||||
占位符 名字 | (4) | (C++17 起) | |||||||
auto
的任何类型(例如单纯的 auto、auto ** 或 auto &),被推导类类型的占位符 (C++20 起),或者 decltype(auto)。非类型模板形参必须拥有结构化类型,它是下列类型之一(可以有 cv 限定,忽略限定符):
(C++11 起) |
|
(C++20 起) |
数组与函数类型可以写在模板声明中,但它们会被自动替换为适合的对象指针和函数指针。
在类模板体内使用的非类型模板形参的名字是不可修改的纯右值,除非它的类型是左值引用类型或类类型 (C++20 起)。
形式为 class Foo 的模板形参不是类型为 Foo
的无名非类型模板形参,虽然 class Foo 还能是详述类型说明符且 class Foo x; 声明 x
为 Foo
类型的对象。
如果非类型模板形参的类型包含占位符类型 template<auto n> struct B { /* ... */ }; B<5> b1; // OK:非类型模板形参的类型是 int B<'a'> b2; // OK:非类型模板形参的类型是 char B<2.5> b3; // 错误(C++20 前):非类型模板形参的类型不能是 double 对于类型中使用了占位符类型的非类型模板形参包,每个模板实参的类型会独立进行推导,而且不需要互相匹配: template<auto...> struct C {}; C<'C', 0, 2L, nullptr> x; // OK |
(C++17 起) |
指名类类型 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 < 形参列表 > typename(C++17)|class 名字(可选)
|
(1) | ||||||||
template < 形参列表 > typename(C++17)|class 名字(可选) = default
|
(2) | ||||||||
template < 形参列表 > typename(C++17)|class ... 名字(可选)
|
(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++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) 仅有的例外是引用或指针类型的非类型模板形参以及类类型的非类型模板形参及其子对象之中的引用或指针类型的非静态数据成员 (C++20 起),它们不能指代下列对象或者是下列对象的地址:
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 |
(C++17 起) |
模板类型实参
类型模板形参的模板实参必须是类型标识,它可以指名不完整类型:
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
,A
的每个模板形参必须与 P
的对应模板形参严格匹配 (C++17 前)P
必须至少与 A
一样特殊 (C++17 起)。若 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 的第三(默认)形参是非类型形参
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; // C++17 在 CWG 150 后 OK // 更早时为错误:非严格匹配 X<C> xc; // C++17 在 CWG 150 后 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; // OK template <int> struct SI { /* ... */ }; template <template <auto> class> void FA(); // 注意:C++17 FA<SI>(); // 错误
正式来说,给定以下对两个函数模板的重写,根据函数模板的偏序规则,如果对应于模板模板形参
若重写生成了非法类型,则 |
(C++17 起) |
默认模板实参
默认模板实参在形参列表中于 = 号之后指定。可以为任何种类的模板形参(类型、非类型或模板)指定默认实参,但不能对形参包指定。
若为主类模板、主变量模板 (C++14 起)或别名模版的模板形参指定默认实参,则其每个后继模板形参都必须有默认实参,但最后一个可以是模板形参包。在函数模板中,对跟在默认实参之后的形参没有限制,而仅当类型形参具有默认实参,或可从函数实参推导时,才可跟在形参包之后。
以下情况不允许默认形参
- 在类模板的成员的类外定义中(必须在类体内的声明中提供它们)。注意非模板类的成员模板可以在其类外定义中使用默认形参(见 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<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 起) |
示例
#include <iostream> // 简单的非类型模板形参 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] > s2; // 错误:数组元素无连接 S2<okay> s2; // 能用 int a[5]; int f(int n) { return n; } 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
本节未完成 原因:更多示例 |