你或許不了解的C++函數調用(1)


這篇博客名字起得可能太自大了,搞得自己像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相關的就想到這么多,以后有新的了解再跟大家分享!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM