c++11-17 模板核心知識(十三)—— 名稱查詢與ADL

張雅宸發表於2020-12-06

在C++中,如果編譯器遇到一個名稱,它會尋找這個名稱代表什麼。比如x*y,如果x和y是變數的名稱,那麼就是乘法。如果x是一個型別的名稱,那麼就宣告瞭一個指標。

C++是一個context-sensitive的語言 : 必須知道上下文才能知道表示式的意義。那麼這個和模板的關係是什麼呢?構造一個模板必須知道幾個上下文:

  • 模板出現的上下文
  • 模板被例項化的上下文
  • 例項化模板引數的上下文

名稱分類

引入兩個重要的概念:

  • qualified name : 一個名稱所屬的作用域被顯式的指明,例如::->或者.this->count就是一個qualified name,但count不是,因為它的作用域沒有被顯示的指明,即使它和this->count是等價的。
  • dependent name:依賴於模板引數。例如:std::vector<T>::iterator. 但假如T是一個已知型別的別名(using T = int),那麼就不是dependent name.

image

名稱查詢

名稱查詢有很多細節,這裡我們只關注幾個主要的點。

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)。函式呼叫表示式中(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,實在有點多,不寫在這裡了。理解為什麼需要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()不會被考慮。

官網的例子

看下官網的例子幫助理解:

#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中的例子,只看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;
}

(完)

朋友們可以關注下我的公眾號,獲得最及時的更新:

image

相關文章