轉載來自:https://subingwen.cn/cpp/autotype/
在 C++11 中增加了很多新的特性,比如可以使用 auto 自動推導變量的類型,還能夠結合 decltype 來表示函數的返回值。使用新的特性可以讓我們寫出更加簡潔,更加現代的代碼。
1. auto
在 C++11 之前 auto 和 static 是對應的,表示變量是自動存儲的,但是非 static 的局部變量默認都是自動存儲的,因此這個關鍵字變得非常雞肋,在 C++11 中他們賦予了新的含義,使用這個關鍵字能夠像別的語言一樣自動推導出變量的實際類型。
1.1 推導規則
C++11 中 auto 並不代表一種實際的數據類型,只是一個類型聲明的 “佔位符”,auto 並不是萬能的在任意場景下都能夠推導出變量的實際類型,使用auto聲明的變量必須要進行初始化,以讓編譯器推導出它的實際類型,在編譯時將auto佔位符替換爲真正的類型。使用語法如下:
auto 變量名 = 變量值;
auto x = 3.14; // x 是浮點型 double auto y = 520; // y 是整形 int auto z = 'a'; // z 是字符型 char auto nb; // error,變量必須要初始化 auto double nbl; // 語法錯誤, 不能修改數據類型
不僅如此,auto 還可以和指針、引用結合起來使用也可以帶上 const、volatile 限定符,在不同的場景下有對應的推導規則,規則內容如下:
當變量不是指針或者引用類型時,推導的結果中不會保留 const、volatile 關鍵字
當變量是指針或者引用類型時,推導的結果中會保留 const、volatile 關鍵字
int temp = 110; auto *a = &temp; auto b = &temp; auto &c = temp; auto d = temp;
這邊可以看到
a 是 int*
b 是int*
c 是int&
d 是int
int tmp = 250; const auto a1 = tmp; auto a2 = a1; const auto& a3 = tmp; auto& a4 = a3;
可以看到
a1 是 const int
a2卻看到const沒有了
a3是const int&
a4是const int&
1.2 auto的限制
auto 關鍵字並不是萬能的,在以下這些場景中是不能完成類型推導的:
不能作爲函數參數使用。因爲只有在函數調用的時候纔會給函數參數傳遞實參,auto 要求必須要給修飾的變量賦值,因此二者矛盾。
int func(auto a, auto b) // error { cout << "a: " << a << ", b: " << b << endl; }
不能用於類的非靜態成員變量的初始化
class Test { auto v1 = 0; // error static auto v2 = 0; // error,類的靜態非常量成員不允許在類內部直接初始化 static const auto v3 = 10; // ok }
不能使用 auto 關鍵字定義數組
int array[] = { 1,2,3,4,5 }; // 定義數組 auto t1 = array; // ok, t1被推導爲 int* 類型 auto t2[] = array; // error, auto無法定義數組 auto t3[] = { 1,2,3,4,5 }; // error, auto無法定義數組
無法使用 auto 推導出模板參數
template <typename T> struct Test {} int func() { Test<double> t; Test<auto> t1 = t; // error, 無法推導出模板類型 return 0; }
1.3 auto 的應用
瞭解了 auto 的限制之後,我們就可以避開這些場景快樂的編程了,下面列舉幾個比較常用的場景:
用於STL的容器遍歷。
在 C++11 之前,定義了一個 stl 容器之後,遍歷的時候常常會寫出這樣的代碼:
#include <map> int main() { map<int, string> person; map<int, string>::iterator it = person.begin(); for (; it != person.end(); ++it) { // do something } return 0; }
可以看到在定義迭代器變量 it 的時候代碼是很長的,寫起來就很麻煩,使用了 auto 之後,就變得清爽了不少:
#include <map> int main() { map<int, string> person; // 代碼簡化 for (auto it = person.begin(); it != person.end(); ++it) { // do something } return 0; }
用於泛型編程,在使用模板的時候,很多情況下我們不知道變量應該定義爲什麼類型,比如下面的代碼:
#include <iostream> #include <string> using namespace std; class T1 { public: static int get() { return 10; } }; class T2 { public: static string get() { return "hello, world"; } }; template <class A> void func(void) { auto val = A::get(); cout << "val: " << val << endl; } int main() { func<T1>(); func<T2>(); return 0; }
在這個例子中定義了泛型函數 func,在函數中調用了類 A 的靜態方法 get () ,這個函數的返回值是不能確定的,如果不使用 auto,就需要再定義一個模板參數,並且在外部調用時手動指定 get 的返回值類型,具體代碼如下:
#include <iostream> #include <string> using namespace std; class T1 { public: static int get() { return 0; } }; class T2 { public: static string get() { return "hello, world"; } }; template <class A, typename B> // 添加了模板參數 B void func(void) { B val = A::get(); cout << "val: " << val << endl; } int main() { func<T1, int>(); // 手動指定返回值類型 -> int func<T2, string>(); // 手動指定返回值類型 -> string return 0; }
2. decltype
在某些情況下,不需要或者不能定義變量,但是希望得到某種類型,這時候就可以使用 C++11 提供的 decltype 關鍵字了,它的作用是在編譯器編譯的時候推導出一個表達式的類型,語法格式如下:
decltype (表達式)
decltype 是 “declare type” 的縮寫,意思是 “聲明類型”。decltype 的推導是在編譯期完成的,它只是用於表達式類型的推導,並不會計算表達式的值。來看一組簡單的例子:
int a = 10; decltype(a) b = 99; // b -> int decltype(a+3.14) c = 52.13; // c -> double decltype(a+b*c) d = 520.1314; // d -> double
可以看到 decltype 推導的表達式可簡單可複雜,在這一點上 auto 是做不到的,auto 只能推導已初始化的變量類型。
2.1 推導規則
通過上面的例子我們初步感受了一下 decltype 的用法,但不要認爲 decltype 就這麼簡單,在它簡單的背後隱藏着很多的細節,下面分三個場景依次討論一下:
表達式爲普通變量或者普通表達式或者類表達式,在這種情況下,使用 decltype 推導出的類型和表達式的類型是一致的。
#include <iostream> #include <string> using namespace std; class Test { public: string text; static const int value = 110; }; int main() { int x = 99; const int& y = x; decltype(x) a = x; decltype(y) b = x; decltype(Test::value) c = 0; Test t; decltype(t.text) d = "hello, world"; return 0; }
變量 a 被推導爲 int 類型
變量 b 被推導爲 const int & 類型
變量 c 被推導爲 const int 類型
變量 d 被推導爲 string 類型
表達式是函數調用,使用 decltype 推導出的類型和函數返回值一致。
#include <iostream> #include <string> using namespace std; class Test { public: string text; static const int value = 110; }; //函數聲明 int func_int(); // 返回值爲 int int& func_int_r(); // 返回值爲 int& int&& func_int_rr(); // 返回值爲 int&& const int func_cint(); // 返回值爲 const int const int& func_cint_r(); // 返回值爲 const int& const int&& func_cint_rr(); // 返回值爲 const int&& const Test func_ctest(); // 返回值爲 const Test int main() { //decltype類型推導 int n = 100; decltype(func_int()) a = 0; decltype(func_int_r()) b = n; decltype(func_int_rr()) c = 0; decltype(func_cint()) d = 0; decltype(func_cint_r()) e = n; decltype(func_cint_rr()) f = 0; decltype(func_ctest()) g = Test(); return 0; }
變量 a 被推導爲 int 類型
變量 b 被推導爲 int& 類型
變量 c 被推導爲 int&& 類型
變量 d 被推導爲 int 類型
變量 e 被推導爲 const int & 類型
變量 f 被推導爲 const int && 類型
變量 g 被推導爲 const Test 類型
函數 func_cint () 返回的是一個純右值(在表達式執行結束後不再存在的數據,也就是臨時性的數據),
對於純右值而言,只有類類型可以攜帶const、volatile限定符,除此之外需要忽略掉這兩個限定符,因此推導出的變量 d 的類型爲 int 而不是 const int。
表達式是一個左值,或者被括號 ( ) 包圍,使用 decltype 推導出的是表達式類型的引用(如果有 const、volatile 限定符不能忽略)。
#include <iostream> #include <vector> using namespace std; class Test { public: int num; }; int main() { const Test obj; //帶有括號的表達式 decltype(obj.num) a = 0; decltype((obj.num)) b = a; //加法表達式 int n = 0, m = 0; decltype(n + m) c = 0; decltype(n = n + m) d = n; return 0; }
obj.num 爲類的成員訪問表達式,符合場景 1,因此 a 的類型爲 int
obj.num 帶有括號,符合場景 3,因此 b 的類型爲 const int&。
n+m 得到一個右值,符合場景 1,因此 c 的類型爲 int
n=n+m 得到一個左值 n,符合場景 3,因此 d 的類型爲 int&
2.2 decltype 的應用
關於 decltype 的應用多出現在泛型編程中。比如我們編寫一個類模板,在裏邊添加遍歷容器的函數,操作如下:
#include <list> using namespace std; template <class T> class Container { public: void func(T& c) { for (m_it = c.begin(); m_it != c.end(); ++m_it) { cout << *m_it << " "; } cout << endl; } private: ? ? ? m_it; // 這裏不能確定迭代器類型 }; int main() { const list<int> lst; Container<const list<int>> obj; obj.func(lst); return 0; }
在程序的第 17 行出了問題,關於迭代器變量一共有兩種類型:只讀(T::const_iterator)和讀寫(T::iterator),有了 decltype 就可以完美的解決這個問題了,當 T 是一個 非 const 容器得到一個 T::iterator,當 T 是一個 const 容器時就會得到一個 T::const_iterator。
#include <list> #include <iostream> using namespace std; template <class T> class Container { public: void func(T& c) { for (m_it = c.begin(); m_it != c.end(); ++m_it) { cout << *m_it << " "; } cout << endl; } private: decltype(T().begin()) m_it; // 這裏不能確定迭代器類型 }; int main() { const list<int> lst{ 1,2,3,4,5,6,7,8,9 }; Container<const list<int>> obj; obj.func(lst); return 0; }#include <list> #include <iostream> using namespace std; template <class T> class Container { public: void func(T& c) { for (m_it = c.begin(); m_it != c.end(); ++m_it) { cout << *m_it << " "; } cout << endl; } private: decltype(T().begin()) m_it; // 這裏不能確定迭代器類型 }; int main() { const list<int> lst{ 1,2,3,4,5,6,7,8,9 }; Container<const list<int>> obj; obj.func(lst); return 0; }
decltype(T().begin()) 這種寫法在 vs2017/vs2019 下測試可用完美運行。
3. 返回類型後置
在泛型編程中,可能需要通過參數的運算來得到返回值的類型,比如下面這個場景:
#include <iostream> using namespace std; // R->返回值類型, T->參數1類型, U->參數2類型 template <typename R, typename T, typename U> R add(T t, U u) { return t + u; } int main() { int x = 520; double y = 13.14; // auto z = add<decltype(x + y), int, double>(x, y); auto z = add<decltype(x + y)>(x, y); // 簡化之後的寫法 cout << "z: " << z << endl; return 0; }
關於返回值,從上面的代碼可以推斷出和表達式 t+u 的結果類型是一樣的,因此可以通過通過 decltype 進行推導,關於模板函數的參數 t 和 u 可以通過實參自動推導出來,因此在程序中就也可以不寫。雖然通過上述方式問題被解決了,但是解決方案有點過於理想化,因爲對於調用者來說,是不知道函數內部執行了什麼樣的處理動作的。
因此如果要想解決這個問題就得直接在 add 函數身上做文章,先來看第一種寫法:
template <typename T, typename U> decltype(t + u) add(T t, U u) { return t + u; }
當我們在編譯器中將這幾行代碼改出來後就直接報錯了,因此 decltype 中的 t 和 u 都是函數參數,直接這樣寫相當於變量還沒有定義就直接用上了,這時候變量還不存在,有點心急了。
在C++11中增加了返回類型後置語法,說明白一點就是將decltype和auto結合起來完成返回類型的推導。其語法格式如下:
// 符號 -> 後邊跟隨的是函數返回值的類型 auto func(參數1, 參數2, ...) -> decltype(參數表達式)
通過對上述返回類型後置語法代碼的分析,得到結論:auto 會追蹤 decltype() 推導出的類型,因此上邊的 add() 函數可以做如下的修改:
#include <iostream> using namespace std; template <typename T, typename U> // 返回類型後置語法 auto add(T t, U u) -> decltype(t + u) { return t + u; } int main() { int x = 520; double y = 13.14; // auto z = add<int, double>(x, y); auto z = add(x, y); // 簡化之後的寫法 cout << "z: " << z << endl; return 0; }
爲了進一步說明這個語法,我們再看一個例子:
#include <iostream> using namespace std; int& test(int& i) { return i; } double test(double& d) { d = d + 100; return d; } template <typename T> // 返回類型後置語法 auto myFunc(T& t) -> decltype(test(t)) { return test(t); } int main() { int x = 520; double y = 13.14; // auto z = myFunc<int>(x); auto z = myFunc(x); // 簡化之後的寫法 cout << "z: " << z << endl; // auto z = myFunc<double>(y); auto z1 = myFunc(y); // 簡化之後的寫法 cout << "z1: " << z1 << endl; return 0; }
在這個例子中,通過 decltype 結合返回值後置語法很容易推導出來 test(t) 函數可能出現的返回值類型,並將其作用到了函數 myFunc() 上。