国庆长假期间又翻了翻 《C++ Primer》,看到模板函数特化,就想起来以前遇到的一个问题。这个问题我曾经在 TopLanguage 讨论组请教过(链接),今天翻出来又仔细想了想,做一个总结吧。
困惑起源于以字符串作为参数,如何匹配到特化的模板函数。代码如下,其中注释部分是该句对应的输出(使用 VS2008 编译器,一会儿再讨论 g++ 的问题):
#include <iostream>
#include <typeinfo>
using namespace std;template<typename T>
void foo(const T& t)
{
cout << "foo: generic " << typeid(t).name() << endl;
}template<>
void foo<const char *>(const char * const& t)
{
cout << "foo: special " << typeid(t).name() << endl;
}template<typename T>
void bar(const T t)
{
cout << "bar: generic " << typeid(t).name() << endl;
}template<>
void bar<const char *>(const char * t)
{
cout << "bar: special " << typeid(t).name() << endl;
}int main()
{
char str[] = "hello";
const char con_str[] = "hello";
const char * const p = "hello";
foo("hello"); // foo: generic char const [6]
foo(static_cast<const char * const>("hello")); // foo: special char const *
foo(static_cast<const char *>("hello")); // foo: special char const *
foo(str); // foo: generic char const [6]
foo(con_str); // foo: generic char const [6]
foo(p); // foo: special char const *
bar("hello"); // bar: special char const *
bar(str); // bar: generic char *
bar(con_str); // bar: special char const *
bar(p); // bar: special char const *
cout << typeid("hello").name() << endl; // char const [6]
cout << typeid(str).name() << endl; // char [6]
cout << typeid(con_str).name() << endl; // char const [6]
cout << typeid(p).name() << endl; // char const *
return 0;
}
首先让我奇怪的问题是,第一个 foo 函数调用 foo("hello"),为什么实际调用的不是特化的 foo 函数?
其实这个例子是有起源的,《C++ Primer》第四版 Section 16.6.1 的最后给出这样一个例子:
// define the general compare template
template <class T>
int compare(const T& t1, const T& t2) { /* ... */ }int main() {
// uses the generic template definition
int i = compare("hello", "world");
// ...
}// invalid program: explicit specialization after call
template<>
int compare<const char*>(const char* const& s1,
const char* const& s2)
{ /* ... */ }
并解释说:
This program is in error because a call that would match the specialization is made before the specialization is declared. When the compiler sees a call, it must know to expect a specialization for this version. Otherwise, the compiler is allowed to instantiate the function from the template definition.
那么我认为作者暗含的意思里有,compare("hello", "world") 这个函数调用是 match 特化的 compare 函数的。但是从我们给出的第一段代码输出来看,并不是这个样子的,所以我谨慎地怀疑,《C++ Primer》给出的这个例子是有错的。虽然这段程序的确有错,但是即使将特化函数提到前面,compare("hello", "world") 仍然不会调用该特化函数。
请教了别人、书本和标准之后,下面我试着对上面每句的输出做一下解释(当然,可能有错,请指正):
1. foo("hello"); // foo: generic char const [6]
"hello"具有类型 char const [6],由于 foo 模板使用的是引用参数,因此数组实参不会被转换成指针,而是追求一个较为精确的匹配,因此编译器实例化一个 void foo<char const [6]>(const char (& t)[6]) 模板函数(VS2008),这也是为什么我们能看到参数的类型输出是 char const [6];
2. foo(static_cast<const char * const>("hello")); // foo: special char const *
"hello"被 cast 成了 const char * const 类型,自然与特化的函数 void foo<const char *>(const char * const& t) 能够精确匹配,因此调用的是特化的 foo;
3. foo(static_cast<const char *>("hello")); // foo: special char const *
"hello"被 cast 成了 const char * 类型,虽然少了一个 const,但是 C++ 标准中有这样的说法:
14.8.2.3
If the orignial A is a reference type, A can be more cv-qualified than the deduced A
这种 cv-qualifier 并不影响推导,最终仍然是匹配到特化的 foo 函数;
4. foo(str); // foo: generic char const [6]
str 和 "hello" 也是仅仅相差一个 cv-qualifier,也不影响推导,其结果与 1 是一致的;
5. foo(con_str); // foo: generic char const [6]
con_str 和 "hello" 的类型一样,显然其结果与 1 应是一致的;
6. foo(p); // foo: special char const *
p 的类型其实就是 2 中参数被 cast 之后的类型,显然其结果应该与 2 一致;
7. bar("hello"); // bar: special char const *
乍一看就有些奇怪,为什么把模板参数换成值(而不是引用),特化的情况就与 foo 不同了呢?C++ 标准中有这样的规定:
14.8.2.3
If A is not a reference type:
-- If P is an array type, the pointer type produced by the array-to-pointer standard conversion (4.2) is used in place of P for type deduction;
因此,这里 "hello" 原本是一个数组类型,由于模板的参数不是引用类型,所以 "hello" 的类型被转换为指针类型 char const * 参加推导,正好与特化的 bar 函数匹配;
8. bar(str); // bar: generic char *
由于模板参数不是引用类型,没有 const 限定的 str 无法匹配特化的 bar,因此编译器实例化一个 void bar<char *>(char * t) 模板函数;
9. bar(con_str); // bar: special char const *
由于 con_str 与 "hello" 的类型一样,因此其结果与 7 是一致的;
10. bar(p); // bar: special char const *
这里 p 的类型本身就是特化函数的参数类型,显然要被推导为调用特化函数。
解释完了字符串参数的模板函数推导问题,下面来讨论一下 g++ 和 VS2008 的不同。上面同样的代码,使用 g++ 编译之后,输出是这个样子的:
foo: generic A6_c
foo: special PKc
foo: special PKc
foo: generic A6_c
foo: generic A6_c
foo: special PKc
bar: special PKc
bar: generic Pc
bar: special PKc
bar: special PKc
A6_c
A6_c
A6_c
PKc
当然,需要解释的是 g++ 内部对符号的字面做了一些变化,我们可以使用 c++filt demangle 这些符号:
$ c++filt [-t] A6_c PKc Pc
char [6]
char const *
char *
与 VS2008 的输出相比,我有一个疑问,为什么 g++ 没有为 const char [6] 输出正确的 const 类型名呢?
还有,我们提到了第 1 种情况下,编译器为 foo("hello") 调用实例化了一个 void foo<char const [6]>(const char (& t)[6]) 类型的函数。假如我们提供了一个类似的特化函数,那么 foo("hello") 会调用该特化函数;但是,使用 g++ 编译器时,特化函数的类型必须是 void foo<char [6]>(const char (& t)[6]) 而不是 void foo<char const [6]>(const char (& t)[6]),这让我感觉非常奇怪。只有不提供模板参数时,比如 void foo(const char (& t)[6]),两个编译器才能都推导出调用特化函数。
需要验证的话,您可以尝试在第一段代码中增加下面两个特化函数,再在两个编译器上编译那段代码:
template<>
void foo<char [6]>(const char (& t)[6])
{
cout << "foo: special<char [6]> " << typeid(t).name() << endl;
}template<>
void foo<char const [6]>(const char (& t)[6])
{
cout << "foo: special<char const [6]> " << typeid(t).name() << endl;
}