天天看點

c++類型推斷解析

作者:xTech
c++類型推斷解析

前言

很高興見到你。

類型推斷,在我們使用模闆的時候,是一個非常重要的話題。在c++11中,引入了auto關鍵字,也使得類型推斷不局限于模闆類型推斷。舉個簡單的例子:

template<typename T>
void show(T t) {
    std::cout << t << std::endl;
}

int i = 1;
int& num = i;
int array[8];
show(i);   // 類型T被推斷為int
show(num); // 類型T也是被推斷為int
show(array); // 類型T被推斷為int*
複制代碼           

當我們傳遞參數給模闆函數show時,編譯器會根據我們的參數類型去推斷模闆類型T的類型。如果你對上面的代碼的類型推斷出現了疑惑,那麼閱讀這篇文章能夠給你帶來幫助。

對類型推斷不熟悉,可能會帶來一些很隐蔽的問題。例如上面的引用資料類型傳遞給show函數時,發生了值拷貝,因為T被推斷成了int類型,有些讀者可能會有為什麼不是int&,為何會發生拷貝等問題。

模闆比對

值模闆

參考以下代碼:

// 模闆類型T,沒有前置const、volatile修飾,也沒有指針、引用修飾
template<typename T>
void show(T t) {
    std::cout << t << std::endl;
}

int x = 1;
const int y = 2;
int& z = x;
const int& w = y;

show(x); // T = int, t = int
show(y); // T = int, t = int
show(z); // T = int, t = int
show(w); // T = int, t = int
複制代碼           

代碼中的模闆類型隻有一個T,不是T&等,我這裡稱之為值模闆。這種類型的模闆,采用的為值拷貝,會移除參數的const、volatile、以及引用修飾。是以最終類型推斷的T均為int類型。形參t的類型即為T,是以其類型和T保持一緻。

此模闆類型的核心在于值拷貝,那麼也不難了解,拷貝之後其實參修飾都變得沒有意義了。

指針類型在與值模闆的關聯下會出現一些可能比較模糊的關系,但實際上他也是符合上面我們所說的邏輯的。參考以下代碼:

template<typename T>
void show(T t) {
    std::cout << t << std::endl;
}

int x = 1;
const int y = 2;

const int* p1 = &y;
const int* const p2 = &y;
int* const p3 = &x;

show(p1); // T = const int*,t = const int*
show(p2); // T = const int*,t = const int*
show(p3); // T = int*,      t = int*
複制代碼           

主要關注到show(p2);。p2是一個指向const int的const 指針。當他比對到T時,指針本身的const屬性會被忽略,拷貝指針本身。但指針的類型不會被修改,還是指向const int資料。是以,類型推斷結果為const int*。

引用模闆

參考以下代碼:

template<typename T>
void show(T& t) {
    std::cout << t << std::endl;
}

int x = 1;
const int y = 2;
int& z = x;
const int& w = y;

show(x); // T = int,      t = int& 
show(y); // T = const int,t = const int& 
show(z); // T = int,      t = int& 
show(w); // T = const int, t = const int& 
複制代碼           

在帶有左值引用的模闆函數中,形參t都會被轉化為左值引用類型。這裡我強調是左值引用,因為右值引用有所不同,讀者需要注意一下。t的類型推到也非常符合直覺,并沒有什麼需要多餘解釋的。

模闆T則去除了引用修飾,這個好了解。是以T後面跟了&,那麼他本身肯定是不帶引用的。

同樣的邏輯,我們也可以了解以下模闆函數的類型推導:

template<typename T>
void show(const T& t) {
    std::cout << t << std::endl;
}

show(x); // T = int,t = const int& 
show(y); // T = int,t = const int& 
show(z); // T = int, t = const int& 
show(w); // T = int, t = const int& 
複制代碼           

形參模闆增加了const修飾,那麼類型T就會移除其const修飾。相對應的,t類型會增加const修飾,這不難了解是吧。

前面提到,這是一個左值引用。那麼對于右值引用呢?參考以下代碼:

template<typename T>
void show(T&& t) {
    std::cout << t << std::endl;
}

int x = 1;
const int y = 2;
int& z = x;
const int& w = y;

show(x); // T = int&,      t = int& 
show(y); // T = const int&,t = const int& 
show(z); // T = int&,       t = int& 
show(w); // T = const int&, t = const int& 
show(1); // T = int,        t = int&&
複制代碼           

形如T&&,沒有const、volatile修飾,稱為通用引用。通用引用可以适配左值引用與右值引用。通用引用的特性在于:他既能接收左值引用、又能接收右值引用。

