ADL 及其更多

C++ 有一些很诡异的特性,比如说查找函数名称。我们都知道 nested namespace 里面可以直接用外层定义的函数,但是有意思的是,如果出现重载,反而会挂掉:

namespace a {
  void foo(int a) {
  }

  namespace b {
    void foo(const std::string& a) {
    }

    void bar() {
      foo(1);
    }
  }
}

这里 bar 调用会失败,这告诉我们在一个 namespace 里面如果恰好碰到了同名函数,重载检查仅仅在该 namespace 里面进行检查,也就是说 a::b::foo 不会因为 a::foo 的存在改变自己的行为。不过这也很 surprising,因为如果 a::foo 先存在,然后就会有 a::b 里面的 code 调用,如果不是写 a::foo 的话当 a::b::foo 被添加进来的时候就会 break。

关于函数的查询更有意思的是 ADL(又名 Koenig lookup),ADL 是很多调用能够得以简化的重要策略。一个时常被人提起的例子就是 operator<<,这通常被用来将某个自己定义的对象输出到流里面,或者调试、记录 log。比如我们在 namespace a 里面定义了自己的类 foo,作为“附带的功能”(后面继续讨论这个意思到底是什么),我们可以在 namespace a 里面定义如下 code

namespace a {
  class foo {};

  std::ostream&
  operator<< (std::ostream& out, const foo& f) {
    out << foo.something();
    return out;
  }
}

namespace b {
  void func() {
    a::foo f;
    std::cout << f << std::endl;
  }
}

这个有意思的地方在于 func 明明在另一个 namespace 里面,这里面并没有定义如何将 f 输出到 std::cout,事实上 C++ 编译器能准确的调用前面的 operator<<。这就是所谓的 ADL(augument dependent lookup),也就是说编译器看到 operator<< 时,两个参数类型分别为 std::ostream& 和 const foo&,它就会在这两个 argument 所在的 namespace 里面寻找(更多的话类似),这个寻找是一个“同时进行”的,如果有一个特别 match,那么就会抛弃其他的 match,如果 match 程度相当就会报 ambiguity 而编译失败。怎么理解这个过程呢?我们以 std::swap 为例,这个函数的语义是表示交换两个对象的内容,对于 default constructible 和 assignable 的对象,我们知道存在一个通用的形式,这也就是 std::swap 定义的一个模板,当然它还拥有一些特化的 case。我们假定在 namespace a 中定义了类 foo,我们发现它使用 STL 的实现性能比较差,我们想为其提供一个特化,这样 namespace b 中使用它相关的代码可以比较高效的完成这个过程,那么应该如何写相关的 code 呢?我曾经很 sb 的认为应该打开 std 把特化放在那里面,其实这是错误的。

我们先看看下面的代码:

namespace a {
  class foo {};

  void swap (foo& x, foo& y) {
    // my implementation
  } 
}

namespace b {
  void func (a::foo& x, a::foo& y) {
    using std::swap;
    swap(x, y);
  }
}

最有意思的莫过于 func 里面那句 using,看起来似乎是说咱这个地方要用 std::swap,但是其实事情却不是这样:

  • using 将 std::swap 引入到 func 的 scope 里面
  • 如果我们没有定义 a::swap,那么这时候就会调用 STL 提供的原始 swap
  • 然而一旦提供了 a::swap,这时候对 swap 的搜索除了有 std::swap 这个候选以外,还存在 a::swap,因为前者仅仅是一个模板,比 match 程度的话后者要强

更有意思的是,如果 argument 是模板类,这个搜索还会进入模板参数所在的 namespace。为什么不主张重新打开 std 把特化放在那个命名空间呢?我们要借助一下 Sutter 大牛很久以前的一篇文章所讨论的 interface principle:

  • 什么是 class?一组数据以及对这组数据进行的操作集合
  • 因此一个 class foo 的 interface 包括那些 mention foo 和被 foo 提供出来的函数
  • 本质上 free function 和 member function 并没有区别:因为后者完全就是 syntactic sugar,编译器 implicitly passing this pointer 而已

从这点上看将 a::foo 相关的函数放在 namespace a 里面才是更合适的做法。有的时候存在一些选择:一个函数究竟应该作为成员函数提供、还是 free function 呢?其实从 ADL 来看,如果是表达所提供的类 foo 与其他的对象的关系,free function 要更为方便、应用面要更广一些,因为成员函数意味着“第一个”参数只能是 foo。

——————
And as he passed over Penuel the sun rose upon him, and he halted upon his thigh

Advertisements
ADL 及其更多

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s