函数模板

来自cppreference.com
< cpp‎ | language

函数模板定义一族函数。

语法

template < 形参列表 > 函数声明 (1)
template < 形参列表 > requires 约束 函数声明 (2) (C++20 起)
带占位符函数声明 (3) (C++20 起)
export template < 形参列表 > 函数声明 (4) (C++11 前)

解释

形参列表 - 非空的模板形参的逗号分隔列表,每项是非类型形参类型形参模板形参或这些的形参包之一。与任何模板一样,形参可以受约束 (C++20 起)
函数声明 - 函数声明。所声明的函数名成为模板名。
约束 - 约束表达式,它限制此函数模板接受的模板形参。
带占位符函数声明 - 函数声明,其中至少一个形参的类型使用了占位符 auto概念 auto:每个占位符都会对应模板形参列表中的一个虚设形参。(见下文“简写函数模板”)

export 是一个可选的修饰符,它声明模板被导出(用于类模板时,它声明所有成员被导出)。对被导出模板进行实例化的文件不需要包含它的定义:声明就足够了。export 的实现稀少而且在细节上相互不一致。

(C++11 前)

简写函数模板

当函数声明或函数模板声明的形参列表中出现占位符类型(auto概念 auto)时,该声明会声明一个函数模板,并且为每个占位符向模板形参列表追加一个虚设的模板形参:

void f1(auto); // 与 template<class T> void f(T) 相同
void f2(C1 auto); // 如果 C1 是概念,那么与 template<C1 T> void f2(T) 相同
void f3(C2 auto...); // 如果 C2 是概念,那么与 template<C2... Ts> void f3(Ts...) 相同
void f4(const C3 auto*, C4 auto&); // 与 template<C3 T, C4 U> void f4(const T*, U&); 相同
template <class T, C U>
void g(T x, U y, C auto z); // 与 template<class T, C U, C W> void g(T x, U y, W z); 相同

简写函数模板可以和所有函数模板一样进行特化。

template<>
void f4<int>(const int*, const double&); // f4<int, const double> 的特化


(C++20 起)

函数模板实例化

函数模板自身并不是类型、函数或任何其他实体。不会从只包含模板定义的源文件生成任何代码。模板只有实例化才会有代码出现:必须确定各模板实参,使得编译器能生成实际的函数(或从类模板生成类)。

显式实例化

template 返回类型 名字 < 实参列表 > ( 形参列表 ) ; (1)
template 返回类型 名字 ( 形参列表 ) ; (2)
extern template 返回类型 名字 < 实参列表 > ( 形参列表 ) ; (3) (C++11 起)
extern template 返回类型 名字 ( 形参列表 ) ; (4) (C++11 起)
1) 显式实例化定义(显式指定所有无默认值模板形参时不会推导模板实参
2) 显式实例化定义,对所有形参进行模板实参推导
3) 显式实例化声明(显式指定所有无默认值模板形参时不会推导模板实参)
4) 显式实例化声明,对所有形参进行模板实参推导

显式实例化定义强制实例化它所指代的函数或成员函数。它可以出现在程序中模板定义后的任何位置,而对于给定的实参列表,它在整个程序中只能出现一次,不要求诊断。

显式实例化声明(extern 模板)阻止隐式实例化:本来会导致隐式实例化的代码必须改为使用已在程序的别处所提供的显式实例化。

(C++11 起)

在函数模板特化或成员函数模板特化的显式实例化中,尾部的各模板实参在能从函数参数推导时不需要指定:

template<typename T>
void f(T s)
{
    std::cout << s << '\n';
}
 
template void f<double>(double); // 实例化 f<double>(double)
template void f<>(char);         // 实例化 f<char>(char),推导出模板实参
template void f(int);            // 实例化 f<int>(int),推导出模板实参

函数模板或类模板成员函数的显式实例化不能使用 inlineconstexpr。如果显式实例化的声明指名了某个隐式声明的特殊成员函数,那么程序非良构。

构造函数的显式实例化不能使用模板形参列表(语法 (1)),也始终不需要使用,因为能推导它们(语法 (2))。

预期的析构函数的显式实例化必须指名该类的被选择的析构函数。

(C++20 起)

显式实例化声明不会抑制 inline 函数,auto 声明,引用,以及类模板特化的隐式实例化。(从而当作为显式实例化声明目标的 inline 函数被 ODR 式使用时,它会为内联而隐式实例化,但此翻译单元中不会生成它的非内联副本)

带有默认实参的函数模板的显式实例化定义不会使用或试图初始化该实参:

char* p = 0;
template<class T> T g(T x = &p) { return x; }
template int g<int>(int); // OK,即使 &p 不是 int。

隐式实例化

当代码在要求存在函数定义的语境中指涉某个函数,或定义存在与否会影响程序语义 (C++11 起),而这个特定函数尚未被显式实例化时,发生隐式实例化。如果模板实参列表能从语境推导,那么不必提供它。

#include <iostream>
 
template<typename T>
void f(T s)
{
    std::cout << s << '\n';
}
 
int main()
{
    f<double>(1); // 实例化并调用 f<double>(double)
    f<>('a');     // 实例化并调用 f<char>(char)
    f(7);         // 实例化并调用 f<int>(int)
    void (*pf)(std::string) = f; // 实例化 f<string>(string)
    pf("∇");                     // 调用 f<string>(string)
}

如果有表达式需要某函数进行常量求值,那么函数定义存在与否会影响程序语义,即使不要求常量求值表达式,或常量表达式求值不使用该定义。

template<typename T> constexpr int f() { return T::value; }
template<bool B, typename T> void g(decltype(B ? f<T>() : 0));
template<bool B, typename T> void g(...);
template<bool B, typename T> void h(decltype(int{B ? f<T>() : 0}));
template<bool B, typename T> void h(...);
void x() {
  g<false, int>(0); // OK: B ? f<T>() : 0 不会潜在常量求值
  h<false, int>(0); // 错误:即使 B 求值为 false 且从 int 到 int 的列表初始化不可能是窄化
                    // 仍实例化 f<int>
}
(C++11 起)

注意:完全省略 <> 允许重载决议同时检验模板与非模板重载。

模板实参推导

实例化一个函数模板需要知道它的所有模板实参,但不需要指定每个模板实参。编译器会尽可能从函数实参推导缺失的模板实参。这会在尝试进行函数调用以及取函数模板的地址时发生。

template<typename To, typename From> To convert(From f);
 
void g(double d) 
{
    int i = convert<int>(d);    // 调用 convert<int,double>(double)
    char c = convert<char>(d);  // 调用 convert<char,double>(double)
    int(*ptr)(float) = convert; // 实例化 convert<int, float>(float)
}

模板运算符依赖此机制,因为除了将它重写为函数调用表达式之外,不存在为运算符指定模板实参的语法。

#include <iostream>
int main() 
{
    std::cout << "Hello, world" << std::endl;
    // operator<< 经由 ADL 查找为 std::operator<<,
    // 然后推导出 operator<<<char, std::char_traits<char>>
    // 同时推导 std::endl 为 &std::endl<char, std::char_traits<char>>
}

模板实参推导在函数模板的名字查找(可能涉及实参依赖查找)之后,重载决议之前进行。

细节见模板实参推导

显式模板实参

函数模板的模板实参可从以下途径获得:

  • 模板实参推导
  • 默认模板实参
  • 显式指定,可以在下列语境中进行:
  • 在函数调用表达式中
  • 当取函数地址时
  • 当初始化到函数的引用时
  • 当构成成员函数指针时
  • 在显式特化中
  • 在显式实例化中
  • 在友元声明中

不存在为重载的运算符转换函数和构造函数显式指定模板实参的方法,因为它们不会通过函数名调用。

所指定的各模板实参必须与各模板形参在种类上相匹配(即类型对类型,非类型对非类型,模板对模板)。实参的数量不能多于形参(除非形参中有形参包,这种情况下每个非包形参必须对应一个实参)。

所指定的非类型实参必须要么与其对应的非类型模板形参的类型相匹配,要么可以转换到这些类型

不参与模板实参推导的函数实参(例如对应的模板实参已经被显式指定)会参与到它对应的函数形参的类型的隐式转换(如在通常重载决议中一样)。

当有额外的实参时,模板实参推导可以扩充显式指定的模板形参包:

template<class ... Types> void f(Types ... values);
void g() {
  f<int*, float*>(0, 0, 0); // Types = {int*, float*, int}
}

模板实参替换

当已经指定、推导出或从默认模板实参获得了所有的模板实参之后,函数形参列表中对模板形参的每次使用都会被替换成对应的模板实参。

函数模板在替换失败时(即以推导或提供的模板实参替换模板形参失败)会从重载集中移除。这样就有许多方式通过模板元编程来操作重载集:细节见 SFINAE

替换之后,所有数组和函数类型的函数形参都被调整为指针,且所有函数形参都会移除顶层 cv 限定(如在常规函数声明中一样)。

移除顶层 cv 限定并不会影响形参在函数内展现的类型:

template <class T> void f(T t);
template <class X> void g(const X x);
template <class Z> void h(Z z, Z* zp);
 
// 两个不同的函数的类型相同,但 t 在这些函数中有不同的 cv 限定
f<int>(1);       // 函数类型是 void(int),t 是 int
f<const int>(1); // 函数类型是 void(int),t 是 const int
 
// 两个不同的函数的类型和 x 相同
// (指向这两个函数的指针不相等,且函数局部的静态变量可以拥有不同地址)
g<int>(1);       // 函数类型是 void(int),x 是 const int
g<const int>(1); // 函数类型是 void(int),x 是 const int
 
// 只移除顶层 cv 限定:
h<const int>(1, NULL); // 函数类型是 void(int, const int*) 
                       // z 是 const int,zp 是 int*

函数模板重载

函数模板与非模板函数可以重载。

非模板函数与具有相同类型的模板特化始终不同。即使具有相同类型,不同函数模板的特化也始终互不相同。两个具有相同返回类型和相同形参列表的函数模板是不同的,而且可以用显式模板实参列表进行区分。

当使用了类型或非类型模板形参的表达式在函数形参列表或返回类型中出现时,该表达式会为了重载而保留为函数模板签名的一部分:

template<int I, int J> A<I+J> f(A<I>, A<J>); // 重载 #1
template<int K, int L> A<K+L> f(A<K>, A<L>); // 同 #1
template<int I, int J> A<I-J> f(A<I>, A<J>); // 重载 #2

对于两个涉及模板形参的表达式,如果两个包含这些表达式的函数定义根据 单一定义规则相同,那么称它们等价,就是说,除了模板形参的命名可以不同之外,这两个表达式含有相同的记号序列,其中的各个名字通过名字查找都解析到相同的实体。两个 lambda 表达式始终不等价。 (C++20 起)

template <int I, int J> void f(A<I+J>);  // 模板重载 #1
template <int K, int L> void f(A<K+L>);  // 等价于 #1

在确定两个待决表达式是否等价时,只考虑其中所涉及的各待决名,而不考虑名字查找的结果。如果相同模板的多个声明在名字查找的结果上有所不同,那么使用它们中的第一个:

template <class T> decltype(g(T())) h();  // decltype(g(T())) 是待决类型
int g(int);
template <class T> decltype(g(T())) h() { // h() 的再声明使用较早的查找,
    return g(T());                        // 尽管此处的查找找到了 g(int)
}
int i = h<int>(); // 模板实参替换失败;g(int) 不在 h() 的首个声明处的作用域中

当满足下列条件时,认为两个函数模板等价

  • 它们在同一作用域声明
  • 它们具有相同的名字
  • 它们拥有等价的模板形参列表,意思是列表长度相同,且每对对应的形参均满足下列条件:
  • 两个形参的种类相同(都是类型、都是非类型或都是模板),
  • 它们都是形参包或都不是,
  • 如果是非类型,那么它们的类型等价,
  • 如果是模板,那么它们的模板形参等价,
  • 如果有一个声明带概念名,那么另一个也有等价的概念名。
(C++20 起)
  • 它们的返回类型和形参列表中所有涉及模板实参的表达式均等价
  • 模板形参列表之后的 requires 子句(如果存在)中的各个表达式均等价
  • 函数声明符之后的 requires 子句(如果存在)中的各个表达式均等价
