模板形参与模板实参

来自cppreference.com
< cpp‎ | language

每个模板为一或多个指示于模板声明语法中 parameter-list 的模板形参所参数化:

template < parameter-list > declaration (1)

每个 parameter-list 中的形参可以是:

  • 非类型模板形参;
  • 类型模板形参;
  • 模板模板形参。

目录

[编辑] 非类型模板形参

type name(可选) (1)
type name(可选) = default (2)
type ... name(可选) (3) (C++11 起)
placeholder name (4) (C++17 起)
1) 带可选名称的非类型模板形参。
2) 带可选名称和默认值的非类型模板形参。
3) 带可选名称的非类型模板参数包
4) 带占位符类型的非类型模板形参。 placeholder 可为任何包含占位符 auto 的类型(例如单纯的 autoauto **auto &被推导类类型的占位符 (C++20 起)decltype(auto)

type 是下列类型之一(可选地有 cv 限定,忽略限定符):

(C++20 前)
  • 满足下列要求的类型:

类型拥有强结构相等性,若对于 const T 类型泛左值 xx <=> xstd::strong_orderingstd::strong_equality 类型的合法表达式,且该表达式不调用三路比较运算符函数,或调用结构比较运算符。

C结构比较运算符是三路比较运算符函数,在 C 的定义内定义为默认,而且不调用任何非结构比较运算符的三路比较运算符函数。

(C++20 起)

数组与函数类型可写在模板声明中,但它们被自动替换为适合的指向对象指针和指向函数指针。

非类型模板形参的名称用于类模板体内时,它是不可修改的纯右值,除非其类型是左值引用类型。

形式为 class Foo 的模板形参不是 Foo 类型的非类型模板形参,即使 class Foo 还能是详细类型指定符class Foo x; 声明 xFoo 类型对象。

若非类型模板形参的类型包含占位符类型 auto 、被推导类型的占位符 (C++20 起)decltype(auto) ,则可以推导它。如同在虚设声明 T x = template-argument; 中推导变量 x 的类型一般进行推导,其中 T 是模板形参的声明类型。若不容许被推导类型作为非类型模板形参,则程序为病式。

template<auto n> struct B { /* ... */ };
B<5> b1;   // OK :非类型模板形参类型为 int
B<'a'> b2; // OK :非类型模板形参类型为 char
B<2.5> b3; // 错误:非类型模板形参类型不能是 double

对于类型使用占位符类型的非类型模板形参包,对每个模板实参独立地推导类型,而且这些类型不需要匹配:

template<auto...> struct C {};
C<'C', 0, 2L, nullptr> x; // OK
(C++17 起)

指名类类型 T 的非类型模板形参标识符代表 const T 类型的静态存储期对象,称该对象为模板形参对象,其值是转换到模板形参后的对应模板实参的值。程序中所有这种拥有相同类型、相同值的模板形参代表同一模板形参对象。

struct A { friend auto operator<=>(const A&, const A&) = default; };
template<A a> void f() {
    &a; // OK
    const A& ra = a, &rb = a; // 都绑定到同一模板形参对象
    assert(&ra == &rb); // 通过
}
(C++20 起)

[编辑] 类型模板形参

type-parameter-key name(可选) (1)
type-parameter-key name(可选) = default (2)
type-parameter-key ... name(可选) (3) (C++11 起)

type-parameter-keytypenameclass 之一。类型模板形参声明中,此二关键词无区别。

1) 无默认类型的类型模板实参。
template<class T>
class My_vector { /* ... */ };
2) 有默认类型的类型模板实参。
template<class T = void>
struct My_op_functor { /* ... */ };
3) 类型模板形参包
template<typename... Ts>
class My_tuple { /* ... */ };

形参的名称是可选的:

// 上述模板的声明:
template<class> class My_vector;
template<class = void> struct My_op_functor;
template<typename...> class My_tuple;

在模板声明体内,类型形参之名是 typedef 名,即为在实例化模板时提供的类型的别名。

[编辑] 模板模板形参

template < parameter-list > typename(C++17)|class name(可选) (1)
template < parameter-list > typename(C++17)|class name(可选) = default (2)
template < parameter-list > typename(C++17)|class ... name(可选) (3) (C++11 起)
1) 带可选名称的模板模板形参。
2) 带可选名称和默认模板的模板模板形参。
3) 带可选名称的模板模板形参包

不同于类型模板形参声明,模板模板形参只能用关键词 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;
};

有制约模板形参

任何模板形参可以有制约,若使用下列语法:

qualified-concept-name name default(可选) (1)
qualified-concept-name name(可选) = default (2)
qualified-concept-name ... name (3)
1) 概念所制约的模板形参,名称可选
2) 概念所制约的模板形参,带默认,名称可选
3) 概念所制约的模板参数包
qualified-concept-name - 概念名或概念后随模板实参列表(角括号中)之一。无论何种方式,概念名均为可选地有限定

每个有制约模板形参声明的模板形参,其种类(类型、非类型、模板)和类型均匹配 qualified-concept-name 所代表的概念的原型形参。 default 若存在则必须匹配声明的模板形参。

template<typename T> concept C1 = true;
template<template<typename> class X> concept C2 = true;
template<int N> concept C3 = true;
template<typename... Ts> concept C4 = true;
template<char... Cs> concept C5 = true;
 
