c++11-17 模板核心知識(十三)—— 名稱查詢與ADL
阿新 • • 發佈:2020-12-06
- [名稱分類](#名稱分類)
- [名稱查詢](#名稱查詢)
- [ordinary lookup](#ordinary-lookup)
- [ADL (Argument-Dependent Lookup)](#adl-argument-dependent-lookup)
- [官網的例子](#官網的例子)
- [ADL的缺點](#adl的缺點)
在C++中,如果編譯器遇到一個名稱,它會尋找這個名稱代表什麼。比如x*y,如果x和y是變數的名稱,那麼就是乘法。如果x是一個型別的名稱,那麼就聲明瞭一個指標。
C++是一個`context-sensitive`的語言 : 必須知道上下文才能知道表示式的意義。那麼這個和模板的關係是什麼呢?構造一個模板必須知道幾個上下文:
* 模板出現的上下文
* 模板被例項化的上下文
* 例項化模板引數的上下文
## 名稱分類
引入兩個重要的概念:
* qualified name : 一個名稱所屬的作用域**被顯式的指明**,例如`::`、`->`或者`.`。`this->count`就是一個qualified name,但count不是,因為它的作用域沒有被顯示的指明,即使它和`this->count`是等價的。
* dependent name:依賴於模板引數。例如:`std::vector::iterator`. 但假如T是一個已知型別的別名(using T = int),那麼就不是dependent name.
![image](https://user-images.githubusercontent.com/14103319/101244818-7a5a3200-3743-11eb-8f54-129ea8f73ea1.png)
## 名稱查詢
名稱查詢有很多細節,這裡我們只關注幾個主要的點。
### ordinary lookup
對於qualified name來說,會有顯示指明的作用域。如果作用域是一個類,那麼基類也會被考慮在內,但是類的外圍作用域不會被考慮:
```c++
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** :
```c++
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
```
這個例子也很符合直覺。
但是下面這個例子就沒那麼正常:
```c++
template
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)。**在[函式呼叫表示式](https://en.cppreference.com/w/cpp/language/operator_other)中(f(a1, a2, a3, ... ),包含隱式的呼叫過載operator,例如 << ),ADL應用一系列的規則來查詢`unqualified function names`。**
ADL會將函式表示式中實參的`associated namespaces`和`associated classes`加入到查詢範圍,這也就是為什麼叫**Argument-Dependent** Lookup. 例如:某一型別是指向class X的指標,那麼它的`associated namespaces`和`associated classes`會包含X和X所屬的任何class和namespace.
對於給定的型別,`associated classes`和`associated namespaces`按照一定的規則來定義,大家可以看下[官網Argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl),實在有點多,不寫在這裡了。理解為什麼需要ADL、什麼時候應用到ADL時,按照對應的場景再去查就行~
額外需要注意的一點是,ADL會忽略using :
```c++
#include
namespace X {
template 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()不會被考慮。
### 官網的例子
看下[官網](https://en.cppreference.com/w/cpp/language/adl)的例子幫助理解:
```c++
#include
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。
再來一個:
```c++
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](https://blog.csdn.net/guangcheng0312q/article/details/103750458)中的例子,只看swap就行,類的其他函式可以略過:
```c++
#include
namespace A {
template
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
void swap(A::smart_ptr &lhs, A::smart_ptr &rhs) noexcept {
lhs.swap(rhs);
}
}
// 開啟這個註釋,會引發ADL衝突
//namespace std {
// // 提供一個非成員swap函式for ADL(Argument Dependent Lookup)
// template
// void swap(A::smart_ptr &lhs, A::smart_ptr &rhs) noexcept {
// lhs.swap(rhs);
// }
//
//}
int main() {
using std::swap;
A::smart_ptr 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](https://user-images.githubusercontent.com/14103319/92430320-11583200-f1c7-11ea-940d-1e297d2f394