(C++20 起)

对于两个涉及模板形参的表达式,如果它们不等价但它们对于任何给定的模板实参集的求值都产生相同的值,那么称它们功能等价

如果两个函数模板本来可以等价,但它们的返回类型和形参列表中一或多个涉及模板形参的表达式功能等价,那么称它们功能等价

另外,如果为两个函数模板指定的约束不同,但它们接受且被相同的模板实参列表的集合所满足,那么它们功能等价但不等价

(C++20 起)

如果程序含有功能等价但不等价的函数模板声明,那么程序非良构;不要求诊断。

// 等价
template <int I> void f(A<I>, A<I+10>); // 重载 #1
template <int I> void f(A<I>, A<I+10>); // 重载 #1 的再声明
 
// 不等价
template <int I> void f(A<I>, A<I+10>); // 重载 #1
template <int I> void f(A<I>, A<I+11>); // 重载 #2
 
// 功能等价但不等价
// 程序非良构,不要求诊断
template <int I> void f(A<I>, A<I+10>);      // 重载 #1
template <int I> void f(A<I>, A<I+1+2+3+4>); // 功能等价

当同一个函数模板特化与多于一个重载的函数模板相匹配时(这通常由模板实参推导所导致),执行重载函数模板的偏序处理以选择最佳匹配。

具体而言,在以下情形中发生偏序处理:

1) 对函数模板特化的调用的重载决议
template<class X> void f(X a);
template<class X> void f(X* a);
int* p;
f(p);
2)函数模板特化的地址时:
template<class X> void f(X a);
template<class X> void f(X* a);
void (*p)(int*) = &f;
3) 选择作为函数模板特化的布置 operator delete 以匹配布置 operator new 时:
4)友元函数声明显式实例化显式特化指代函数模板特化时:
template<class X> void f(X a);  // 第一个模板 f
template<class X> void f(X* a); // 第二个模板 f
template<> void f<>(int *a) {}  // 显式特化
// 模板实参推导出现两个候选:
// f<int*>(int*) 与 f<int>(int*)
// 偏序选择 f<int>(int*),因为它更特殊

非正式而言,“A 比 B 更特殊”意味着“A 比 B 接受更少的类型”。

正式而言,为确定任意两个函数模板中哪个更特殊,偏序处理首先对两个模板之一进行以下变换:

  • 对于每个类型、非类型及模板形参,包括形参包,生成一个唯一的虚构类型、值或模板,并将其替换到模板的函数类型中
  • 如果要比较的两个函数模板中只有一个是成员函数,且该函数模板是某个类 A 的非静态成员,那么向它的形参列表的开头插入一个新的形参,当成员函数模板有 && 限定时,它的类型是 cv A&&,否则是 cv A&(其中 cv 是成员函数模板的 cv 限定)——这有助于对运算符的定序,它们是同时作为成员和非成员函数查找的:
