動態

詳情 返回 返回

學懂現代C++——《Effective Modern C++》之類型推導和auto - 動態 詳情

前言

之前分享過Scott Meyers的兩本書《Effective C++》和《More Effective C++》。這兩本書對我深入學習C++有着很大的幫助,建議所有想進階C++技術的同學都可以看看。但是,這兩本書是大神Scott在C++11之前出的,而C++11對於C++社區來説是一次重大的變革,被稱為現代C++,用以區分C++11之前的傳統C++。

好在Scott在之後也帶來了全新的《Effective Modern C++》,其中也是延續了作者一貫的風格和質量。帶來了42個獨家技巧,助你改善C++11和C++14的高效用法(封面語)。

本文首先就帶同學們一起看看這本書的前兩章——類型推導和auto。

首先申明本文只是做知識點的總結,書中有更詳細的推導和講解過程,感興趣的同學還是強烈建議大家去讀原書。

類型推導

條款1:理解模板類型推導

模板類型推導是C++11應用最廣泛的特性之一——auto的基礎。所以,理解auto的推導規則和正確使用方式的基礎就是理解模板類型推導的規則。

先來看看模板和其調用的一般形式。

template<typename T>
void f(ParamType param);

f(expr);

這裏需要T的類型推導結果,依賴於expr的類型和ParamType的形式。其中,ParamType的形式需要分三種情況討論。

  • 情況1:ParamType是個指針或者引用,但不是萬能引用(形式如T&&)。

在這種情況下,模板類型推導具有以下規則:

  1. expr的引用屬性會被忽略。
  2. 忽略expr的引用性後,expr的類型和ParamType的類型進行模式匹配,由此決定T的類型。

舉個例子:

// 聲明模板
template<typename T>
void f(T& param);

// 聲明變量
int a = 1;
const int ca = a;
const int& cra = a;

//調用模板
f(a);   //a的類型是int,T的類型是int,param的類型是int&。
f(ca);  //ca的類型是const int,T的類型是const int,param的類型是const int&。
f(cra); //cra的類型是const int&,T的類型是const int,param的類型是const int&。
要點1:在模板類型推導過程中,具有引用類型的實參會被當成非引用類型來處理。
  • 情況2:ParamType是個萬能引用。

在這種情況下,模板類型推導規則如下:

  1. 如果expr是個左值,則T和ParamType都會被推到為左值引用。
  2. 如果expr是個右值,則和情況1中的推導規則相同。

舉個同情況1類似的例子:

// 聲明模板
template<typename T>
void f(T&& param);

// 聲明變量
int a = 1;
const int ca = a;
const int& cra = a;

//調用模板
f(a);   //a是左值,類型是int,T的類型是int&,param的類型是int&。
f(ca);  //ca是左值,類型是const int,T的類型是const int&,param的類型是const int&。
f(cra); //cra是左值,類型是const int&,T的類型是const int&,param的類型是const int&。
f(1);   //1是右值,類型是int,T的類型是int,param的類型是int&&。
要點2:對萬能引用形參進行推導時,左值實參會進行特殊處理。
  • 情況3:ParamType即不是指針也不是引用。

這種情況就是按值傳遞,其目標推導規則如下:

  1. expr的引用屬性會被忽略。
  2. 忽略expr的引用性後,如果expr還具有const或volatile屬性,也會被忽略。

還是看一下例子:

// 聲明模板
template<typename T>
void f(T param);

// 聲明變量
int a = 1;
const int ca = a;
const int& cra = a;

//調用模板
f(a);   //a的類型是int,T和param的類型都是int。
f(ca);  //ca的類型是const int,T和param的類型都是int。
f(cra); //cra的類型是const int&,T和param的類型都是int。
要點3:對按值傳遞的形參進行推導時,實參中的const或volatile屬性,也會被忽略。
  • 有一個特殊情況需要注意的就是數組或函數做模板的實參的情況。
要點4:數組或函數類型的實參在模板推導過程中會退化為對應的指針。除非形參param是按引用傳遞的,這時就會被推導為數組或函數的引用。

條款2:理解auto類型推導

如果你已經熟練掌握了前面模板類型推導的規則,那麼恭喜你也基本掌握了auto類型的推導了。因為除了一個特殊情況外,auto類型推導和模板類型推導的規則是一樣的。

先看和模板類型推導一樣規則的示例:

auto a = 1;         //a的類型是int
const auto ca = a;  //ca的類型是const int
const auto& cra = a;//cra的類型是const int&
auto&& ra1 = a;     //ra1的類型是int&
auto&& ra2 = ca;    //ra2的類型是const int&
auto&& ra3 = 1;     //ra3的類型是int&&

