這篇博客名字起得可能太自大了,搞得自己像C++大牛一樣,其實並非如此。C++有很多隱藏在語法之下的特性,使得用戶可以在不是特別了解的情況下簡單使用,這是非常好的一件事情。但是有時我們可能會突然間發現一個很有意思的現象,然后去查資料,最終學到了C++的一個特性。所以很可能每個人理解的C++都有很大不同,我只是從自己的角度去跟大家分享而已。
C++的函數調用相比於C的函數調用要復雜很多,這主要是由於函數重載、類、命名空間等特性造成的。
根據Stephan T. Lavavej的介紹,C++編譯器在解析一次函數調用的時候,要按照順序做以下事情(根據具體情況,有些步驟可能會跳過的):
1) 名字查找(Name Lookup)
2) 模板參數類型推導(Template Argument Deduction)
3) 重載決議(Overload Resolution)
4) 訪問控制(Access Control)
5) 動態綁定(Dynamic Binding)
本篇博客主要跟大家分享下自己對Name lookup的理解。
對於編譯器來說,完成一次函數調用之前,必須能夠先找到這個函數。在C中這個問題很簡單,就是函數調用點向上找函數聲明,如果能找到就匹配,如果找不到就報錯。在C++中有函數重載(Function Overload)和名字空間(Namespace)的概念,使得這個問題變得有些復雜,但非常有意思。
一、從一段程序講起
首先,問大家個問題,在C++程序中,我們經常這樣寫:
#include <iostream> int main() { std::cout << "Hello, Core C++!" << std::endl; }
請問:上面main函數中的語句使用了重載操作符<<,如果用普通函數調用的語法該怎么寫?
顯然,這個語句一共有兩次operator<<函數調用。那么這兩個operator<<函數調用是一樣的函數嗎?如果不是,區別在哪里?
OK,告訴大家答案吧,上面的代碼等價於這樣寫:
#include <iostream> int main() { operator<<(std::cout, "Hello, Core C++!"); std::cout.operator<<(std::endl); }
大家看出來了吧?第一次operator<<調用的是一個全局函數,而第二次調用的是一個成員函數。
如果再深入一些,std::endl到底是個什么東西?直覺上這就是用來換行的,可能就是一個\n。而事實上,std::endl是一個函數。為什么呢?我們先看看VC中std::endl的代碼:
template<class _Elem, class _Traits> inline basic_ostream<_Elem, _Traits>& __CLRCALL_OR_CDECL endl(basic_ostream<_Elem, _Traits>& _Ostr) { // insert newline and flush stream _Ostr.put(_Ostr.widen('\n')); _Ostr.flush(); return (_Ostr); }
std::endl是一個全局函數,接受一個basic_ostream參數_Ostr。函數內部做了兩件事情:一、調用_Ostr的put(const char*)成員函數,輸出\n;二、調用_Ostr的flush()函數。其中第二步保證了ostream立即刷新,這也就是std::cout<<”\n”和std::cout<<std::endl的區別。也就只有std::endl是個函數才能完成這樣的操作。
還是最開始的例子,如果寫成這樣:
#include <iostream> int main() { cout << "Hello, Core C++!" << endl; }
編譯器會提示“undeclared identifier”,因為我們沒有指定任何namespace,編譯器默認到全局命名空間中查找,相當於::cout << "Hello, Core C++!" << ::endl;,而程序中並沒有提供的cout和endl,因此找不到。這個大家應該都比較熟悉了。
再問大家一個問題:
operator<<(std::cout, "Hello, Core C++!");
為什么這個語句不寫成:
std::operator<<(std::cout, "Hello, Core C++!");
也能通過編譯呢?畢竟operator<<是在std名字空間里,全局名字空間里面並沒有,為什么沒有報錯呢?
二、Name Lookup的主要機制
這就要從C++標准中對於名字查找的描述說起了。C++中有三種主要名字查找機制:
a) 隱式名字查找(Unqualified name lookup);
b) 基於參數的名字查找(Argument-dependent name lookup,ADL);
c) 顯式名字查找(Qualified name lookup)。
顯然,如果變量和函數之前不寫任何名字空間,就是隱式名字查找,此時編譯器只會從當前命名空間和全局命名空間中查找;如果寫了名字空間,就是顯式名字查找,編譯器會忠實地按照指定的命名空間去查找。
最有意思的是基於參數的名字查找,簡稱ADL,也叫Koenig Lookup,這種名字查找方式是C++大牛Andrew Koenig發明的。具體來說,對於一個函數調用,如果沒有顯式地寫函數的名字空間,編譯器會根據函數的參數所在的名字空間里面去查找這個函數。最新的C++標准加強了這個規則,叫Pure ADL,也就是只到參數所在的名字空間里去查找,而不到其它名字空間里查找,這樣的好處是防止找到其它名字空間里具有相同簽名的函數,導致非常隱蔽的bug。
這就可以理解為什么
operator<<(std::cout, "Hello, Core C++!");
可以正常編譯了,因為函數中有std::cout這個參數,所以編譯器就會到std名字空間里去查找operator<<這個函數。
這個特點非常重要,否則C++中的操作符重載根本無法做到像現在如此簡潔。可以想象下,如果每次都要去指定操作符的命名空間,語法該有多丑!僅僅通過ADL,就可以看出Andrew Koenig對於C++的貢獻。
注意:
std::cout.operator<<(std::endl);
這個語句不能省略最前面的std::,這是因為C++中類本身也形成了一個名字空間(就是類名),也就是說std::cout.operator<<這個函數的名字空間是std:ostream,而不是std,而std::endl在std名字空間中,ADL是不會向下去查找嵌套的名字空間的的,只會在當前名字空間里去查找。因此最前面的std::不能省略。
三、名字空間污染
對已一開始的例子,可能很多人更喜歡寫成:
#include <iostream> using namespace std; int main() { cout << "Hello, Core C++!" << endl; }
這樣下面使用任何STL里面的類和算法的時候,都不用加上std::前綴了,這樣是方便,但是也是會帶來問題的。using namespace std;這個語句將std里面所有的東西(類、算法、對象等等)都引入到我當前的名字空間中,其中很多東西我是暫時使用不到的。如果我自己在當前名字空間中定義了一些和std中同名的東西的話,就會導致一些意想不到的問題:
#include <iostream> using namespace std; class Polluted { public: Polluted& operator<<(const char*) { return *this; } }; int main() { Polluted cout; cout << "Hello, Core C++!\n"; }
上面這個程序,看上去會輸入Hello, Core C++!,實際上卻什么都沒做。因為cout已經不是std::cout了,而是Polluted的一個對象,這個對象恰巧也有一個operator<<(const char*)函數。因為名字空間查找和普通變量的作用域一樣,局部名字空間會覆蓋全局名字空間和引入的名字空間,所以編譯器雖然兩個cout都找到,但根據局部優先於全局的規則,選用了main函數中定義的cout,而不是std::cout。
這樣的危害在於當程序規模比較大的時候,這樣的問題會變得很隱蔽,甚至測試都不一定能測試到,但是卻會引發非常奇怪的問題,給調試帶來非常大的麻煩。所以using namespace std;盡量少用,最多使用using std::cout,這樣就只引入std中的cout,其它東西都沒有引入,出問題的概率小些,但問題依舊存在,所以如果可能的話,盡量將std::都加上,保證不出問題。
四、using在STL中的使用
2005年,C++對STL進行了擴充,就是所謂的TR1(Technical Report 1),里面加入了很多實用的庫,如shard_ptr、function、bind、regular exprestion等等,它們都位於std::tr1名字空間下。到了C++11,TR1中的很多庫得到了升級,正式成為std名字空間中的一員。但是之前很多代碼已經用了std::tr1,為了確保已有的代碼不被破壞,並且不要重復定義相同的東西。STL采取這樣的方式:將原來std::tr1中的定義移到std中,然后在std::tr1中使用using指令將庫引入到std::tr1中。如VC中有這樣的代碼:
namespace tr1 { // TR1 additions using _STD allocate_shared; using _STD bad_weak_ptr; using _STD const_pointer_cast; using _STD dynamic_pointer_cast; using _STD enable_shared_from_this; using _STD get_deleter; using _STD make_shared; using _STD shared_ptr; using _STD static_pointer_cast; using _STD swap; using _STD weak_ptr; } // namespace tr1
這樣就達到了兼顧新標准和已有代碼的目標。
五、名字空間別名
如果我們有一個很深的名字空間,比如A::B::C::D::E,並且經常會用到這里面的類和函數,我們不希望每次都敲這么長的前綴,當然也不希望通過using namespace A::B::C::D::E來污染名字空間,C++提供了名字空間別名的方式來簡化使用。比如,我們可以通過
namespace ABCDE = A::B::C::D::E;
產生名字空間別名ABCDE,ABCDE::ClassT就等價於A::B::C::D::E::ClassT。
C++11中,這種方式的別名得到了擴展,不僅僅用於名字空間,可以用於任何別名:
using ABCDE = A::B::C::D::E; using ABCDE_ClassT = ABCDE::ClassT;
這樣的語法基本上可以替代typedef了,而且語法更簡潔。
OK,關於Name lookup相關的就想到這么多,以后有新的了解再跟大家分享!