为了简化讨论,本文仅对函数模板的的名称绑定进行总结概括,关于类模板的相关内容,以后再做讨论。
Name Binding
Name Binding就是对模板定义中出现的名称(也称为构造’construct’,也包括操作符等),通过在相关的上下文中查询, 并绑定到声明的过程,比如,
int all = 0;
template<typename T>
int sum(T* t, int s)
{
for(int index = 0; index < s; index++)
all += t[index];
return all;
}
比如这里出现的all
, 就是一个name
, 编译器解析到这个名称的时候要做出决定绑定到哪个对象,以及何时去绑定。
Name Binding分为两步,第一步是在模板定义的时候(Point Of Definition), 第二步是模板实例化的时候(Point Of Instantiation). 而模板中出现的名称(也包括操作符等), 相应的分为两种, 一种叫dependent name, 另一种叫non-dependent name.
Dependent Name 和 Non-dependent Name
Dependent Name就是它的具体类型依赖于模板参数,在模板定义的时候不能确定,需要推迟到模板实例化的时候才能够确定的名称, 比如:
//示例1
int i = 0;
template<typename T>
void g(T t)
{
typename T::Value V;
f(t);
i++;
}
这里V
和 f
都依赖于类型参数T
, 因此都是Dependent Name, 而i
则是Non-dependent name. 对于Non-dependent name, 应该在模板定义上下文中,能够唯一确定(绑定), 这和普通函数中的规则是一致的。 对于Non-dependent name, 需要注意的是,一旦绑定到具体的关联对象, 它就不可以更改了,哪怕在后面模板实例化的时候,有更贴切的匹配,都不能够再绑定到新的对象了。
//示例2
//main.cpp
#include <iostream>
void f(double) { std::cout << "f(double)" << std::endl; }
template<typename T>
void g(T t)
{
f(1); //non-dependent name, 在解析模板时就绑定了
};
void f(int) { std::cout << "f(int)" << std::endl; }
int main()
{
g(1);
}
我们以主流的编译器为例,给出不同编译器的运行结果
编译器 | 编译参数 | 运行结果 |
---|---|---|
gcc 4.9.3 | g++ -Wall -std=c++11 -O2 -o a.out main.cpp | f(double) |
visual c++ 19.00 | cl.exe main.cpp -o a.exe /MD | f(int) |
clang 3.7.0 | clang++ -Wall -std=c++11 -O2 -o a.out main.cpp | f(double) |
从结果来看,似乎gcc 和 clang更符合标准对Non-dependent Name解析范围的定义, 即在模板定义上下文中能够找到匹配,并绑定到该匹配,以后不可以再更改。 而VC++似乎在这一点上没有按照标准来执行查找, 而是将Non-dependent Name的查找推迟了。 为了进一步证明我们的判断, 将代码稍微做些更改,去掉模板函数g
之前的f(double)
函数的定义:
//示例3
//main.cpp
#include <iostream>
template<typename T>
void g(T t)
{
f(1); //non-dependent name, 在解析模板时就绑定了
};
void f(int) { std::cout << "f(int)" << std::endl; }
int main()
{
g(1);
}
编译器 | 编译参数 | 结果 |
---|---|---|
gcc 4.9.3 | g++ -Wall -std=c++11 -O2 -o a.out main.cpp | error: there are no arguments to ‘f’ that depend on a template parameter, so a declaration of ‘f’ must be available [-fpermissive] |
visual c++ 19.00 | cl.exe main.cpp -o a.exe /MD | f(int) |
clang 3.7.0 | clang++ -Wall -std=c++11 -O2 -o a.out main.cpp | error: use of undeclared identifier ‘f’ |
此刻很明确的一点就是VC++
没有按照标准的定义,来查找Non-dependent Name。 我们在下面的MSDN页面查找到了VC++
,非标准行为(Non-standard Behavior)的说明:
The Visual C++ compiler does not currently support binding
non-dependent names when initially parsing a template. This does not
comply with section 14.6.3 of the C++ ISO specification. This can
cause overloads declared after the template (but before the template
is instantiated) to be seen.
注: 以上说明针对目前Visual Studio最新版本, VS2015
.
gcc针对这种非标准行为,有一个编译选项, -fpermissive,可以对不某些不符合标准的行为,允许编译通过。 即对标准做降级处理:
示例 | 编译选项 | 结果 |
---|---|---|
2 | g++ -Wall -std=c++11 -fpermissive -O2 -o a.out main.cpp | f(double) |
3 | g++ -Wall -std=c++11 -fpermissive -O2 -o a.out main.cpp | f(int) |
标准为啥这么定义?
可能有人觉得Visual C++
对于Non-dependent Name的推后查询更加合理,因为越往后,编译器就越有可能找到更匹配的对象,但是标准为什么不允许这种看似更优的行为呢?答案是不能让模板的特化出现不一致的定义。也就是说,对于一个特定的模板特化(一致的模板参数), 在整个程序中它的定义必须一致(ODR), 考虑如下的情形:
//示例4
//main.cpp
#include <iostream>
void f(double) { std::cout << "f(double)" << std::endl; }
template<typename T>
void g(T t)
{
f(1);
};
void func1()
{
g(1); //第一次调用模板函数,参数类型'int'
}
void f(int) { std::cout << "f(int)" << std::endl; }
void func2()
{
g(1); //第二次调用模板函数,参数类型'int'
}
int main()
{
func1();
func2();
}
编译器 | 编译选项 | 结果 |
---|---|---|
gcc 4.9.3 | g++ -Wall -std=c++11 -O2 -o a.out main.cpp | f(double) f(double) |
visual c++ 19.00 | cl.exe main.cpp -o a.exe /MD | f(int) f(int) |
clang 3.7.0 | clang++ -Wall -std=c++11 -O2 -o a.out main.cpp | f(double) f(double) |
从结果来看, 两次调用, 似乎所有的编译器都成功生成了一致的模板特化。 但是像Visual C++
这样允许将查询范围推迟到模板实例化上下文的情况却有很大的潜在风险, 对于一个编译单元, 编译器可以保证只生产一份特化,但是假如多个编译单元都使用同样类型的实参, 生成同样类型的模板特化, 但这些编译单元却有不一样的调用上下文, 那么有可能这些生成的特化版本是不一致的,因为Non-dependent Name可能匹配到了不同的对象,这种行为编译器可能无法察觉,并导致无法预期的后果, 有鉴于此,标准要求Non-dependent Name在定义模板的时候就立即绑定似乎是减少这种’病态’行为的有效的预防手段。
当然还有第二个原因,就是规范不同的编译器实现,使它们对于一样的代码产生一样的行为,不过貌似现在微软的VC++似乎忽略了这一点。
对于dependent-name,由于模板定义的时候无法确认它们的具体身份,只能推迟到模板实例化的时候再考虑了。
Point of Instantiation
模板实例化就是生成模板特定实例的过程,对于函数模板生成的实例叫做模板的特化, 和用户显示特化的实例一样, 类似于一个普通函数, 因为此时模板参数已经确定了。需要注意的是, 编译器生成的模板的特化版本有时也叫“生成的特化(generated specialization)” 或者”隐式特化(implicit specialization)”, 以区别于程序中自定义的显式特化(explicit specialization) 或者叫 用户定义的特化(user-defined specialization)。
对于模板函数的特化,生成什么?又是在哪里生成的呢? 绝大部分C++的文章在这部分的讲解很模糊,甚至互相矛盾,这也造成了不同的编译器解读的不一样,因此有时候一段代码在某一编译器下可以顺利运行, 而在另一个编译器下面却无法编译。
下面给出了一些具体代码示例:
//示例5
//main.cpp
#include <iostream>
using namespace std;
void f(const char*)
{
cout << "f(const char*)\n";
}
template <class T>
void g(T a)
{
f(a);
}
int main()
{
g("hello"); //调用f(const char*)
return 0;
}
关于这段代码, f
显然是一个 dependent-name, 因此它的绑定需要推迟到模板实例化时。 通过上面的解释,我们知道对于Non-dependent name, 编译器会在模板定义上下文中搜索名称,但是对于dependent-name, 编译器仍然会遵循这一步骤,首先在模板定义上下文中搜索对象, 此时void f(const char*)
位于模板定义之前,因此这个函数很快会被绑定。
那么假如我们调换f
和 g
的位置,又会发生什么呢?理论上, f
是一个dependent-name, 因此编译器会在模板实例化的时候搜索,而我们调用模板函数g
发生在f被定义之后,因此应该不会发生绑定行为的改变。
//示例6
//main.cpp
#include <iostream>
using namespace std;
template <class T>
void g(T a) //调换f和g的位置
{
f(a);
}
void f(const char*) //定义f
{
cout << "f(const char*)\n";
}
int main()
{
g("hello"); //f已经被定义了
return 0;
}
下面是在3大编译器上的运行结果:
编译器 | 错误或正确运行时的输出信息 |
---|---|
gcc 4.9.3 | error: ‘f’ was not declared in this scope, and no declarations were found by argument-dependent lookup at the point of instantiation [-fpermissive] f(a); note: ‘void f(const char*)’ declared here, later in the translation unit void f(const char*) |
visual C++ 19.00 | f(const char*) |
clang 3.7.0 | call to function ‘f’ that is neither visible in the template definition nor found by argument-dependent lookup f(a); note: in instantiation of function template specialization ‘g’ requested here g(“hello”); note: ‘f’ should be declared prior to the call site void f(const char*) |
显然只有visual C++
成功的解析到模板之后定义的函数f
, 而gcc
和 clang
给出的错误信息基本一致, 那就是:
SN | Message |
---|---|
1 | Not visible in the template definition (模板定义上下文不可见) |
2 | Not found by Argument-dependent lookup(ADL) |
第一个错误信息现在应该相当明确了,模板定义的上下文显然还没有包含函数f
, 因为它定义在模板函数之后, 第二个错误信息是ADL并没有能够找到任何可以匹配的函数, 因为此时模板实参类型是 ‘const char*
‘, 并不是用户定义类型,因此根本没有执行ADL的查找,自然没有找到相应的函数。
根据以上信息,我们反推,假如ADL能够正确执行, 那是不是就能够找到函数f
呢?
//示例7
//main.cpp
#include <iostream>
using namespace std;
template <class T>
void g(T a)
{
f(a);
}
namespace MyNamespace
{
class X{};
void f(X) { cout << "f(X)\n"; }
}
int main()
{
g(MyNamespace::X{});
return 0;
}
编译器 | 输出 |
---|---|
gcc 4.9.3 | f(X) |
visual C++ 19.00 | f(X) |
clang 3.7.0 | f(X) |
此时, 三大编译器都正确的通过ADL找到了正确的函数。 由此,似乎可以确认的是, Name Binding的过程,始终会首先在模板定义上下文中搜索对象, 不管是对于dependent-name还是non-dependent name, 对于 dependent-name,还会通过ADL进行第二步的查找。 但是对于gcc
和clang
, 并不会直接在模板实例化的上下文查找对象, 而VC++
则将搜索范围扩展到了模板实例化上下文。
困惑1
我们可以理解,gcc 和 clang 不允许直接在模板实例化上下文中直接查找dependent name,道理和不允许在模板实例化上下文查找Non-dependent name是一样的,就是为了避免违反ODR,以免在不同的编译单元产生不一致的模板实例。但是标准为什么允许在模板实例化上下文中进行ADL查找呢?答案是我不知道, 因为如果为了避免歧义,不允许在模板实例化上下文中查找,那为什么又允许在模板实例化上下文中的ADL查找呢?这样不也一样会允许歧义吗?
//示例8
//ff.h
#include <iostream>
namespace N
{
class X {};
int g(X, int i);
}
template<typename T>
double ff(T t, double d)
{
return g(t, d);
}
//ff.cpp
#include "ff.h"
int N::g(X, int i) { std::cout << "g(X,int)" << std::endl; return i; }
double x1 = ff(N::X{}, 1.1);
//main.cpp
#include "ff.h"
namespace N
{
double g(X, double d) { std::cout << "g(X,double)" << std::endl; return d; }
}
auto x2 = ff(N::X{}, 2.2);
int main()
{
extern double x1;
std::cout<<"x1 = " << x1 << std::endl;
std::cout << "x2 = " << x2 << std::endl;
return 0;
}
虽然这段代码,最终所有的编译器都成功的绑定到int g(X, int i)
;但似乎这是编译器的功劳,而不是标准强制要求的规则。编译器有可能生成两份不一致的 template<N::X> double ff(N::X, double)
, 这个问题确实让人费解。当然这也说明减少模板代码对上下文的依赖是一条好的编程规范。
困惑2
虽然直接在模板实例化上下文中搜索函数声明是错误的,但是gcc编译器却认为下面的代码没有问题。
//示例9
//main.cpp
#include <iostream>
using namespace std;
template <typename T> void foo() {
T() + T();
}
namespace {
class A {};
}
void operator+(const A&, const A&)
{
cout << "operator+(const N::A&, const N::A&)" << endl;
}
int main()
{
foo<A>();
}
这里operator+
的定义在全局作用域, 而类型A
实在匿名的namespace,因此编译器是没有办法找到全局作用域里面的operator+
, 但是gcc
还是编译通过了。
困惑3
下面的代码在vc++
下可以编译通过:
//示例10
//main.cpp
#include <iostream>
using namespace std;
template <typename T> void foo() {
g(T());
}
namespace {
class A {};
}
int main()
{
foo<A>();
}
void g(A a)
{
cout << "g(A)" << endl;
}
这应该不能称为’困惑’, vc++
将绑定拖延到了最后链接的阶段, 或者说到链接的阶段才生成模板特化,此时所有的编译单元都是可见的了。因此将函数 g
定义到 main
函数之后,仍然有效。说到模板特化的代码在哪里,何时生成,这个没有统一的规定,有可能是在模板第一次被调用之时, 有可能在每个编译单元的最后, 也有可能是链接的阶段,也有可能是几种方法的混合。