唯一的特殊情況就是在使用了C++11引入的統一初始化——大括號初始化表達式時。如果向模板傳入一個大括號初始化表達式,則無法編譯通過。而auto會將其推導為std::initializer\_list。

舉例如下:

// 聲明模板
template<typename T>
void f(T param);

f({1, 2, 3}); //無法編譯通過

auto a = {1, 2, 3}; //a的類型是std::initializer_list<int>

另外,還有一個要注意的點是:在C++14中可以在函數的返回值或lambda表達式的形參中使用auto,但這裏的auto使用的是模板類型推導,而不是auto類型推導。所以如果在這種情況下使用大括號初始化表達式也是無法編譯通過的。

條款3:理解decltype

要點1:在絕大多數情況下,decltype會返回變量或表達式確切的類型。

在C++11中,decltype的主要用途就在於聲明那些返回值類型依賴於形參類型的函數模板。舉個例子,我們寫一個模板函數f,其形參是一個支持方括號下標語法(即“[]”)的容器和一個下標,並在返回下標操作結果前進行合法性驗證。函數的返回值類型需要與下標操作結果的返回值類型相同。其最終實現如下:

//C++11版
template<typename Container, typename Index>
auto f(Container&& c, Index i) -> decltype(std::forward<Container>(c)[i]) {
    checkInvalid();// 合法性驗證
    return std::forward<Container>(c)[i];
}

//C++14版
template<typename Container, typename Index>
decltype(auto) f(Container&& c, Index i) {
    checkInvalid();// 合法性驗證
    return std::forward<Container>(c)[i];
}

//使用
auto str = f(makeStringDeque(), 5);//其中makeStringDeque是一個返回std::deque<std::string>的工廠函數

條款4:掌握查看類型推導結果的方法

要點1:利用IDE編輯器、編譯器錯誤信息和Boost.TypeIndex庫常常能夠查看到推導得到的類型。

要點2:有些工具得出的結果可能無用或者不準確。所以,理解C++類型推導規則是必要的。

auto

條款5:優先選用auto,而非顯式類型聲明

C++11新增的auto,最大的作用就是讓我們遠離了那些囉嗦的顯示類型聲明。比如我用std::function定義一個如下的函數:

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; };

可以看到這個定義寫起來真是囉嗦,且還容易一不小心寫錯。如果我們用auto來定義則可以寫成:

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

更有甚者,在C++14中auto可以使用在lambda表達式的形參中,於是我們可以得到一個可以應用於任何類似指針之物指涉到的值的比較函數,如下:

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

而且,通常情況下,std::function對象一般都會比使用auto生命的變量使用更多的內存。所以,在能使用auto的情況下,我們通常都應該選擇使用auto。

要點1:auto變量必須初始化,基本上對會導致兼容性和效率問題的類型不匹配現象免疫,還可以簡化重構流程,通常也比顯式指定類型要少打一些字。

條款6:當auto推導的類型不符合要求時,使用帶顯式類型的初始化物習慣用法

在條款2中,我們已經見識了auto推導的一個特殊情況。同樣,還存在着一個普遍的規律是,“隱形”代理類和auto無法和平共處。

我們需要先認識一下“隱形”代理類。所謂代理類,就是指為了模擬或增強其他類型的類。代理類在C++中很常見,比如標準庫中的智能指針就是將資源管理嫁接到裸指針上。舉個例子,通常我們會認為std::vector<T>的operator[]會返回T&。但實際上,std::vector<bool>的operator[]的返回值類型是std::vector<bool>::reference,它就是一個“隱形”的代理類。請看以下代碼:

std::vector<bool> features(const Widget& w);// 返回的vector中每一個bool值都代表着Widget是否提供一個特定的功能

Widget w;

auto highPriority = feature(w)[1];//第一個feature表示這個Widget是否具有高優先級

// processWidget表示按w的優先級處理Wdiget,函數期望highPriority類型是bool
processWidget(w, highPriority);//未定義行為!!!

為什麼最後一句是一個未定義行為呢?因為這裏auto推導的類型是std::vector<bool>::reference。而這個代理類可能會導致highPriority含有一個空懸的指針(具體原因設計標準庫的實現,細節請看書)。

要點1:“隱形”的代理類型可以導致auto根據初始化表達式推導出“錯誤的”類型。

解決這個問題的方法就是強制進行一次類型轉換。我們將上面出問題的語句替換為以下語句,就可以解決這個未定義行為:

auto highPriority = static_cast<bool>(feature(w)[1]);
要點2:auto推導出“錯誤的”類型時可以進行強制類型轉換,讓auto強制推導出你想要的類型。

Add a new 評論

Some HTML is okay.