是以我們對于上面的例子中的t類型的推導其實并不奇怪。奇怪的在于T的類型:對于右值的實參,T被正常推導成int,但是當實參是左值時,為什麼會被推導成引用類型?

這其實涉及到一個概念:引用折疊。名字很高大上,但是引用折疊很簡單:T& && = T&。在c++中是不能存在引用的引用的,但是在類型推到的過程中會存儲引用的引用。此時會将兩個引用折疊,變成一個。是以就為什麼T類型帶有左值引用了。

最後注意一下,隻有T&&類型的才被稱為通用引用,而如果是const T&&則不是通用引用,他是一個const的右值引用,無法接收左值引用。

指針模闆

參考以下代碼:

template<typename T>
void show(T* t) {
    std::cout << &t << std::endl;
}

int x = 1;
const int y = 2;

show(&x); // T = int,      t = int* 
show(&y); // T = const int,t = const int* 
複制代碼           

指針類型和引用類型的類型推斷邏輯幾乎一模一樣,也比較符合我們的直覺。

數組與函數

對于數組和函數類型參數,情況有些特殊。參考以下代碼:

template<typename T>
void show(T t) {
    //
}

template<typename T>
void showR(T& t) {
    //
}

template<typename T>
void showP(T* t) {
    //
}

int fun(int){return 0;}

int array[3] = {1,2,3};

show(fun);   // T = int(*)(int),t = int(*)(int)
show(array); // T = int*,       t = int* 

showP(fun);   // T = int(int),t = int(*)(int)
showP(array); // T = int,  t = int* 

showR(fun);   // T = int(int),t = int(&)(int)
showR(array); // T = int[3],  t = int &[3] 
複制代碼           

對于數組,首先我們需要明确一個概念:數組與指針是不一樣的類型。。雖然在使用上很類似,但數組有長度、越界判斷等,都是和指針不同的。

當我們把一個數組對象傳遞給T類型時,數組類型會轉化為指針類型,是以模闆确定為int*。相同的邏輯,對于T*模闆,自然模闆也就會被初始化為int。

函數是類似的,函數對象也會被轉化成指針,是以模闆T會被初始化為指針類型,而指針類型的模闆會被初始化為函數本身的類型。

最後再看到引用類型的模闆。數組與函數對象,傳遞給引用類型的模闆時,不會被退化成指針類型,而是會成為他本來的類型,且數組本身的長度也會被保留。我們可以利用這種方式來擷取到數組的長度:

template<typename T, std::size_t N> 
std::size_t arraySize(T (&u)[N])
{                                                      
    return N; 
} 

int intArray[2]{0,1};
auto size = arraySize(intArray);
複制代碼           

auto比對

c++11增加了auto關鍵字,将類型推斷不僅局限于模闆中。如以下代碼:

auto num = 1;        // int
auto& numRef = num;  // int&
auto* numPtr = # // int*
auto temp = numRef;  // int
複制代碼           

在類型推斷邏輯上,和模闆基本是一模一樣的,沒有不同。唯一的不同在于:

auto list = {1,2,4}; // initialiez_list<int>
複制代碼           

對于花括号資料,auto會直接推斷為initialiez_list<Type>,而模闆是無法推斷的。如以下代碼是非法的:

template<typename T>
void show(T t){}

show({1,2,3}); // 無法推斷T的類型
複制代碼           

此外,auto還可以用于自定義函數傳回值。如下:

// c++11
auto show() -> int {
    // ...
}

// c++14 auto被推斷為int
auto show() {
    // ...
    return 1;
}
複制代碼           

在c++11中,我們需要後置指明auto的類型,而在c++14中,則可以通過最後一行代碼來推斷傳回值類型。在c++11中,這似乎作用不是很大,但是我們可以結合模闆和decltype來實作一些更加神奇的玩法:

template<typename T>
auto show(T t) -> decltype(Convert(t)) {
    // ...
}
複制代碼           

這裡auto的類型為Convert(t),可以根據參數類型來決定函數的傳回值類型。

總結

類型推斷是c++模闆程式設計中一個比較重要的内容,經過前面的學習,讀者應該基本能掌握這部分的内容。在實際的開發中,有時候我們不确定其所推斷的類型,可以通過開發IDE、typeid等手段來判斷類型,輔助我們确定具體的類型。

全文到此,原創不易,覺得有幫助可以點贊收藏評論轉發。 有任何想法歡迎評論區交流指正。 如需轉載請評論區或私信溝通。 另外歡迎光臨筆者的個人部落格:傳送門

繼續閱讀