Item 5: 比起顯式的類型聲明,更偏愛auto

本文翻譯自《effective modern C++》,由於水平有限,故無法保證翻譯完全正確,歡迎指出錯誤。謝謝!

啊,簡單愉快的代碼:

inr x;

等等,討厭!我忘了初始化x,所以它的值是不確定的。可能,它可能被初始化成了0,這取決於你的編譯環境。哎。

不要緊,讓我們簡單並愉快地聲明一個局部變量,通過解引用一個iterator來初始化它:

template<typename It>
void dwim(It b, It e)
{
    while(b != e){
        typename std::iterator_traits<It>::value_type
          currValue = *b;
        ...
    }
}

呸,真的要用“typename std::iterator_traits::value_type”表示iterator指向的值的類型嗎?我已經忘了這有多愉快了,等等,我之前真的說過這愉快嗎?

好的,再看一個簡單愉快的例子(第三個了):愉快地聲明一個局部變量,讓他的類型是一個閉包。噢,對的,閉包的類型只有編譯器知道,因此不能被寫出來,哎,討厭。

討厭,討厭,討厭!C++編程,並不是一段愉快的經歷(它本應該是愉快的)。

好的吧,它曾經不是。但是在C++11中,由於auto提供的好意,所有的這些麻煩都離去了。auto變量從初始化表達式中推導它們的類型,所以它們必須被初始化。這意味着,當你快速行駛在現代C++的超級高速公路上時,你能和變量未初始化問題揮手再見了:

int x1;         //可能未初始化

auto x2;        //錯誤!需要初始化表達式

auto x3 = 0;    //好的,x的值是良好定義的

通過對iterator解引用來聲明局部變量時,這個高速公路沒有之前那樣的困難:

template<typename It>   //和之前一樣
void dwim(It b, It e)
{
    while(b != e){
        auto currValue =*b;
        ...
    }
}

並且,因爲auto使用類型推導(看 Item 2),它能表示只有編譯器知道的類型:

auto derefUPLess =
    [](const std::unique_ptr<Widget>& p1,
       const std::unique_ptr<Widget>& p2)
    { return *p1 < *p2; };

非常酷,在C++14中,溫度進一步下降(事情變得更簡單),因爲lambda表達式的參數可以涉及auto:

auto derefLess =
    [](const auto& p1,
      (const auto& p2)
    { return *p1 < *p2; };

雖然很酷,你可能覺得我們不需要使用auto來聲明一個變量來包含閉包,因爲我們可以使用std::function對象。這是對的,我們能這麼做,但是事情並不是你想的那樣。有的讀者可能在想“什麼是std::function對象” ,那就讓我們先來理清這個對象把。

std::function是C++11標準庫中的模板,這個模板擴張了函數指針的概念。函數指針只能指向函數,但是,std::function對象能指向所有可調用對象。也就是,所有能像函數一樣用“()”調用的東西。就像你創建函數指針的時候,必須明確指向函數的類型(也就是你想指向的函數的簽名),當你傳入std::function對象時,你必須明確你引用的函數的類型。你通過std::function模板的參數來做到這一點。舉個例子,爲了聲明一個能調用任何下面函數簽名的std::function對象,

bool(const std::unique_ptr<Widget>&,
     const std::unique_ptr<Widget>&)

你會這麼寫:

std::function<bool(const std::unique_ptr<Widget>&,
              const std::unique_ptr<Widget>&)> func;

因爲lambda表達式產生一個可調用對象,所以閉包能被存放在std::function對象中。這意味着我們能不使用auto就聲明一個C++11版本的derefUpLess:

std::function<bool(const std::unique_ptr<Widget>&,
              const std::unique_ptr<Widget>&)>
    derefUpLess = [](const std::unique_ptr<Widget>& p1,
                     const std::unique_ptr<Widget>& p2)
    { return *p1 < *p2; };

你要知道下面的要點,就算把這冗長的語法和重複的變量類型放在一邊,使用std::function和使用auto也不是一樣的。一個用auto聲明的變量存放和閉包同樣類型的閉包,並且只使用和閉包所要求的內存一樣多的內存。std::function聲明的變量存放了一個閉包,這個閉包是std::function模板的實例,並且他對任何給出的簽名都需要調整大小。這個大小可能不夠一個閉包來存儲,當遇到這樣的情況時,std::function的構造函數將申請堆內存來存儲閉包。結果就是相比auto聲明的對象,std::function對象通常要使用更多的內存。再考慮下實現的細節,由於inline函數的限制,間接函數的調用,調用閉包時,通過std::function對象調用總是比通過auto聲明的對象調用要慢。換句話說,std::function方法通常比auto方法更大更慢,並且可能產生內存溢出異常。更好的是,就像你在上面的例子中看到的那樣,寫”auto“要做的工作完全少於寫std::function實例類型。在auto和存儲閉包的std::function對比中,auto完勝了。(相似的爭論會出現在std::bind函數的返回值的存儲中,同樣可以使用auto或std::function,但是在Item 34中,我盡全力來讓你信服,無論如何使用lambdas表達式來代替std::bind)

auto的優點不侷限於避免未初始化的變量,冗長的變量聲明和直接閉包的聲明。它的另外一個能力就是能避免我們由於使用“類型快捷方式”(“type shortcuts”)而造成問題。這裏給出一些你看起來會做的事,你可能會這麼寫:

std::vector<int> v;
...
unsigned sz = v.size();

v.size()的官方返回類型是std::vector::size_type,但是很少有開發者意識到這點。std::vector::size_type常被指定爲無符號整形,所以很多開發者認爲unsigned夠用了,並寫代碼時也會像上面這麼寫。這會造成一些有趣的結果。舉個例子,在32位的Windows下,unsigned和std::vector::size_type大小是一樣的。但是64位Windows下,unsigned是32位的,而std::vector::size_type是64位的。這意味着工作在32位Windows和工作在64位Windows下會表現的不一樣,當把你的程序從32位移植到64下時,沒有人希望遇到這樣的問題。

使用auto能保證你不會遇到這樣的問題:

auto sz = v.size(); //sz的類型是std::vector<int>::size_type

難道你還不肯定使用auto的智慧?那再看看這代碼:

std::unordered_map<std::string, int> m;
...

for(const std::pair<std::string, int>& p : m)
{
    ... //用p做些事
}

這看起來很有道理,但是這裏存在一個問題,你看到了嗎?

想想看什麼需要記住的東西遺漏了,std::unordered_map的鍵的部分是const的,所以在hash table(std::unordered_map的存儲類型)中的std::pair的類型不是std::pair

            你要記住的事
  • 比起顯式類型聲明,auto變量必須被初始化,它常常免於由類型不匹配造成的可移植性和效率的問題,它能簡化程序的重構,並且通常只需要寫的更少的代碼。

  • auto類型變量的陷阱在Item 2和Item 6中討論。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章