c++11-17 模板核心知识(十三)—— 名称查找与ADL

  • 名称分类
  • 名称查找
    • ordinary lookup
    • ADL (Argument-Dependent Lookup)
    • 官网的例子
    • ADL 的缺点

在 C++中,如果编译器遇到一个名称,它会寻找这个名称代表什么。比如 x*y,如果 x 和 y 是变量的名称,那么就是乘法。如果 x 是一个类型的名称,那么就声明了一个指针。

C++是一个context-sensitive的语言 : 必须知道上下文才能知道表达式的意义。那么这个和模板的关系是什么呢?构造一个模板必须知道几个上下文:

  • 模板出现的上下文
  • 模板被实例化的上下文
  • 实例化模板参数的上下文

名称分类

引入两个重要的概念:

  • qualified name : 一个名称所属的作用域被显式的指明,例如::->或者.this->count就是一个 qualified name,但 count 不是,因为它的作用域没有被显示的指明,即使它和this->count是等价的。
  • dependent name:依赖于模板参数的名称,也就是访问运算符左面的表达式类型依赖于模板参数。例如:std::vector<T>::iterator 是一个 Dependent Name,但假如 T 是一个已知类型的别名(using T = int),那就不是 Dependent Name。

名称查找

名称查找有很多细节,这里我们只关注几个主要的点。

ordinary lookup

对于 qualified name 来说,会有显示指明的作用域。如果作用域是一个类,那么基类也会被考虑在内,但是类的外围作用域不会被考虑:

int x;

class B {
public:
  int i;
};

class D : public B {};

void f(D *pd) {
  pd->i = 3;    // finds B::i
  D::x = 2;      // ERROR: does not find ::x in the enclosing scope
}

这点很符合直觉。

相反,对于非 qualified name 来说,会在外围作用域逐层查找(假如在类成员函数中,会先找本类和基类的作用域)。这叫做 ordinary lookup :

extern int count;             // #1

int lookup_example(int count) // #2
{
  if (count < 0) {
    int count = 1;           // #3
    lookup_example(count);   // unqualified count refers to #3
  }
  return count + ::count;      // the first (unqualified) count refers to #2 ;
}                              // the second (qualified) count refers to #1

这个例子也很符合直觉。

但是下面这个例子就没那么正常:

template<typename T>
T max (T a, T b) {
    return b < a ? a : b;
}

namespace BigMath {
  class BigNumber {
    ...
};

  bool operator < (BigNumber const&, BigNumber const&);
  ...
}

using BigMath::BigNumber;

void g (BigNumber const& a, BigNumber const& b) {
  ...
  BigNumber x = ::max(a,b);
  ...
}

这里的问题是:当调用 max 时,ordinary lookup不会找到 BigNumber 的operator <。如果没有一些特殊规则,那么在 C++ namespace 场景中,会极大的限制模板的适应性。ADL 就是这个特殊规则,用来解决此类的问题。

ADL (Argument-Dependent Lookup)

ADL 出现在 C++98/C++03 中,也被叫做 Koenig lookup,应用在非 qualified name 上(下文简称 unqualified name)。在函数调用表达式[1]中(f(a1, a2, a3, ... ),包含隐式的调用重载 operator,例如 << ),ADL 应用一系列的规则来查找unqualified function names

ADL 会将函数表达式中实参的associated namespacesassociated classes加入到查找范围,这也就是为什么叫Argument-Dependent Lookup. 例如:某一类型是指向 class X 的指针,那么它的associated namespacesassociated classes会包含 X 和 X 所属的任何 class 和 namespace.

对于给定的类型,associated classesassociated namespaces按照一定的规则来定义,大家可以看下官网 Argument-dependent lookup[2],实在有点多,不写在这里了。理解为什么需要 ADL、什么时候应用到 ADL 时,按照对应的场景再去查就行~

额外需要注意的一点是,ADL 会忽略 using :

#include <iostream>

namespace X {
  template <typename T> void f(T);
}

namespace N {
  using namespace X;
  enum E { e1 };
  void f(E) { std::cout << "N::f(N::E) called\n"; }
}    // namespace N

void f(int) { std::cout << "::f(int) called\n"; }

int main() {
  ::f(N::e1);    // qualified function name: no ADL
  f(N::e1);     // ordinary lookup finds ::f() and ADL finds N::f(), the latter is preferred
}