template<C1 T> void f1();     // OK : T 是类型模板形参
template<C2 X> void f2();     // OK : X 是有一个类型形参的模板
template<C3 N> void f3();     // OK : N 拥有类型 int
template<C4... Ts> void f4(); // OK : Ts 是类型的模板形参包
template<C4 T> void f5();     // OK : T 是类型模板形参
template<C5... Cs> void f6(); // OK : Cs 是 char 的模板形参包
 
template<C1 T = int> struct S1; // OK
template<typename T> struct S0;
template<C2 X = S0> struct S3;  // OK
template<C3 N = 0> struct S2;   // OK
template<C1 T = 0> struct S4;   // 错误:默认实参不是类型

每个 qualified-concept-name 为指代概念 C 的 Q 的有制约形参 P 根据下列规则引入一个制约表达式 E

  • QC (无实参列表),
  • P 不是参数包,则 E 是单纯的 C<P>
  • P 为参数包且 C 为变长,则 EC<P...>
  • P 为参数包且 C 不是变长,则 E 为折叠表达式 (C<P> && ...)
  • QC<A1,A2...,AN> ,则 E 分别为 C<P,A1,A2,...AN>C<P...,A1,A2,...AN>C<P,A1,A2,...AN> && ...
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>
(C++20 起)

[编辑] 模板实参

为令模板实例化,每个模板形参(类型、非类型或模板)必须为对应的模板实参所替换。对于类模板,实参必须显式提供从初始化器推导 (C++17 起)或为默认。对于函数模板,实参要显式提供、从语境推导或为默认。

若实参可被转译成类型 id和表达式,则它始终被转译成类型 id ,即使对应模板形参是非类型:

template<class T> void f(); // #1
template<int I> void f(); // #2
void g() {
    f<int()>(); // "int()" 既是类型又是表达式,
                // 调用 #1 因为它被转译成类型
}

[编辑] 模板非类型实参

实例化拥有非类型实参的模板时,应用下列限制:

  • 对于整数和算术类型,在实例化期间提供的模板实参必须是模板形参类型的转换后常量表达式(故而应用某些隐式转换)。
  • 对于指向对象指针,模板实参必须指代拥有静态存储期链接(内部或外部)的完整对象地址,或求值为适合的空指针的常量表达式,或 std::nullptr_t 值。
  • 对于指向函数指针,合法实参是指向拥有链接的函数的指针(或求值为空指针值的常量表达式)。
  • 对于左值引用形参,于实例化提供的实参不能是临时量、无名左值或无链接的具名左值(换言之,实参必须拥有链接)。
  • 对于指向成员指针,实参必须是表示成 &Class::Member 的指向成员指针,或求值为空指针值的常量表达式,或 std::nullptr_t 值。

特别而是,这隐含着字符串字面量、数组元素的地址和非静态成员的地址,不能用以实例化其对应非类型模板形参是指向对象指针的模板形参的模板。

(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 起)不能指代/是下列对象的地址

  • 子对象(包含非静态类成员、基类子对象或数组元素);
  • 临时对象(包含在引用初始化期间创建者);
  • 字符串字面量
  • typeid 的结果;
  • 或预定义变量 __func__
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;  // 错误:数组元素的地址
X<&s.m> x4;   // 错误:非静态成员的地址
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 起)

[编辑] 模板类型实参

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

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' 指名类型
}

[编辑] 模板模板实参

模板模板形参的模板模板实参是必须是一个 id 表达式,它指名一个类模板或模板别名。

实参是类模板时,匹配形参时只考虑初等模板。部分特化若存在,也仅在基于此模板模板形参的特化恰好要被实例化时得到考虑。

template<typename T> class A { int x; }; // 初等模板
template<class 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 与模板模板形参 PA 的每个模板形参必须准确匹配 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>();  // 错误

形式上,若给定下列对二个函数模板的重写,根据函数模板的部分排序规则,对应 P 的函数模板至少与对应 A 的函数模板同样特化,则模板模板形参 P 至少与模板模板实参 A 同样特化。给定虚设模板 X ,它拥有 A 的模板形参列表(包含默认实参):

  • 二个函数模板各自拥有同 PA 的模板形参。
  • 每个函数模板拥有单个函数形参,其类型是 X 以模板实参的特化,模板实参对应于来自各自函数模板的模板形参,其中对于每个函数模板的模板形参列表中的模板形参 PP ,形成对应的模板实参 AA 。若 PP 声明参数包,则 AA 是包展开 PP... ;否则, AA 是 id 表达式 PP

若重写生成非法类型,则 P 不是至少与 A 同样特化。

(C++17 起)

[编辑] 默认模板实参

默认模板实参指定于形参列表,于 = 号后。默认项可以为任何模板形参种类指定(类型、非类型或模板),但不能对参数包指定。

若为初等类模板、初等变量模板 (C++14 起)或别名模版的模板形参指定默认项,则每个后继模板形参都必须有默认实参,除了最后者可以是模板形参包。函数模板中,仅若它们有默认项,或能从函数参数推导,参数包方可后随多个模板形参。

不允许默认形参

(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 起)

[编辑] 示例

[编辑] 非类型模板形参

#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