模板形参与模板实参

来自cppreference.com
< cpp‎ | language


 
 
C++ 语言
 
 

每个模板都会由一个或多个模板形参参数化。

模板形参列表 (参考模板声明语法)中的每个形参具有以下类别之一:

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

目录

[编辑] 常量模板形参

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

类型 名字 (可选) (1)
类型 名字 (可选) = 默认值 (2)
类型 ... 名字 (可选) (3) (C++11 起)
类型 - 以下类型之一:
  • 结构化类型(见下文)
(C++17 起)
(C++20 起)
名字 - 常量模板形参的名字
默认值 - 默认模板实参
1) 常量模板形参。
2) 带有默认模板实参的常量模板形参。
3) 常量模板形参包


结构化类型 是下列类型之一(可以有 cv 限定,忽略限定符):

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

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

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

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

指名类类型 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 < 形参列表 > 类型形参关键词 名字 (可选) (1)
template < 形参列表 > 类型形参关键词 名字 (可选) = 默认值 (2)
template < 形参列表 > 类型形参关键词 ... 名字 (可选) (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++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<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++26 前,常量模板形参在标准用词中被称为非类型模板形参。用语是由 P2841R6 / PR#7587 更改的。

模板形参中,对类型和常量形参都可以使用类型约束,这取决于是否有 auto

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_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 184 C++98 未指明模板模板形参中的模板形参是否可以有默认实参 添加相应说明
CWG 1922 C++98 不明确名字是注入类名的类模板是否可以使用之前的声明中的默认模板实参 可以使用
CWG 2032 C++14 对于变量模板,有默认实参的模板形参后的模板形参没有任何限制 应用与类模板和别名模板相同的限制
CWG 2542 C++20 不明确闭包类型是不是结构化类型 不是结构化类型
CWG 2845 C++20 闭包类型不是结构化类型 无捕获时是结构化类型