現代C++之理解auto類型推斷


理解auto類型推斷

上一篇帖子中講述了模板類型推斷,我們知道auto的實現原理是基於模板類型推斷的,回顧一下模板類型推斷:

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

使用下面的函數調用:

f(expr);

我們看到模板類型推斷過程涉及到了模板template、函數f以及參數(包括模板參數和函數參數),調用f的時候,編譯器會推斷T和ParamType的類型。auto的實現和這三個部分是有着對應關系的。當使用auto聲明一個變量,auto關鍵字扮演的是模板類型推斷中T的角色,而類型說明符扮演的是ParamType的角色。看下面的例子:

auto x = 27;  //類型說明符就是auto自己
const auto cx =x; //類型說明符為const auto
const auto& rx =x;//類型說明符為const auto&

編譯器使用auto對上面的類型進行推斷就如同使用了下面的模板類型推斷:

template<typename T> 
void func_for_x(T param); //ParamType即非引用也非指針
func_for_x(27); // 推斷x的類型,T為int ,ParamType 為 int

template<typename T> 
void func_for_cx(const T param); //ParamType即非引用也非指針
func_for_cx(x); //用於推斷cx的類型,T為int,ParamType為 const int

template<typename T> 
void func_for_rx(const T& param);//ParamType為引用
func_for_rx(x); // 用於推斷rx的類型,T為int,ParamType為const int&

繼續回顧上一篇帖子的內容,基於ParamType的三種形式,模板類型推斷也對應着三種不同情況。而auto的類型說明符扮演的是ParamType,因此使用auto進行變量聲明,也會有三種情況:

  • 類型說明符是指針或者引用類型,但不是universal reference
  • 類型說明符是universal reference。
  • 類型說明符即非指針也非引用。

上面舉的例子是第一種和第三種情況:

auto x = 27; //case 3 x類型被推斷為int
const auto cx = x; //case 3  cx被推斷為 const int
const auto &rx = x; //case 1 rx被推斷為const int &

舉一個情況2的例子:

auto&& uref1 = x; //x為左值,uref1被推斷為左值引用
auto&& uref2 = cx; // cx  const int 左值,uref2被推斷為const int &
auto&& uref3 = 27; // 27 為 int 右值,uref3被推斷為 int &&

上篇帖子介紹了對於模板中的非引用ParamType,傳入函數或者數組實參的時候會退化為指針的情況(而使用引用ParamType的時候,數組實參會被推斷為指向數組的引用),auto類型推斷也會如此:

const char name[] =  "R. N. Briggs";
auto arr1 = name; // arr1 的類型為const char*
auto& arr2 = name; // arr2 的類型為const char (&)[13]
void someFunc(int, double); 
auto func1 = someFunc; // func1的 類型為 void (*)(int, double)
auto& func2 = someFunc; // func2的類型為 void (&)(int, double)

上面介紹的都是auto和模板類型推斷使用原理相同的部分,下面說的不一樣的。

C++98中初始化一個Int有兩種方式:

int x1=27;
int x1(27);

在C++11中,支持統一初始化(uniform initialization):

int x3 = {27};
int x3{27};

四種語法形式的結果只有一個,初始化一個Int值為27。這里我們將都使用auto進行初始化:

 auto x1 = 27;
 auto x2(27);
 auto x3 = {27};
 auto x4{27}; 

上面的四句話都能編譯通過,但並沒有和原來的四種形式意義完全一致。前面兩個是一樣的,后面兩句話聲明的變量類型是std::initializer_list ,其中包含了單個元素,值為27。

 auto x1 = 27; //x1為int,值為27
 auto x2(27);//同上
 auto x3 = {27};//x3為 std::initializer_list<int>,值為{27}
 auto x4{27}; //同上

這里就用到了一個對於auto的特殊類型推斷規則:當用大括號括起來的值對auto變量進行初始化的時候(叫做統一初始化式),變量類型會被推斷為 std::initializer_list。如果不能夠推斷成此類型(比如,大括號中的值不是同一類型),編譯會出錯:

 auto x5 = { 1, 2, 3.0 }; // error! 類型不一致,不能將推斷為std::initializer_list<T>

這里會發生兩種類型推斷,一種是將統一初始化式推斷為std::initializer_list ,而std::initializer_list 本身也是一個類型為T的模板,因此會根據統一初始化式中的實參對T進行模板類型推斷,這是第二種類型推斷。上面的類型推斷會失敗是因為第二種類型推斷會失敗。

對統一初始化式的處理的不一致是auto和模板類型推斷的唯一區別。使用統一初始化式對auto變量初始化會將其推斷為std::initializer_list ,但是模板類型推斷不會這么做:

auto x = { 11, 23, 9 }; // x的類型為 std::initializer_list<int>

template<typename T> // 和auto x等同的模板類型推斷
void f(T param); 

f({ 11, 23, 9 }); // 錯誤!這里不能推斷T的類型。

如果要達到auto的效果,得按照下面的方式來做:

template<typename T>
void f(std::initializer_list<T> initList);
f({ 11, 23, 9 }); // T被推斷為int, initList 的類型為 std::initializer_list<int>

在C++11中使用auto時,這里比較容易出錯,你本來想聲明別的變量,最終卻將其聲明成了一個 std::initializer_list 。因此,要謹慎使用統一初始化

在C++14中,允許將auto作為函數返回值,也可以用其修飾lambda表達式中的參數。但是這些auto使用的都是模板類型推斷,而不是auto類型推斷,因此一個函數返回值為auto 類型時,返回統一初始化式的值會出錯:

auto createInitList()
{
    return { 1, 2, 3 }; // 錯誤!不能推斷{1,2,3}
}

下面的方式是對的:

std::initializer_list<int> createInitList()
{
    return { 1, 2, 3 }; // 
}

最后總結一下:

  • 模板類型推斷是auto的基礎,auto關鍵字扮演了模板類型推斷中的T,而類型說明符扮演的是ParamType。
  • 對於模板類型推斷和auto類型推斷,大多數場景下推斷規則相通,有一種特殊情況,就是統一初始化式。
  • C++14中使用auto可以作為函數返回值,也可以作為lambda表達式的參數修飾符,但需要注意,這里的auto使用的是模板類型推斷,而不是auto類型推斷。


免責聲明!

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



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