namespace N中的using namespace X会被 ADL 忽略,所以在 main 函数中,X::f()不会被考虑。

官网的例子

看下官网[3]的例子帮助理解:

#include <iostream>
int main() {
    std::cout << "Test\n"; // There is no operator<< in global namespace, but ADL
                           // examines std namespace because the left argument is in
                           // std and finds std::operator<<(std::ostream&, const char*)
    operator<<(std::cout, "Test\n"); // same, using function call notation

    // however,
    std::cout << endl; // Error: 'endl' is not declared in this namespace.
                       // This is not a function call to endl(), so ADL does not apply

    endl(std::cout); // OK: this is a function call: ADL examines std namespace
                     // because the argument of endl is in std, and finds std::endl

    (endl)(std::cout); // Error: 'endl' is not declared in this namespace.
                       // The sub-expression (endl) is not a function call expression
}

注意最后一点(endl)(std::cout);,如果函数的名字被括号包起来了,那也不会应用 ADL。

再来一个:

namespace A {
      struct X;
      struct Y;
      void f(int);
      void g(X);
}

namespace B {
    void f(int i) {
        f(i);      // calls B::f (endless recursion)
    }
    void g(A::X x) {
        g(x);   // Error: ambiguous between B::g (ordinary lookup)
                //        and A::g (argument-dependent lookup)
    }
    void h(A::Y y) {
        h(y);   // calls B::h (endless recursion): ADL examines the A namespace
                // but finds no A::h, so only B::h from ordinary lookup is used
    }
}

这个比较好理解,不解释了。

ADL 的缺点

依赖 ADL 有可能会导致语义问题,这也是为什么有的时候需要在函数前面加::,或者一般推荐使用 xxx::func,而不是 using namespace xxx 。因为前者是 qualified name,没有 ADL 的过程。

引用现代 C++之 ADL[4]中的例子,只看 swap 就行,类的其他函数可以略过:

#include <iostream>

namespace A {
    template<typename T>
    class smart_ptr {
    public:
        smart_ptr() noexcept : ptr_(nullptr) {

        }

        smart_ptr(const T &ptr) noexcept : ptr_(new T(ptr)) {

        }

        smart_ptr(smart_ptr &rhs) noexcept {
            ptr_ = rhs.release();       // 释放所有权,此时rhs的ptr_指针为nullptr
        }

        smart_ptr &operator=(smart_ptr rhs) noexcept {
            swap(rhs);
            return *this;
        }

        void swap(smart_ptr &rhs) noexcept { // noexcept == throw() 保证不抛出异常
            using std::swap;
            swap(ptr_, rhs.ptr_);
        }

        T *release() noexcept {
            T *ptr = ptr_;
            ptr_ = nullptr;
            return ptr;
        }

        T *get() const noexcept {
            return ptr_;
        }

    private:
        T *ptr_;
    };

// 提供一个非成员swap函数for ADL(Argument Dependent Lookup)
    template<typename T>
    void swap(A::smart_ptr<T> &lhs, A::smart_ptr<T> &rhs) noexcept {
        lhs.swap(rhs);
    }
}

// 开启这个注释,会引发ADL冲突
//namespace std {
//    // 提供一个非成员swap函数for ADL(Argument Dependent Lookup)
//    template<typename T>
//    void swap(A::smart_ptr<T> &lhs, A::smart_ptr<T> &rhs) noexcept {
//        lhs.swap(rhs);
//    }
//
//}

int main() {

    using std::swap;
    A::smart_ptr<std::string> s1("hello"), s2("world");
    // 交换前
    std::cout << *s1.get() << " " << *s2.get() << std::endl;
    swap(s1, s2);      // 这里swap 能够通过Koenig搜索或者说ADL根据s1与s2的命名空间来查找swap函数
    // 交换后
    std::cout << *s1.get() << " " << *s2.get() << std::endl;
}

(完)

参考资料

[1]

函数调用表达式: en.cppreference.com/w/c

[2]

官网 Argument-dependent lookup: en.cppreference.com/w/c

[3]

官网: en.cppreference.com/w/c

[4]

现代 C++之 ADL: blog.csdn.net/guangchen

编辑于 2022-07-20 18:59