struct A {};
template<class T> struct B {
  template<class R> int operator*(R&);            // #1
};
template<class T, class R> int operator*(T&, R&); // #2
int main() {
  A a;
  B<A> b;
  b * a; // 模板实参推导对于 int B<A>::operator*(R&) 给出 R=A 
         //           对于 int operator*(T&, R&),T=B<A>,R=A
// 为进行偏序处理,将成员 template B<A>::operator*
// 变换成 template<class R> int operator*(B<A>&, R&);
//     int operator*(   T&, R&)  T=B<A>, R=A
// 与  int operator*(B<A>&, R&)  R=A 间的偏序
// 选择 int operator*(B<A>&, A&) 为更特殊者

在按上方描述变换两个模板之一后,以变换后的模板为实参模板,以另一模板的原模板类型为形参模板,执行模板实参推导。然后以第二个模板(进行变换后)为实参,以第一个模板的原始形式为形参重复这一过程。

用于确定顺序的类型取决于语境:

  • 在函数调用的语境中,这些类型是在这个函数调用中具有实参的函数形参的类型(不考虑默认函数实参、形参包和省略号形参——见下文)
  • 在调用用户定义的转换函数的语境中,使用转换函数模板的返回类型
  • 在其他语境中,使用函数模板类型

形参模板中的每个以上列出的类型都会被推导。推导开始前,以下列方式对形参模板的每个形参 P 和实参模板的对应实参 A 进行调整:

  • 如果 PA 此前都是引用类型,那么确定哪个有更多的 cv 限定(其他所有情况下,就偏序目的而言都忽略 cv 限定)
  • 如果 P 是引用类型,那么以它所引用的类型替换它
  • 如果 A 是引用类型,那么以它所引用的类型替换它
  • 如果 P 有 cv 限定,那么 P 被替换为自身的无 cv 限定版本
  • 如果 A 有 cv 限定,那么 A 被替换为自身的无 cv 限定版本

在这些调整后,遵循从类型进行模板实参推导规则,从 A 推导 P

如果 P 是函数形参包,那么实参模板的每个剩余形参类型的类型 A 都与该函数参数包的声明符标识的类型 P 进行比较。每次比较都为该函数参数包所展开的模板参数包中的后继位置的进行模板实参的推导。

如果 A 从函数参数包变换而来,那么将它与形参模板的每个剩余形参类型进行比较。

如果变换后的模板 1 的实参 A 可以用来推导模板 2 的对应形参 P,但反之不可,那么对于从这一对 P/A 所推导的类型而言,这个 AP 更特殊。

如果双向推导均成功,且原 PA 都是引用类型,那么做附加的测试:

  • 如果 A 是左值引用而 P 是右值引用,那么认为 AP 更特殊
  • 如果 AP 有更多的 cv 限定,那么认为 AP 更特殊

所有其他情况下,对于这一对 P/A 所推导的类型而言,没有模板比另一个更特殊。

在以两个方向考虑每个 PA 后,如果对于所考虑的每个类型,

  • 模板 1 对所有类型至少与模板 2 一样特殊
  • 模板 1 对某些类型比模板 2 特殊
  • 模板 2 对任何类型都不比模板 1 更特殊,或并非对任何类型都至少一样特殊

那么模板 1 比模板 2 更特殊。如果上述条件在切换模板顺序后为真,那么模板 2 比模板 1 更特殊。否则,没有模板比另一个更特殊。 持平的情况下,如果一个函数模板有一个尾部的形参包而另一个没有,那么认为带有被忽略的形参者比有空形参包者更特殊。

如果在考虑所有的重载模板对之后,有一个无歧义地比所有其他的都更特殊,那么选择这个模板的特化,否则编译失败。

在下列示例中,虚构实参被称为 U1, U2:

template<class T> void f(T);        // 模板 #1
template<class T> void f(T*);       // 模板 #2
template<class T> void f(const T*); // 模板 #3
void m() {
  const int* p;
  f(p); // 重载决议选取:#1:void f(T)  [T = const int *]
        //             #2:void f(T*) [T = const int]
        //             #3:void f(const T *) [T = int]
// 偏序处理
// 从变换后的 #2 到 #1:从 void(U1*) 到 void(T) :P=T  A=U1*:推导成功:T=U1*
// 从变换后的 #1 到 #2:从 void(U1)  到 void(T*):P=T* A=U1 :推导失败
// 对于 T 而言 #2 比 #1 更特殊
// 从变换后的 #3 到 #1:从 void(const U1*) 到 void(T):P=T, A=const U1*:成功
// 从变换后的 #1 到 #3:从 void(U1) 到 void(const T*):P=const T*, A=U1:失败
// 对于 T 而言 #3 比 #1 更特殊
// 从变换后的 #3 到 #2:从 void(const U1*) 到 void(T*):P=T* A=const U1*:成功
// 从变换后的 #2 到 #3:从 void(U1*) 到 void(const T*):P=const T* A=U1*:失败
// 对于 T 而言 #3 比 #2 更特殊
// 结果:#3 被选择
// 换言之,f(const T*) 比 f(T) 或 f(T*) 更特殊
}
template<class T> void f(T, T*);   // #1
template<class T> void f(T, int*); // #2
void m(int* p) {
    f(0, p); // #1 的推导:void f(T, T*)   [T = int]
             // #2 的推导:void f(T, int*) [T = int]
// 偏序:
// 从 #2 到 #1:从 void(U1,int*) 到 void(T,T*):
// P1=T    A1=U1  :T=U1
// P2=T*   A2=int*:T=int:失败
// 从 #1 到 #2:从 void(U1, U2*) 到 void(T,int*):
// P1=T    A1=U1  :T=U1
// P2=int* A2=U2* :失败
// 对于 T 而言没有更特殊的一方,调用有歧义
}
template<class T> void g(T);  // 模板 #1
template<class T> void g(T&); // 模板 #2
void m() {
  float x;
  g(x); // 从 #1 推导:void g(T)  [T = float]
        // 从 #2 推导:void g(T&) [T = float]
// 偏序:
// 从 #2 到 #1:从 void(U1&) 到 void(T):
// P=T          A=U1(调整后):成功
// 从 #1 到 #2:从 void(U1)  到 void(T&):
// P=T(调整后) A=U1         :成功
// 对于 T 而言没有更特殊的一方,调用有歧义
}
template<class T> struct A { A(); };
 
template<class T> void h(const T&); // #1
template<class T> void h(A<T>&);    // #2
void m() {
  A<int> z;
  h(z);  // 从 #1 推导:void h(const T&) [T = A<int>]
         // 从 #2 推导:void h(A<T>&)    [T = int]
// 偏序:
// 从 #2 到 #1:从 void(A<U1>&)    到 void(const T&):
// P=T    A=A<U1>   :成功 T=A<U1>
// 从 #1 到 #2:从 void(const U1&) 到 void(A<T>&):
// P=A<T> A=const U1:失败
// 对于 T 而言 #2 比 #1 更特殊
 
  const A<int> z2;
  h(z2); // 从 #1 推导:void h(const T&) [T = A<int>]
         // 从 #2 推导:void h(A<T>&)    [T = int],但替换失败
// 只有一个可选择的重载,不尝试偏序处理,调用 #1
}

因为在调用语境中只考虑有明确的调用实参的形参,所以没有明确的调用实参的形参,包括函数形参包、省略号形参及有默认实参的形参均被忽略:

template<class T> void f(T);         // #1
template<class T> void f(T*, int=1); // #2
void m(int* ip) {
  int* ip;
  f(ip); // 调用 #2(T* 比 T 更特殊)
}
template<class T> void g(T);       // #1
template<class T> void g(T*, ...); // #2
void m(int* ip) {
  g(ip); // 调用 #2(T* 比 T 更特殊)
}
template<class T, class U> struct A { };
template<class T, class U> void f(U, A<U,T>* p = 0); // #1
template<         class U> void f(U, A<U,U>* p = 0); // #2
void h() {
  f<int>(42, (A<int, int>*)0); // 调用 #2
  f<int>(42);                  // 错误:歧义
}
template<class T            > void g(T, T = T()); // #1
template<class T, class... U> void g(T, U...);    // #2
void h() {
  g(42); // 错误:歧义
}
template<class T, class... U> void f(T, U...); // #1
template<class T            > void f(T);       // #2
void h(int i) {
  f(&i); // 因为形参包与无形参之间的决胜规则调用 #2
         // (注意:在 DR692 与 DR1395 之间时有歧义)
}
template<class T, class... U> void g(T*, U...); // #1
template<class T            > void g(T);        // #2
void h(int i) {
  g(&i); // OK:调用 #1(T* 比 T 更特殊)
}
template <class ...T> int f(T*...);  // #1
template <class T>  int f(const T&); // #2
f((int*)0); // OK:选择 #2;非变参模板比变参模板更特殊
            // (DR1395 之前有歧义,因为两个方向的推导均失败)
template<class... Args>           void f(Args... args);        // #1
template<class T1, class... Args> void f(T1 a1, Args... args); // #2
template<class T1, class T2>      void f(T1 a1, T2 a2);        // #3
f();        // 调用 #1
f(1, 2, 3); // 调用 #2
f(1, 2);    // 调用 #3;非变参模板 #3 比变参模板 #1 与 #2 更特殊

在偏序处理内的模板实参推导期间,如果有实参不在任何被偏序考虑的类型中,那么不要求该实参与模板形参相匹配:

template <class T>          T f(int); // #1
template <class T, class U> T f(U);   // #2
void g() {
  f<int>(1); // #1 的特化为显式:T f(int) [T = int]
             // #2 的特化为推导:T f(U)   [T = int, U = int]
// 偏序(只考虑实参类型):
// 从 #2 到 #1:从 U1(U2)  到 T(int):失败
// 从 #1 到 #2:从 U1(int) 到 T(U)  :成功:U=int, T 未使用
// 调用 #1
}

对包含模板形参包的函数模板进行的偏序处理,与为这些模板形参包所推导的实参数量无关。

template<class...> struct Tuple { };
template<          class... Types> void g(Tuple<Types ...>);      // #1
template<class T1, class... Types> void g(Tuple<T1, Types ...>);  // #2
template<class T1, class... Types> void g(Tuple<T1, Types& ...>); // #3
 
g(Tuple<>());            // 调用 #1
g(Tuple<int, float>());  // 调用 #2
g(Tuple<int, float&>()); // 调用 #3
g(Tuple<int>());         // 调用 #3


为编译对函数模板的调用,编译器必须在非模板重载、模板重载和模板重载的特化间作出决定。

template<class T> void f(T);      // #1:模板重载
template<class T> void f(T*);     // #2:模板重载
void                   f(double); // #3:非模板重载
template<>        void f(int);    // #4:#1 的特化
 
f('a');        // 调用 #1
f(new int(1)); // 调用 #2
f(1.0);        // 调用 #3
f(1);          // 调用 #4

函数重载 vs 函数特化

注意,只有非模板和主模板重载参与重载决议。特化并不是重载,因此不受考虑。只有在重载决议选择最佳匹配的主函数模板后,才检验它的特化以查看最佳匹配者。

template<class T> void f(T);    // #1:所有类型的重载
template<>        void f(int*); // #2:#1 的特化,针对指向 int 的指针
template<class T> void f(T*);   // #3:所有指针类型的重载
 
f(new int(1)); // 调用 #3,虽然 #1 的特化是完美匹配

在对翻译单元的头文件进行排序时,记住此规则很重要。有关函数重载与函数特化之间的更多示例在下面展开:



首先考虑一些不使用实参依赖查找的场景。对于这种情况,我们使用调用 (f)(t)。如 ADL 中的描述,将函数名包在括号中可抑制实参依赖查找。

  • g() 中声明的引用点之前的多个 f() 重载。
#include <iostream>
struct A{};
template<class T> void f(T)  {std::cout << "#1\n";} // 在 f() 的引用点前的重载 #1
template<class T> void f(T*) {std::cout << "#2\n";} // 在 f() 的引用点前的重载 #2
template<class T> void g(T* t) 
{
    (f)(t); // f() 的引用点
}
 
int main()
{
    A *p = nullptr;
    g(p);   // g() 和 f() 的引用点
}
 
// #1 与 #2 都被添加到候选列表;
// 选择 #2 因为它是更好的匹配。

输出:

#2


  • 匹配较好的模板重载在引用点之后声明。
#include <iostream>
struct A{};
template<class T> void f(T)  {std::cout << "#1\n";} // #1
template<class T> void g(T* t) 
{
    (f)(t); // f() 的引用点
}
template<class T> void f(T*) {std::cout << "#2\n";} // #2
 
int main()
{
    A *p = nullptr;
    g(p);   // g() 和 f() 的引用点
}
 
// 只有 #1 会添加到到候选列表;#2 在引用点之后定义;
// 因此,即使它是较佳匹配,重载也不会考虑它。

输出:

#1


  • 匹配较好的显式模板特化在引用点之后声明。
#include <iostream>
struct A{};
template<class T> void f(T)    {std::cout << "#1\n";} // #1
template<class T> void g(T* t) 
{
    (f)(t); // f() 的引用点
}
template<>        void f<>(A*) {std::cout << "#3\n";} // #3
 
int main()
{
    A *p = nullptr;
    g(p);   // g() 和 f() 的引用点
}
 
// 添加 #1 到候选列表;#3 是在引用点后定义的较好匹配。候选列表由最终被选择的 #1 组成。
// 之后,在引用点后声明的 #1 的显式特化 #3 被选择,因为它是较好的匹配。
// 此行为由 14.7.3/6 [temp.expl.spec] 掌控且与 ADL 无关。

输出:

#3


  • 匹配较好的模板重载在引用点之后声明。匹配最佳的显式模板特化在这个较好的匹配重载之后声明。
#include <iostream>
struct A{};
template<class T> void f(T)    {std::cout << "#1\n";} // #1
template<class T> void g(T* t) 
{
    (f)(t); // f() 的引用点
}
template<class T> void f(T*)   {std::cout << "#2\n";} // #2
template<>        void f<>(A*) {std::cout << "#3\n";} // #3
 
int main()
{
    A *p = nullptr;
    g(p);   // g() 和 f() 的引用点
}
 
// #1 是候选列表的唯一成员且它最终被选择。
// 之后,跳过显式特化 #3,因为它实际特化了在引用点后声明的 #2。

输出:

#1


现在让我们考虑使用实参依赖查找的情况(即我们用更常见的调用格式 f(t))。

  • 匹配较好的模板重载在引用点之后声明。
#include <iostream>
struct A{};
template<class T> void f(T)  {std::cout << "#1\n";} // #1
template<class T> void g(T* t) 
{
    f(t); // f() 的引用点
}
template<class T> void f(T*) {std::cout << "#2\n";} // #2
 
int main()
{
    A *p = nullptr;
    g(p); // g() 和 f() 的引用点
}
 
// #1 被作为常规查找的结果添加到候选列表;
// #2 在引用点之后定义但经由 ADL 查找添加到候选列表。
// #2 作为较好的匹配被选择。

输出:

#2


  • 匹配较好的模板重载在引用点之后声明。匹配最佳的显式模板特化在这个匹配较好的重载之前声明。
#include <iostream>
struct A{};
template<class T> void f(T)    {std::cout << "#1\n";} // #1
template<class T> void g(T* t) 
{
    f(t); // f() 的引用点
}
template<>        void f<>(A*) {std::cout << "#3\n";} // #3
template<class T> void f(T*)   {std::cout << "#2\n";} // #2
 
int main()
{
    A *p = nullptr;
    g(p); // g() 和 f() 的引用点
}
 
// #1 被作为常规查找的结果添加到候选列表;
// #2 在引用点之后定义但经由 ADL 查找添加到候选列表。
// 作为较好的匹配,从各主模板中选择 #2。
// 因为 #3 在 #2 之前声明,所以它是 #1 的显式特化。
// 从而最终选择 #2。

输出:

#2


  • 匹配较好的模板重载在引用点之后声明,匹配最佳的显式模板特化最后声明。
#include <iostream>
struct A{};
template<class T> void f(T)    {std::cout << "#1\n";} // #1
template<class T> void g(T* t) 
{
    f(t); // f() 的引用点
}
template<class T> void f(T*)   {std::cout << "#2\n";} // #2
template<>        void f<>(A*) {std::cout << "#3\n";} // #3
 
int main()
{
    A *p = nullptr;
    g(p); // g() 和 f() 的引用点
}
 
// #1 被作为常规查找的结果添加到候选列表;
// #2 在引用点之后定义但经由 ADL 查找添加到候选列表。
// 作为较好的匹配,从各主模板中选择 #2。
// 因为 #3 在 #2 之后声明,所以它是 #2 的显式特化;
// 从而被选为调用的函数。

输出:

#3


当实参是一些 C++ 基础类型时,没有 ADL 关联的命名空间。因此这些场景与上述非 ADL 示例等同。


有关重载决议的详细规则,见重载决议

函数模板特化

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

DR 应用于 出版时的行为 正确行为
CWG 581 C++98 允许构造函数模板的显式特化或实例化中的模板形参列表 已禁止
CWG 1321 C++98 不明确首个声明与重声明中的相同待决名是否等价 它们等价并且含义与首个声明中的相同
CWG 1395 C++11 从形参包推导 A 时失败,且对于空形参包没有决胜规则 允许推导,添加决胜规则

参阅