C11簡潔之道:類型推導
1、 概述
C++11裏面引入了auto和decltype關鍵字來實現類型推導,通過這兩個關鍵字不僅能方便的獲取復雜的類型,還能簡化書寫,提高編碼效率。
2、 auto
2.1 auto關鍵字的新定義
auto關鍵字並不是一個全新的關鍵字,早在舊標準中就已經有定義:“具有自動儲存期的局部變量”,不過用處並不大,如下:
auto int i = 0; //c++98/03,可以默認寫成int i = 0; static int j = 0;
上述代碼中,auto int是舊標準中的用法,與之相對的是static int,代表了靜態類型的定義方法,我們很少這樣使用auto,因為非static的局部變量本身就具有自動存儲期。
所以,c++11中考慮到之前的auto使用較少,不再表示存儲類型指示符(如static,mutable等),而是改成了類型指示符,用來提示編譯器對此類型的變量做類型的自動推導。
我們來看一組例子:
auto x = 5; //OK,x:int auto pi = new auto(1); //OK,pi:int * const auto *v = &x, u = 6; //OK,v:const int *, u:const int static auto y = 0.0; //OK,y:double //auto int r; //error,auto不再表示存儲類型指示符 //auto s; //error,無法推導出類型
x被自動推導為int,並被初始化為5;
pi被推導為int *,同時說明auto還支持new的類型推導;
&x類型為int *,所以const auto *說明auto是int,所以v為const int *;
因為v為int *,所以推導auto為ing,u也應該為int,這裏需要註意的是,u的初始化必須和前面推導的auto類型相同,不然會出現二義性,否則編譯器通不過;
y被推導為double;
auto已經不再是存儲類型指示符,所以,r會提示錯誤;
s沒類型進行推導,所以會報錯沒有初始化。
由列子可以得出以下結論:
auto並不能代表一個實際的類型,只是一個類型申明的占位符;
auto申明的變量必須馬上初始化,讓編譯器在編譯期間推導出實際類型,在編譯的時候用實際的類型替換掉類型占位符auto。
2.2 auto推導規則
auto可以同指針引用結合起來使用,還可以帶上cv限定符(const,volatile的統稱)。下面我們再來看一組列子:
int x = 0; auto *a = &x; //a -> int*,auto->int auto b = &x; //b -> int*,auto->int* auto &c = x; //c -> int&,auto->int auto d = c; //d -> int ,auto->int const auto e = x; //e->const int auto f = e; //f->int const auto &g = x; //g->const int & auto &h = g; //f->const int &
上述例子的推導結果如代碼,從例子我們可以得出以下結論:
auto可以自動推導指針類型;
當不申明為指針或者引用時,auto的推導結果和初始化表達式拋棄引用和cv限定符後的類型一致;
當申明為指針或者引用時,auto推導的結果將保持初始化表達式的cv屬性。
2.3 auto的限制
auto申明的時候必須初始化,那麽auto肯定是不能作為函數參數的。
void func(auto a = 2){} //error:不能用於函數參數
auto不能用於非靜態成員變量。
struct Foo { auto var1 = 0; // error static const auto var2 = 1; //OK,var2->static const int }
auto無法定義數組。
int arr[10] = {0}; auto aa = arr; //OK,aa->int * auto arr2[10] = {0}; //error無法通過編譯
auto無法推導出模版參數。
std::vector<int> vec1; //OK std::vector<auto> vec2 = vec1; //error 無法通過編譯
在Foo中,auto僅能推導static const的整型或者枚舉成員(因為其他靜態類型在c++標準中無法就地初始化),c++11中可以接受非靜態成員變量的就地初始化,但不支持auto類型非靜態成員變量的初始化。
2.4 auto使用場景
auto在我看來,最主要的是可以簡潔代碼。類型推導雖然說可以作自動推導,但是在真正寫代碼的時候,還要考慮可讀性,auto並不能代碼更多的好處。
如果在c++11之前,我們定義一個map,在遍歷的時候,通常需要這麽寫:
void func() { map<int, int> test_map; map<int, int>::iterator it = test_map.begin(); for(it; it != test_map.end(); ++it) { //do something } }
那麽我們在使用auto之後,代碼就會很簡單了,根據map.begin()就可以推導出類型。
void func2() { map<int, int> test_map; for(auto it = test_map.begin(); it != test_map.end(); ++it) { //do something } }
是不是感覺簡潔多了。更簡潔的還在後面,我們在一個unordered_multimap中查找一個範圍:
void func3() { unordered_multimap<int, int> test_map; pair<unordered_multimap<int, int>::iterator, unordered_multimap<int, int>::iterator> range = test_map.equal_range(5); }
很明顯,這個euqal_range返回的類型顯得太繁瑣,而實際上可能並不在乎這裏的具體類型,這時就可以通過auto來簡化書寫。使用auto之後:
void func4() { unordered_multimap<int, int> test_map; auto range = test_map.equal_range(5); }
如果在某些情況不知道返回值類型,我們可以通過auto來做推導,然後統一處理。
class Foo { public: static int get() { return 0; } }; class Bar { public: static const char *get() { return "0"; } }; template <class A> void func5() { auto val = A::get(); // do something } void func6() { func5<Foo>(); func5<Bar>();
return; }
假如我們定義一個泛型函數func5,對具有靜態方法get的類型A得到的結果做統一的後續處理,如果不使用auto,那麽就必須再增加一個模板參數,並在外部手動指定get的返回值類型。
auto是一個很強大的工具,但是如果不加選擇的隨意使用auto會導致代碼可讀性和可維護性嚴重下降,因此,在使用的時候,一定要權衡好使用的“度”,那麽帶來的價值會非常大。
3、 decltype
3.1 基本定義
auto的申明必須要初始化才能確定auto代表的類型,那如果我們既需要得到類型,又不定義變量的時候怎麽辦呢?C++11提供了decltype來解決這個問題,它的定義如下:
decltype(exp) //exp表示一個表達式。
decltype有點像sizeof,不過sizeof是計算表達式類型大小的標識符。和sizeof一樣,decltype也是在編譯時期推導類型的,並且不會真正計算表達式的值。我們來看一組例子:
int x = 0; decltype(x) y = 1; //y->int decltype(x+y) z = 0; //z->int const int & i = x; decltype(i) j = y; //j->const int & const decltype(z) *p = &z; //*p->const int, p->const int * decltype(z) *pi = &z; //*pi->int, pi->int * decltype(pi) * pp = π // *pp->int *, pp->int **
decltype和auto一樣,可以加上引用指針,以及cv限定符進行推導。
3.2 推導規則
網上各種版本的規則眾多,下面是我簡單整理的一些規則:
exp是標識符、類訪問表達式,decltype(exp)和exp的類型一致;
exp是函數調用,decltype(exp)和函數返回值一致;
exp是一個左值,則decltype(exp)是exp的一個左值引用,否則和exp的類型一致。
3.2.1 標識符表達式和類訪問表達式
class Foo { public: static const int miNum = 0; int ix; }; int n = 0; volatile const int &x = n; decltype(n) a = n; //a->int decltype(x) b = n; //b->const valatile int & decltype(Foo::miNum) c = 0; //c->const int Foo foo; decltype(foo.ix) d = 0; //d->int,類訪問表達式
變量abc保留了表達式的所有屬性(cv、引用),對於表達式,decltype的推導和表達式一致,而d是一個類訪問表達式,因此也和表達式類型一致。
3.2.2 函數調用
int &func_int_r(void); //左值,lvalue,簡單理解為可尋址值 int &&func_int_rr(void); //x值,xvalue,右值引用本身是一個xvalue int func_int(void); //純右值,pvalue const int &func_cint_r(void); //左值 const int && func_cint_rr(void); //x值 const int func_cint(void); //純右值 cont Foo func_foo(void); //純右值
//下面是測試語句
int x = 0; decltype(func_int_r()) al = x; //al->int& decltype(func_int_rr()) bl = 0; //bl->int && decltype(func_int()) cl = 0; //cl->int decltype(func_cint_r()) a2 = x; //a2->const int & decltype(func_cint_rr()) b2 = 0; //b2->const int && decltype(func_cint) c2 = 0; //c2->int decltype(func_foo()) ff = Foo(); //ff->const Foo
這裏需要註意的是,C2的推導是int而不是const int,這是因為函數的返回值int是個純右值,對於純右值而言,只有類類型可以攜帶cv限定符,除此之外一般忽略掉。所以func_foo推導出來的ff是const Foo。
3.2.3 帶括號的表達式和加法運算表達式
struct Foo{ int x;}; const Foo foo = Foo(); decltype(foo.x) a = 0; //a->int decltype((foo.x)) b = a; //const int & int n = 0, m = 0; decltype(n+m) c = 0; //c->int decltype(n+=m) d = c; //d->int &
這裏需要註意的是,b的推導,括號表達式中的foo.x是一個左值,所以decltype的結果是一個左值引用,foo的定義是const Foo,所以foo.x是一個const int類型,所以b是一個const int &;
n+m返回的是一個右值,所以結果是int,n+=m返回一個左值,所以推導出d是int &。
3.3 decltype實際應用
decltype多出現在泛型變成中,我們來看一個例子,假如我們編寫一個泛型類:
template<class ContainerT> class Foo { typename ContainerT::iterator it; //類型的定義可能有問題 public: void func(ContainerT &container) { it = container.begin(); } } int main(void) { typedef const std::vector<int> container_t; container_t arr; Foo<container_t> foo; foo.func(arr); return 0; }
問題很明顯,當我們傳入一個const容器的時候,我們的定義的iterator不適用,編譯器會報錯,所以當傳入const容器的時候,我們應該使用const_iterator。
如果在C++98/03要解決這樣的問題,我們必須增加一個泛型類:
template<class ContainerT> class Foo<const ContainerT > { typename ContainerT::const_iterator it; //類型的定義可能有問題 public: void func(const ContainerT &container) { it = container.begin(); } }
這樣雖然說可以解決問題,但是太麻煩,Foo的其他代碼也不得不重新寫一次,如果我們用decltype來解決這個問題:
template<class ContainerT> class Foo { decltype(ContainerT().begin()) it; //類型的定義可能有問題 public: void func(ContainerT &container) { it = container.begin(); } }
decltype也經常用在通過變量表達式抽取變量類型上:
vector<int> v; decltype(v)::value_type I = 0;
冗長的代碼中,我們只需要關心變量本身,不需要關心它的具體類型,如例子,我們只需要之道v是一個容器,可以提取value_type就OK,而不是到處都需要出現vector<int>這種精確的類型名稱。
4、 auto和decltype混編
在泛型編程中,通過auto和decltype的混編來提升靈活性。通過參數的運算來獲得返回值類型。
template<typename R, typename T, typename U> R add(T t, U u) { return t + u; } int a = 1; float b = 2.0; auto c = add<decltype(a + b)>(a, b);
我們不關心a+b類型是什麽,通過decltype(a+b)來推導返回值類型。我們還可以通過add函數的定義來獲得返回值類型。
template<typename T, typename U> decltype(t+u) add(T t, U u) //error :t、u尚未定義 { return t + u; }
c++的返回值是前置語法,在返回值的時候參數變量都還不存在,所以這樣是編譯不通過的,我們可以通過構造函數來進行推導:
template<typename T, typename U> decltype(T() + U()) add(T t, U u) { return t + u; }
但是這樣也有一個問題,T、U類型可能是沒有無參構造函數,我們可以進行改善:
template<typename T, typename U> decltype((*(T*)0) + (*(U*)0)) add(T t, U u) { return t + u; }
這樣可以成功的使用decltype來完成返回值的推導,但是代碼可讀性比較差,並增加使用難度。
c++11增加了返回類型後置語法,講decltype和auto結合起來完成返回值類型的推導。我們再來改善上面的add函數:
template<typename T, typename U> auto add(T t, U u) -> decltype(t + u) { return t + u; }
我們再來看一個例子:
int &foo(int &i); float foo(float &f); template<typename T> auto func(T &val) -> decltype(foo(val)) { return foo(val); }
使用decltype結合返回值後置語法很容易推導出foo(val)可能出現的返回值,並用到func上。返回值類型後置語法,是為了解決函數返回值類型依賴與參數而導致難以確定返回值類型的問題,可以很清晰的描述返回值類型,而不是c++98/03那樣晦澀難懂的語法來解決問題。
C11簡潔之道:類型推導