模板形参与模板实参

来自cppreference.com
< cpp‎ | language

模板形参

每个模板都会由一个或多个模板形参参数化,它们在模板声明语法中的 形参列表 中指定:

template < 形参列表 > 声明

形参列表 中的每个形参可以是:

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

模板非类型形参

类型 名字(可选) (1)
类型 名字(可选) = 默认值 (2)
类型 ... 名字(可选) (3) (C++11 起)
占位符 名字 (4) (C++17 起)
1) 可以有名字的模板非类型形参。
2) 可以有名字和默认值的模板非类型形参。
3) 可以有名字的模板非类型形参包
4) 带占位符类型的模板非类型形参。占位符 可以是包含占位符 auto 的任何类型(例如单纯的 autoauto **auto &被推导类类型的占位符 (C++20 起),或者 decltype(auto)

模板非类型形参必须拥有结构化类型,它是下列类型之一(可以有 cv 限定,忽略限定符):

(C++11 起)
  • 所有基类与非静态数据成员是公开且非 mutable 的,且
  • 所有基类与非静态数据成员的类型都是结构化类型或它的(可能多维的)数组。
(C++20 起)

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

在类模板体内使用的模板非类型形参的名字是不可修改的纯右值,除非它的类型是左值引用类型或类类型 (C++20 起)

形式为 class Foo 的模板形参不是类型为 Foo 的无名模板非类型形参,虽然 class Foo 还能是详述类型说明符class Foo x; 声明 xFoo 类型的对象。

如果模板非类型形参的类型包含占位符类型 auto,被推导类型的占位符 (C++20 起),或 decltype(auto),那么它可以被推导。推导会如同在虚设的声明 T x = 模板实参; 中推导变量 x 的类型一样进行,其中 T 是模板形参的声明类型。如果被推导的类型不能用于模板非类型形参,那么程序非良构。

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

指名类类型 T 的某个模板非类型形参的标识符代表一个 const T 类型的静态存储期对象,该对象被称为 模板形参对象 ,它的值是对应模板实参转换到模板形参的类型之后的值。程序中所有具有相同类型和值的这种模板形参都代表同一模板形参对象。模板形参对象应当拥有常量析构

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 起)
类型形参关键词 - typenameclass 之一。这两个关键词在模板类型形参声明中没有区别
类型约束 - 概念的名字或概念名后随模板实参列表(在角括号中)。概念名均对于两者都可以有限定
1) 没有默认类型的模板类型形参。
template<class T>
class My_vector { /* ... */ };
2) 有默认类型的模板类型形参。
template<class T = void>
struct My_op_functor { /* ... */ };
3) 模板类型形参包
template<typename... Ts>
class My_tuple { /* ... */ };
4) 没有默认实参的受约束模板类型形参。
template <My_concept T>
class My_constrained_vector { /* ... */ };
5) 有默认实参的受约束模板类型形参。
template <My_concept T = void>
class My_constrained_op_functor { /* ... */ };
6) 受约束的模板类型形参包
template <My_concept... Ts>
class My_constrained_tuple { /* ... */ };

形参的名字是可选的:

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

在模板声明体内,类型形参的名字是 typedef 名字,它是当模板被实例化时所提供的类型的别名。

对于每个受约束形参 P,它的 类型约束 Q 指定了概念 C,那么根据以下规则引入一个约束表达式 E

  • 如果 QC(没有实参列表),
  • 如果 P 不是形参包,那么 EC<P>
  • 否则,P 是形参包,那么 E 是折叠表达式 (C<P> && ...)
  • 如果 QC<A1,A2...,AN>,那么 E 分别是 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>
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 起)
1) 可以有名字的模板模板形参。
2) 有默认模板且可以有名字的模板模板形参。
3) 可以有名字的模板模板形参包

在模板声明体内,此形参的名字是一个模板名(且需要实参以实例化)。

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
}

模板非类型实参

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

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

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

(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__
  • 以上之一的 (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 与模板模板形参 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 起)

如果为主类模板、主变量模板 (C++14 起)或别名模版的模板形参指定默认实参,那么它后面的所有模板形参都必须有默认实参,但最后一个可以是模板形参包 (C++11 起)。在函数模板中,对跟在默认实参之后的形参没有限制,而只有在类型形参具有默认实参,或可以从函数实参推导时,才能跟在形参包之后 (C++11 起)

以下情况不允许默认形参:

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

模板实参等价性

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

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

  • 它们拥有整数或枚举类型且它们的值相同
  • 或它们拥有指针类型且它们拥有同一指针值
  • 或它们拥有成员指针类型且它们指代同一类成员或都是空成员指针值
  • 或它们拥有左值引用类型且它们指代同一对象或函数
  • 或它们拥有 std::nullptr_t 类型
(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] > 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 C++98 模板模板实参必须准确匹配模板模板形参的形参列表 模板形参可以比模板实参更特殊
(通过 P0522R0 解决)
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 对于变量模板,有默认实参的模板形参后的模板形参没有任何限制 应用与类模板和别名模板相同的限制