激进的ADL(Argument-dependent lookup)

ADL是Argument-dependent lookup的简写,中文译作参数依赖查找。ADL对于避免冗长的代码很有用处,但是也会造成一些歧义。

如果在使用函数的上下文中找不到函数定义,则我们可以在其参数的命名空间(namespace)中查找它。这意味着,当函数被调用时,即使其声明并不在当前作用域内,只要它是某个实参所在的名字空间中声明的,编译器就能查找到它。

1
2
3
4
5
6
7
8
9
10
11
namespace Chrono{
class Date{/*...*/};
bool operator==(const Date&,const std::string&);
std::string format(const Date&)
// ....
}

void f(Chrono::Date d,int i){
std::string s=format(d); //Chrono::format
std::string t=format(i); // error,no matching function
}

当一个类成员调用一个命名函数时,编译器会优先选择同一个类的其他成员及其基类的而不是基于参数类型查找到的函数(注意:运算符依据不同的规则(实参匹配))。

Let X be the lookup set produced by unqualified lookup (3.4.1) and let Y be the lookup set produced by
argument dependent lookup (defined as follows). If X contains:

  • a declaration of a class member, or
  • a block-scope function declaration that is not a using-declaration, or
  • a declaration that is neither a function or a function template

then Y is empty. Otherwise Y is the set of declarations found in the namespaces associated with the
argument types as described below. The set of declarations found by the lookup of the name is the union of X and Y . [ Note: The namespaces and classes associated with the argument types can include namespaces and classes already considered by the ordinary unqualified lookup.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace NS {
class T { };
void f(T){
cout<<"NS::F(T)"<<endl;
}
void g(T, int){
cout<<"NS::g(T,int)"<<endl;
}
}
NS::T parm;
void g(NS::T, float){
cout<<"::g(NS::T,float)"<<endl;
}
int main() {
f(parm); // OK: calls NS::f
g(parm, 1); // OK: calls NS::g(T,int)
extern void g(NS::T, float);
g(parm, 1); // OK: calls g(NS::T, float)
}

// run resault
NS::F(T)
NS::g(T,int)
::g(NS::T,float)

更多关于ADL的介绍可以查阅ISO/IEC 14882:2014(E) §3.4.2 P47,也可以直接在这里查看。

下面要讲到另外一种情况,在我们不希望使用ADL时候的造成的歧义。

考虑下面这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace A{
class base{
int x;
float y;
};
void copy(base *x,base* y){
std::cout<<"A::copy"<<std::endl;
}
}
namespace B{
void copy(A::base* x,A::base* y){
std::cout<<"A::copy"<<std::endl;
}
void algo(A::base& x,A::base& y){
// 调用哪一个copy函数?
copy(&x,&y);
}
}

int main(){
A::base x,y;
B::algo(x,y);
}

实际上上面的代码会有一个编译错误(error: call to 'copy' is ambiguous)。

因为ADL的原因,B::algo在其参数的名字空间(A)中也找到了完全匹配的函数A::copy,造成了歧义。

在写代码时一定要注意这样的情况,最好在调用接收不同名字空间中的实参时,在函数定义内使用using或者::显式指定我们希望调用的版本。

1
2
3
4
5
6
7
8
9
10
void algo(A::base& x,A::base& y){
// using A::copy or B::copy;
using A::copy;
copy(&x,&y,false);
B::copy(&x,&y,false);
}

// run resault
A::copy()
B::copy()

尤其是在未受限模板组合中使用,因为我们不知道传递进来的模板参数所在的名字空间中是否有我们当前需要用到的函数,所以一定要保持一个良好的编码习惯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void g(int){
cout<<"::g(int)"<<endl;
}
struct A{
void g(char){
cout<<"A::g(char)"<<endl;
}
void h(char){
cout<<"A::h(char)"<<endl;
}
};

template<typename T>
class B:public T{
public:
void f(){
// 调用哪个g()?
g(2);
}
};

void f(B<A> x){
x.f();
}

上面的代码中对B<A>的对象调用函数成员f内调用的g是::g(int),而不是从A中继承来的A::g(char),因为g(2)不依赖于模板参数T,因此它在定义点进行绑定。来自模板实参T(恰好用作基类)的名字此时还未被编译器所致,因此不会被考虑。
只有在实例化点我们才能知道参数T的实参是否具有所要求的名字。

注意实例化点绑定定义点绑定的区别。

  • 实例化点绑定:确定依赖性名字含义所需的上下文由模板的使用(给定一组实参)决定。对一个模板类或者一个类成员而言,实例化点恰好位于包含其使用的声明之前。但为了允许递归调用,函数模板的实例化点位于实例化它的声明之后
  • 定义点绑定:编译器将不依赖模板实参的名字当做模板外的名字一样处理。因此,在定义点位置这种名字必须在作用域内。

如果我们希望来自一个依赖性的名字被考虑,就必须明确依赖关系。有三种方式可以实现这个目的:

  • 用依赖性类型(T::g)限定名字
  • 声明一个名字指向此类的一个对象(this->g)
  • 用using声明将名字引入作用域(using T::g)

如果想要关闭ADL,则可以使用以下两种方式:

  • 显式通过::指定(限定调用namespaceName::funcName())
  • 将函数名放入括号中((funcName)())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace TEST{
struct A{};
void func(const A& x){
cout<<"fuck"<<endl;
}
}

int main()
{
TEST::A x;
func(x); // call TEST::func
(func)(x); // error: use of undeclared identifier 'func';
}

全文完,若有不足之处请评论指正。

微信扫描二维码,关注我的公众号。

本文标题:激进的ADL(Argument-dependent lookup)
文章作者:查利鹏
发布时间:2016年11月29日 22时39分
本文字数:本文一共有1.7k字
原始链接:https://imzlp.com/posts/25788/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!