對于連續兩個右尖括號>, 那么它們之間需要一個空格來進行分隔,以避免發生編譯時的錯誤。
#include<iostream>
using namespace std;
template<int i>class X{};
template<class T>class Y{};
Y<X<1>>x1;
Y<X<2>>x2;
int main(){
return 0;
}
C++98編譯器會把>>優先解析為右移符號。除了嵌套的模板標識,在使用形如static_cast、dynamic_cast、reinterpret_cast, 或者const_cast表達式進行轉換的時候,我們也常會遇到相同的情況。
const vector<int> v=static_cast<vector<int>>(v)
C++11標準要求編譯器智能地去判斷在哪些情況下>>不是右移符號。
auto類型推導
在C/C++程序員的眼中,每個變量使用前必須定義幾乎是天經地義的事,這樣通常被視為編程語言中的"靜態類型"的體現。而對于如Python、Perl、JavaScript等語言中變量不需要聲明,而幾乎"拿來就用"的變量使用方式,則被視為是編程語言中"動態類型"的體現。靜態類型和動態類型的主要區別在于對變量進行類型檢查的時間點。對于所謂的靜態類型,類型檢查主要發生在編譯階段;而對于動態類型,類型檢查主要發生在運行階段。形如Python等語言中變量"拿來就用"的特性,則需要歸功于一個技術,即類型推導。
C++11中類型推導的實現的方式之一就是重定義了auto關鍵字。另外一個現實是decltype。
#include<iostream>
using namespace std;
int main(){
auto name="world.\n";
cout<<"hello,"<<name;
}
這里我們使用了auto關鍵字來要求編譯器對變量name的類型進行自動推導。這里編譯器根據它的初始化表達式的類型,推導出name的類型為char*。
auto聲明的變量必須被初始化,以使編譯器能夠從其初始化表達式中推導出其類型。從這個意義上來講,auto并非一種"類型"聲明,而是一個類型聲明時的"占位符",編譯器在編譯時間會將auto替代為變量實際的類型。
auto的優勢
直觀地,auto推導的一個最大優勢就是在擁有初始化表達式的復雜類型變量聲明時簡化代碼。由于C++的發展,聲明變量類型也變得越來越復雜,很多時候,名字空間,模板成為了類型的一部分,導致程序員在使用庫的時候如履薄冰。
#include<string>
#include<vector>
voidvoid loopover(std:: vector<std:: string>&vs){
std:: vector<std:: string>:: iterator i =vs.begin(); //想要使用iterator,往往需要書寫大量代碼
for(; i<vs.end();i++){
//一些代碼
}
}
在不使用using namespace std的情況下,想對一個vector數組進行循環??梢钥吹?,當想定義個迭代器i的時候,我們必須寫出std:: vector<std:: string>:: iterator 這樣長的類型聲明。而使用auto的話,代碼會的可讀性可以成倍增長。
#include<string>
#include<vector>
voidvoid loopover(std:: vector<std:: string>&vs){
//std:: vector<std:: string>:: iterator i =vs.begin(); //想要使用iterator,往往需要書寫大量代碼
for(auto i=vs.begin(); i<vs.end() ; i++){
//一些代碼
}
}
使用了auto,程序員甚至可以將i的聲明放入for循環中,i的類型將由表達式vs.begin() 推導出。事實上,在C++11中,由于auto的存在,使得STL將會變得更加容易,寫出的代碼也會更加清晰可讀。
auto的第二個優勢則在于可以免除程序員在一些類型聲明時的麻煩,或者避免一些在類型聲明時的錯誤。事實上,在C/C++中,存在著很多隱式或者用戶自定義的類型轉換規則(比如整型與字符型進行加法運算后,表達式返回的是整型,這是一條隱式規則)。這個時候,auto就有用武之地了。
#include<string>
#include<vector>
using namespace std;
class PI{
public:
double operator*(float v){
return(double) val*v;//這里精度被擴展了
}
const float val=3.1415927f;
};
int main(){
float radius=1.7e10;
PI pi;
auto circumference=2*(pi*radius);
}
定義了float型的變量radius(半徑) 以及一個自定義類型PI變量pi(π值), 在計算圓周長的時候,使用了auto類型來定義變量circumference。這里,PI在于float類型數據相乘時,其返回值為double。而PI的定義可能是在其他的地方(頭文件里),main函數的程序員可能不知道PI的作者為了避免數據上溢或者精度降低而返回了double類型的浮點數。因此main函數程序員如果使用float類型聲明circumference, 就可能享受不了PI作者細心設計帶來的好處。反之,將circumference聲明為auto,就沒有問題。因為編譯器已經自動地做出了最好的選擇。
但是,auto并不能解決所有的精度問題:(這跟一些動態類型語言中數據會自動進行擴展的特性還是不一樣的)
#include <iostream>
using namespace std;
int main(){
unsigned int a=4294967295;//最大的unsigned Int值
unsigned int b=1;
auto c=a+b; //c的類型依然是unsigned int
cout<<"a="<<a<<endl;//a=4294967295
cout<<"b="<<b<<endl;//b=1
cout<<"a+b="<<c<<endl;//a+b=0
return 0;
}
auto的第三個優點就是其"自適應"性能夠在一定程度上支持泛型的編程。
在PI那個代碼中,如果將operator*返回值變成了long double,此時,main函數并不需要修改,因為auto會"自適應"新的類型。對于不同的平臺上的代碼維護,auto也會帶來一些"泛型"的好處。以strlen函數為例,在32位的編譯環境下,strlen返回的為一個4字節的整型,而在64位的編譯環境下,strlen會返回一個8字節的整型。雖然系統庫<cstring>為其提供了size_t類型來支持多平臺間的代碼共享支持,但是使用auto關鍵字我們同樣可以達到代碼跨平臺的效果。
auto var=strlen("hello world!").
由于size_t的適用范圍往往局限于<cstring>中定義的函數,auto的適用范圍明顯更加廣泛。
當auto應用于模板的定義中,其"自適應"行會得到更加充分的體現。
#include <iostream>
using namespace std;
template<typename T1,typename T2>
double Sum(T1&t1, T2&t2){
auto s=t1+t2;//s的類型會在模板實例化時被推導出來
return s;
}
int main(){
int a=3;
long b=5;
float c=1.0f,d=2.3f;
auto e=Sum<int,long>(a,b);//s的類型被推導為long
auto f=Sum<float,float>(c,d);//s的類型被推導為float
}
Sum模板函數接受兩個參數。由于類型T1、T2要在模板實例化時才能確定,所以在Sum中將變量s的類型聲明為auto的。在函數main中我們將模板實例化時,Sum<int,long>中的s變量會被推導為long類型,而Sum<float,float>中的s變量則會被推導為float??梢钥吹?,auto與模板一起使用時,其"自適應"特性能夠加強C++中"泛型"的能力。不過在這個例子中,由于總是返回double類型的數據,所以Sum模板函數的適用范圍還是受到了一定的限制。另外,應用auto還會在一些情況下取得意想不到的好效果。
#include <iostream>
using namespace std;
#define Max1(a,b)((a)>(b))?(a):(b)
#define Max2(a,b)({\
auto _a=(a);\
auto _b=(b);\
(_a>_b)? _a:_b;})
int main(){
int m1=Max1(1*2*3*4,5+6+7+8);
int m2=Max2(1*2*3*4,5+6+7+8);
}
(#define 里的"" 意味著就是可以把多行一起處理。)
我們定義了兩種類型的宏Max1和Max2。兩者作用相同,都是求a和b中較大者并返回。前者采用傳統的三元運算符表達式,這可能會帶來一定的性能問題。因為a或者b在三元運算符中都出現了兩次,那么無論是取a還是取b,其中之一都會被運算兩次。而在Max2中,我們將a和b都先算出來,再使用三元運算符進行比較。
在傳統的C++98標準中,由于a和b的類型無法獲得,所以我們無法定義Max2這樣的高性能宏。而新的標準中的auto則提供了這種可行性。
auto的使用細則
auto類型指示符與指針和引用之間的關系。在C++11中,auto可以與指針和引用結合起來使用,使用的效果基本上會符合C/C++程序員的想象。
#include <iostream>
using namespace std;
int x;
int *y=&x; //把x的地址給y
double foo();
int &bar();
auto *a=&x;//int*
auto &b=x;//int&
auto c=y;//int*
auto *d=y;//int*
auto *e=&foo();//編譯失敗,指針不能指向臨時變量
auto &f=foo();//編譯失敗,nonconst的左值引用不能和一個臨時變量綁定
auto g=bar(); //int
auto &h=bar(); //int&
(問題:指針和引用的區別)
指針:指針是一個變量,只不過這個變量存儲的是一個地址,指向內存的一個存儲單元;而引用跟原來的變量實質上是同一個東西,只不過是原變量的一個別名而已。
int a=1; int *p=&a;
int a=1; int &b=a;
上面定義了一個整型變量和一個指針變量p, 該指針變量指向a的存儲單元,即p的值是a存儲單元的地址。而下面兩句定義了一個整型變量a和這個整型a的引用b,事實上,a和b是同一個東西,在內存占有同一個存儲單元。
- 可以有const指針,但是沒有const引用;
- 指針可以有多級,但是引用只能是一級(int **p, 合法而 int &&a是不合法的)
- 指針的值可以為空,但是引用的值不能為NULL, 并且引用在定義的時候必須初始化;
- 指針的值在初始化后可以改變,即指向其它的存儲單元,而引用在進行初始化后就不會再改變了。
- "sizeof引用"得到的是所指向的變量(對象)的大小,而"sizeof指針"得到的是指針本身的大小;
- 指針和引用的自增(++)運算意義不一樣;
變量a、c、d的類型指針類型,且都指向變量x。實際上對于a、c、d三個變量而言,聲明其為auto* 或 auto 并沒有區別。而如果要使得auto聲明的變量是另一個變量的引用,則必須使用auto&,如圖本例中的變量b和h一樣。
其次,auto與volatile和const之間也存在著一些項目的聯系。volatile和const代表了變量的兩種不同的屬性:易失的和常量的。在C++標準中,它們常常被一起叫做cv限制符。鑒于cv限制符的特殊性,C++11標準規定auto可以與cv限制符一起使用,不過聲明為auto的變量并不能從其初始化表達式中"帶走" cv限制符。
#include <iostream>
using namespace std;
int main(){
double foo();
float *bar();
const auto a=foo();//a:const double
const auto &b=foo(); //b:const double&
volatile auto*c=bar(); //c:volatile float*
auto d=a;//d:double
auto &e = a;//e:const double&
auto f=c;//f:float*
volatile auto&g=c;//g:volatile float*&
return 0;
}
我們可以通過非cv限制的類型初始化一個cv限制的類型,如變量a、b、c所示。不過通過auto聲明的變量d、f卻無法帶走a和f的常量性或者易失性。而引用,比如,聲明為引用的變量e、g都保持了其引用的對象相同的屬性(事實上,指針也是一樣的)。
此外,跟其他的變量指示符一樣,同一個賦值語句中,auto可以用來聲明多個變量的類型,不過這些變量的類型必須相同。如果這些變量的類型不相同,編譯器則會報錯。事實上,用auto來聲明多個變量類型時,只有第一個變量用于auto的類型推導,然后推導出來的數據類型被作用于其他的變量。所以,不允許這些變量的類型不相同。
auto x=1,y=2;//x和y的類型均為int
//m是一個指向const int類型變量的指針,n是一個int類型的變量
const auto*m=&x,n=1;
auto i=1,j=3.14f;//編譯失敗
auto o=1,&p=o,*q=&p;//從左向右推導
對于變量m和n,這里似乎是auto被替換成了int,所以m是一個int*指針類型,而n只是一個int類型。同樣的情況也發生在變量o、p、q上,這里o是一個類型為int的變量,p是o的引用,而q是p的指針。auto的類型推導按照從左往右,且類似于字面替換的方式進行。事實上,標準里稱auto是一個將要推導出的類型的"占位符"。這樣的規則無疑是直觀而讓人略感意外的。包括C++11新引入的初始化列表,以及new,都可以使用auto關鍵字。
#include<initializer_list>
auto x=1;
auto x1(1);
auto y{1};//使用初始化列表的auto
auto z=new auto(1);//可以用于new
不過auto也不是萬能的,受限于語法的二義性,或者是實現的困難性,auto往往也會有上的限制。例外都寫在了下面代碼中:
#include<vector>
using namespace std;
void fun(auto x=1){} //1:auto函數參數,無法通過編譯
struct str{
auto var=10;//2:auto非靜態成員變量,無法通過編譯
};
int main(){
char x[3];
auto y=x;
auto z[3]=x;//3:auto數組,無法通過編譯
//4:auto模板參數(實例化時),無法通過編譯
vector<auto> v={1};
}
(1) 對于函數fun來說,auto不能是其形參類型??赡芨杏X對于fun來說,由于其有默認參數,所以應該推導fun形參x的類型為int型。但事實卻無法符合大家的想象。因為auto是不能做形參的類型的。如果程序員需要泛型的參數,還是需要求助于模板。
(2) 對于結構體來說,非靜態成員變量的類型不能是auto的。同樣的,由于var定義了初始值,讀者可能認為auto可以推導str成員var的類型為int的。但編譯器阻止了auto對結構體中的非靜態成員進行推導,即使成員擁有初始值。
(3) 聲明auto數組。我們可以看到,main中的x是一個數組,y的類型是可以推導的,而聲明auto z[3]這樣的數組同樣會被編譯器禁止。
(4) 在實例化模板的時候使用auto作為模板參數,如果main中我們聲明的vector<auto> v。雖然讀者可能認為這里一眼而知是int類型,但編譯器卻阻止了編譯。
為了避免和C++98中auto的含義發生混淆,C++11只保留auto作為類型指示符的用法,以下的語句在C++98和C語言中都是合法的。但是在C++11中,編譯器會報錯。
auto int i=1;
auto只是C++11中類型推導體現的一部分。其余的,則會在decltype中得到體現。
decltype
typeid 與decltype
C++98對動態類型支持就是C++中的運行時類型識別(RTTI)
RTTI的機制是為每個類型產生一個type_info類型的數據,程序員可以在程序中使用typeid隨時查詢一個變量的類型,typeid就會返回變量相應的type_info數據。而type_info的name成員函數可以返回類型的名字。而在C++11中,又增加了hash_code這個成員函數,返回該類型唯一的哈希值,以供程序員對變量的類型隨時進行比較。
#include<iostream>
#include<typeinfo>
using namespace std;
class White{};
class Black{};
int main(){
White a;
Black b;
cout<<typeid(a).name()<<endl;//5White
cout<<typeid(b).name()<<endl;//5Black
White c;
bool a_b_sametype=(typeid(a).hash_code()==typeid(b).hash_code());
bool a_c_sametype=(typeid(a).hash_code()==typeid(c).hash_code());
cout<<"Same type?" <<endl;
cout<<"A and B?"<<(int)a_b_sametype<<endl;
cout<<"A and C?"<<(int)a_c_sametype<<endl;
}
這里我們定義了兩個不同的類型White和Black,以及其類型的變量a和b。此外我們使用typeid返回類型的type_info,并分別引用name打印類型的名字(5這樣的前綴是g++這類編譯器輸出的名字,其他編譯器可能會打印出其他的名字,這個標準并沒有明確規定),應用hash_code進行類型的比較。在RTTI的支持下,程序員可以在一定程度上了解程序中類型的信息(相比于is_same模板函數的成員類型value在編譯時得到信息,hash_code是運行時得到的信息)。
除了typeid外,RTTI還包括C++中的dynamic_cast等特性。但是,RTTI會帶來一些運行時的開銷,所以編譯器會讓用戶選擇性地關閉該特性(比如XL C/C++編譯器得-qnortti, GCC的選項-fno-rttion, 或者微軟編譯器選項/GR-)。而且很多時候,運行時才確定出類型對于程序員來說為時過晚,程序員更多需要的是在編譯時期確定出類型()標準庫中非常常見。而通常程序員是要使用這樣的類型而不是識別該類型,因此RTTI無法滿足需求。
在C++的發展中,類型推導是隨著模板和泛型編程的廣泛使用而引入的。因為在泛型編程中,類型成了未知數。例如:
#include <iostream>
using namespace std;
template<typename T1,typename T2>
double Sum(T1&t1, T2&t2){
auto s=t1+t2;//s的類型會在模板實例化時被推導出來
return s;
}
其中,模板函數Sum的參數的t1和t2類型都是不確定的,因此t1+t2這個表達式將返回的類型也就不可由Sum的編寫者確定。無疑,這樣的狀況會限制模板的使用范圍和編寫方式。 最好的解決辦法就是讓編譯器輔助進行類型推導。
與auto類似地,decltype也能進行類型推導,不過兩者的使用方式卻又一定的區別。
#include<iostream>
#include<typeinfo>
using namespace std;
int main(){
int i;
decltype(i)j=0;
cout<<typeid(j).name() <<endl;//打印出"i",g++表示int
float a;
double b;
decltype(a+b)c;
cout<<typeid(c).name() <<endl;//打印出"d",g++表示double
}
變量j的類型由decltype(i)進行聲明,表示j的類型跟i相同(或者準確地說,跟i這個表達式返回得類型相同)。而c的類型則跟(a+b)這個表達式返回的類型相同。而由于a+b加法表達式返回的類型為double(a會被擴展為double類型與b相加),所以c的類型被decltype推導為double。
decltype的類型推導并不是像auto一樣是從 變量聲明的初始化表達式 獲得變量的類型,decltype總是以一個普通的表達式為參數,返回該表達式的類型。而與auto相同的是,作為
一個類型指示符,decltype可以將獲得的類型來定義另外一個變量。與auto相同的是,作為一個類型指示符,decltype可以將獲得的類型來定義另外一個變量。與auto相同,decltype類型推導也是在編譯時進行的。
使用decltype推導類型是非常常見的事情。比較典型的就是decltype與typdef/using的合用。在C++11的頭文件中,我們常常能看以下這樣的代碼:
using size_t=decltype(sizeof(0));
using ptrdiff_t=decltype((int*)0-(int*)0);
using nullptr_t=decltype(nullptr);
這里的size_t以及ptrdiff_t還有nullptr_t都是由decltype推導出的類型。在一些常量、基本類型、運算符、操作符都已經被定義好的情況下,類型可以按照規則被推導出來。而使用using, 都可以為這些類型取名。這就顛覆了之前類型拓展需要將擴展類型"映射"到基本類型的常規做法。
此外,decltype在某些場景下,可以極大地增加代碼的可讀性。
#include<iostream>
#include<vector>
using namespace std;
int main(){
vector<int> vec;
typedef decltype(vec.begin()) vectype;
for(vectype i=vec.begin();i<vec.end();i++){
//做一些事情
}
for(decltype(vec)::iterator i=vec.begin();i<vec.end();i++){
//做一些事情
}
}
//需要默寫
(C++的 vector容器 和typedef
1、vector容器基本操作
(1) 頭文件#include<vector>,
(2) 創建vector對象, vector<int> vec;
(3) 尾部插入數字:vec.push_back(a);
(4) 使用下標訪問元素,cout<<vec[0]<<endl;
下標是從0開始的。
(5) 使用迭代器訪問元素.
vector<int>:: iterator it;
for(it=vec.begin();it!=vec.end();it++)
cout<<*it<<endl;
(6) 插入元素: vec.insert(vec.begin()+i,a);在第i+1個元素前面插入a; (7) 刪除元素:
vec.erase(vec.begin()+2);刪除第3個元素
vec.erase(vec.begin()+i,vec.end()+j);刪除區間[i,j-1];區間從0開始
(8) 向量大?。簐ec.size();
(9) 清空:vec.clear();
2、typedef用法小結
A :定義一種類型的別名,而不只是簡單的宏替換??梢杂米魍瑫r聲明指針型的多個對象。比如:
char* pa,pb
這多數不符合我們的意圖,它只聲明了一個指向字符變量的指針,和一個字符變量。
但用如下方法可以:
typedef char* PCHAR; //一般用大寫
PCHAR pa,pb;//可行,同時聲明了兩個指向字符變量的指針。
//雖然:char *pa,*pb;也可行,但相對來說沒有用typedef的形式直觀,尤其在需要大量指針的地方,typedef的方式更省事。
B :用typedef來定義與平臺無關的類型。
比如定義一個叫 REAL 的浮點類型,在目標平臺一上,讓它表示最高精度的類型為:
typedef long double REAL;
在不支持long double 的平臺二上,改為:
typedef double REAL;
在連double都不支持的平臺三上,改為:typedef float REAL;
也就是說,當跨平臺時,只要改下typedef本身就行,不用對其他源碼做任何修改。
另外,因為typedef是定義了一種類型的新別名,不是簡單的字符串替換,所以它3比宏來得更加穩?。m然用宏有時可以完成以上的用途)。
我們定義了vector的iterator的類型。這個類型還可以再main函數中重用。當我們遇到一些具有復雜類型的變量或表達式時,就可以利用decltype和typedef/using的組合來將其轉化為一個簡單的表達式,這樣在以后的代碼寫作中可以提高可讀性和可維護性。此外我們可以看到decltype(vec)::iterator這樣的靈活用法,這看起來和auto非常類似,也類似于是一種"占位符"式的替代。在C++11中,我們有時會遇到匿名的類型,而擁有了decltype這個利器之后,重用匿名類型也并非難事。
#include<iostream>
using namespace std;
enum class{K1,K2,K3} anon_e;//匿名的強類型枚舉
union{
decltype(anon_e) key;
char*name;
}anon_u; // 匿名的Union聯合體
struct{
int d;
decltype(anon_u) id;
}anon_s[100];//匿名的struct數組
int main() {
decltype(anon_s) as;
as[0].id.key=decltype(anon_e)::K1;//引用匿名強類型枚舉中的值
}
(匿名枚舉的功能等價于靜態常成員變量)
(問題:什么是匿名的強類型枚舉)
匿名的強類型枚舉anon_e、匿名的聯合體anon_u,以及匿名的結構體數組anon_s??梢钥吹剑灰ㄟ^匿名類型的變量名anon_e、anon_u,以及anon_s, decltype可以推導其類型并且進行重用。
有了decltype,我們可以適當擴大模板泛型的能力。如果稍微改變下函數模板的接口,就可以將該目標適用于更大的范圍。
#include<iostream>
using namespace std;
template<typename T1, typename T2>
void Sum(T1& t1, T2&t2, decltype(t1+t2)&s){
s=t1+t2;
}
int main(){
int a=3;
long b=5;
float c=1.0f, d=2.3f;
long e;
float f;
Sum(a,b,e); //s的類型被推導為long
Sum(c,d,f); //s的類型被推導為float
}
代碼Sum函數模板增加了類型為decltype(t1+t2)的s作為參數,而函數本身不返回任何值。這樣一來,Sum的適用性增加,返回的類型是根據t1+t2推導而來的類型。
這里最大的問題,在于返回值得類型必須一開始就被指定,程序員必須清楚Sum運算的結果使用什么樣的類型來存儲是合適的(這里指的是long e 和float f)對于一些泛型編程中依然不能滿足需求。解決的方法是結合decltype和auto關鍵字,使用追蹤返回類型的函數定義來使得編譯器對函數返回值進行推導。
某些情況下,模板庫的使用人員可能認為一些自然而簡單的數據結構,比如數組,也是可以被模板類所包含的。不過很明顯,如果t1+t2是兩個數組,t1+t2不會是合法的表達式。為了避免不必要的誤解,模板庫的開發人員應該為這些特殊情況考慮其他的版本。
#include<iostream>
using namespace std;
template<typename T1, typename T2>
void Sum(T1& t1, T2&t2, decltype(t1+t2)&s){
s=t1+t2;
}
void Sum(int a[], int b[], int c[]){
//數組版本
}
int main(){
int a[5], b[5], c[5];
Sum(a,b,c);//選擇數組版本
int d,e,f;
Sum(d,e,f);//選擇模板的實例化版本
}
//默寫
由于聲明了數組版本Sum,編譯器在編譯Sum(a,b,c)的時候,會優先選擇數組版本,而編譯Sum(d,e,f)的時候,依然會對應到模板的實例化版本。這就能夠保證Sum模板函數最大的可能性。
#include<map>
using namespace std;
int hash(char*);
map<char*, decltype(hash)>dict_key;//無法通過編譯
map<char*, decltype(hash(nullptr))> dict_key1;
標準庫中的map模板。因為該map是為了存儲字符串以及與其對應的哈希值的,因此我們可以decltype(hash(nullptr)) 來確定哈希值得類型。這樣的定義非常直觀。但是需要注意的是,decltype只能接受表達式做參數,像函數名做參數的表達式decltype(hash)是無法通過編譯的。
(問題:標準庫的map函數介紹)
事實上,decltype在C++11的標準庫中也有一些應用,一些標準庫的實現也會依賴于decltype的類型推導。一個典型的例子是基于decltype的模板類result_of,其作用是推導函數的返回類型。我們可以看一下應用的實例。
#include<type_traits>
using namespace std;
typedef double(*func)(); //double型的函數指針。
int main(){
result_of<func()>:: type f;//由func()推導其結果類型
}
(typedef 用途四:為復雜的聲明定義一個新的簡單的別名。方法是:在原來的聲明里逐步用別名替換一部分復雜聲明,如此循環,把帶變量名的部分留到最后替換,得到的就是原聲明的最簡化版)
從變量名看起,先往右,再往左,碰到一個圓括號就調轉閱讀的方向;括號內分析完就跳出括號,還是按先右后左的順序,如此循環,直到整個聲明分析完。舉例:
int (func)(int p);
首先找到變量名func,外面有一對圓括號,而且左邊是一個號,這說明func是一個指針;然后跳出這個圓括號,先看右邊,又遇到圓括號,這說明(func)是一個函數,所以func是一個指向這類函數的指針,即函數指針,這類函數具有int類型的形參,返回值類型是int。
int (func[5])(int );
func右邊是一個[]運算符,說明func是具有5個元素的數組;func的左邊有一個,說明func的元素是指針(注意這里的不是修飾func,而是修飾func[5]的,原因是[]運算符優先級比高,func先跟[]結合)。跳出這個括號,看右邊,又遇到圓括號,說明func數組的元素是函數類型的指針,它指向的函數具有int*類型的形參,返回值類型為int。
也可以記住2個模式:
type ()(....)函數指針
type ()[]數組指針
這里的f 的類型最終被推導為double,而result_of并沒有真正調用func() 這個函數,這一切都是因為底層的實現使用了decltype。result_of的一個可能的實現方式如下:
#include<iostream>
using namespace std;
template<class>
struct result_of;
template<class F,class... ArgTypes>
struct result_of<F(ArgTypes...)>
{
typedef decltype(
std:: declval<F>()(std:: declval<ArgTypes>()...)
)type;
};
這里標準庫將decltype作用于函數調用上(使用了變長函數模板),并將函數調用表達式返回的類型typedef為一個名為type的類型。這樣一來,result_of<func()>:: type就會被decltype推導為double.
decltype推導四規則
作為auto的伙伴,decltype在C++11中非常重要,不過和auto一樣,由于應用廣泛,所以使用decltype也有很多的細則條款需要注意。很多時候,用戶會發現decltype的行為不如預期,那么下面的這些規則將解釋為什么這些行為不如預期。
int i;
decltype(i) a;//a:int
decltype((i)) b;//b:int& ,無法編譯通過
我們在編譯的時候,發現decltype((i)) b; 這樣的語句編譯不過。編譯器會提示b是一個引用,但沒有被賦初值。
當程序員用decltype(e)來獲取類型時,編譯器將依序判斷以下四規則:
1、如果e是一個沒有帶括號的標記符表達式或者類成員訪問表達式,那么decltype(e)就是e所命名的實體的類型。此外,如果e是一個被重載的函數,則會導致編譯時錯誤。
2、否則,假設e的類型是T,如果e是一個將亡值,那么decltype(e)為T&&。
3、否則,假設e的類型是T,如果a是一個左值,則decltype(e)為T&。
4、否則,假設e的類型是T,則decltype(e)為T。
標記符表達式:基本上,所有除去關鍵字、字面量等編譯器需要使用的標記之外的程序員自定義的標記都可以是標記符。而單個標記符對應的表達式就是標記符表達式。
int arr[4];
那么arr是一個標記符表達式,而arr[3]+0, arr[3]等,則都不是標記符表達式。
decltype(i)a,使用了推導規則1--因為i是一個標記符表達式,所以類型被推導為int。而decltype((i)) b;中,由于(i)不是一個標記符表達式,但卻是一個左值表達式(可以有具體的地址),因此,按照decltype推導規則3,其類型應該是一個int的引用。
{回顧下:左、右值以及左值引用和右值引用:}
不過C++中,有一個被廣泛認同的說法,那就是可以取值的、有名字的就是左值,反之,不能取地址的、沒有名字的就是右值。
【在C++11中,右值引用就是對一個右值進行引用的類型。事實上,由于右值通常不具有名字,我們也只能通過引用的方式找到它的存在。
右值引用和左值引用都是屬于引用類型。無論是聲明一個左值引用還是右值引用,都必須立即進行初始化。原因可以理解為是引用類型本身自己并不擁有所綁定對象的內存,只是該對象的一個別名。左值引用是具名變量值的別名,而右值引用則是不具名(匿名)變量的別名。(也就是說需要找一個寄主)】
#include<vector>
#include<iostream>
using namespace std;
int i=4;
int arr[5]={0};
int *ptr=arr;
struct S{
double d;
}s;
void Overloaded(int);
void Overloaded(char);//重載的函數
int&& RvalRef();
const bool Func(int);
//規則1:單個標記符表達式以及訪問類成員,推導為本類型
decltype(arr) var1;//int[5], 標記符表達式
decltype(ptr) var2;//int*, 標記符表達式
decltype(s.d) var4;//double, 成員訪問表達式
//decltype(Overloaded) var5;//無法通過編譯,是個重載的函數
//規則2:將亡值,推導為類型的右值引用
decltype(RvalRef()) var6=1; //int&& 而對于右值引用而言,一定要賦初始化。
//規則3:左值,推導為類型的引用
decltype(true?i:i) var7=i;//int&,三元運算符,這里返回一個i的左值
decltype((i)) var8=i;//int&,帶圓括號的左值
decltype(++i) var9=i;//int&,++i返回i的左值
decltype(arr[3]) var10=i;//int&[] 操作返回左值
decltype(*ptr) var11=i;//int&* 操作返回左值
decltype("lval") var12="lval";//const char(&)[9], 字符串字面常量為左值
//規則4:以上都不是,推導為本類型
decltype(1) var13;//int,除字符串外字面常量為右值
decltype(i++) var14;//int,i++返回右值
decltype((Func(1))) var15;//const bool,圓括號可以忽略。
int main(){
return 0;
}
通過decltype(++i)和decltype(i++)可以看出編譯器所識別的不同的類型。
可以看到,規則1不但適用于基本數據類型,還適用于指針、數組、結構體,甚至函數類型的推導,事實上,規則1在decltype類型推導中運用的最為廣泛。
規則3其實是一個左值規則。decltype的參數不是標志表達式或者類成員訪問表達式,且參數都為左值,推導出的類型均為左值引用。規則4則是適用于以上都不適用者。
引起麻煩的只能是規則3帶來的左值引用的推導。一個簡單的能夠讓編譯器提示的方法是:如果使用decltype定義變量,那么先聲明這個變量,再在其他語句里對其進行初始化。這樣一來,由于左值引用總是需要初始化的,編譯器會報錯提示。另外一些時候,C++11標準庫中添加的模板類is_lvalue_reference 或者 is_rvalue_reference,可以幫助程序員進行一些推導結果的識別。
#include<type_traits>
#include<iostream>
using namespace std;
int i=4;
int arr[5]={0};
int *ptr=arr;
int *ptr=arr;
int&&RvalRef();
int main(){
cout<<is_rvalue_reference<decltype(RvalRef())>:: value<<endl;//1
cout<<is_lvalue_reference<decltype(true?i:i)>:: value<<endl;//1
cout<<is_lvalue_reference<decltype((i))>:: value<<endl;//1
cout<<is_lvalue_reference<decltype(++i)>:: value<<endl;//1
cout<<is_lvalue_reference<decltype(arr[3])>:: value<<endl;//1
cout<<is_lvalue_reference<decltype(*ptr)>:: value<<endl;//1
cout<<is_lvalue_reference<decltype("lval")>:: value<<endl;//1
cout<<is_lvalue_reference<decltype(i++)>:: value<<endl;//0
cout<<is_rvalue_reference<decltype(i++)>:: value<<endl;//0
}
如果程序員在書寫中不是非常確定decltype是否將類型推導為左值引用,也可以通過這樣的小實驗來輔助確定。
cv限制符的繼承與冗余的符合
與auto類型推導時不能"帶走" cv限制符不同,decltype是能夠"帶走"表達式的cv限制符的。不過,如果對象的定義中有const或volatile限制符,使用decltype進行推導時,其成員不會繼承const或volatile限制符。
#include <type_traits>
#include <iostream>
using namespace std;
const int ic=0;
volatile int iv;
struct S{int i;};
const S a={0};//結構體變量a
volatile S b;//結構體變量b
volatile S *p=&b;//結構體指針p
int main(){
cout<< is_const<decltype(ic)>:: value<<endl;//1
cout<< is_volatile<decltype(iv)>:: value<<endl;//1
cout<< is_const<decltype(a)>:: value<<endl;//1
cout<< is_volatile<decltype(b)>:: value<<endl;//1
cout<< is_const<decltype(a.i)>:: value<<endl;//0,成員不是const
cout<< is_volatile<decltype(p->i)>:: value<<endl;//0,成員不是volatile
}
運用C++庫<type_traits>函數里的is_const和is_volatile來查看類型是否是常量或者易失的。可以看到,結構體變量a、b和結構體指針p和cv限制符并沒有出現在其成員的decltype類型推導結果中。
而與auto相同的,decltype從表達式推導出類型后,進行類型定義時,也會允許一些冗余的符號。比如cv限制符以及引用符號&,通常情況下,如果推導出的類型以及有了這些屬性,冗余的符號則會被忽略:
#include <type_traits>
#include <iostream>
using namespace std;
int i=1;
int &j=i;
int *p=&i;
const int k=1;
int main(){
decltype(i) &var1=i;
decltype(j) &var2=i;//冗余的&,被忽略。 這里依然是 int& 左值引用
cout<<is_lvalue_reference<decltype(var1)>:: value<<endl;//1,是左值引用
cout<<is_rvalue_reference<decltype(var2)>:: value<<endl;//0,不是右值引用
cout<<is_lvalue_reference<decltype(var2)>:: value<<endl;//1,是左值引用
decltype(p) *var3=&i;//無法通過編譯
decltype(p) *var3=&p;//var3的類型是Int**
auto *v3=p;//v3的類型是Int*
v3=&i;
const decltype (k) var4=1;//冗余的const,被忽略
}
我們定義了類型為decltype(i)& 的變量var1, 以及類型為decltype(j)&的變量var2。由于i的類型為int,所以這里的引用符號保證var1成為一個int&引用類型。而由于j本身就是一個int&的引用類型,所以decltype之后的&成為了冗余符號,會被編譯器忽略,因此j的類型依然是int&.
這里特別需要注意的是decltype(p)*的情況。可以看到,在定義var3變量的時候,由于p的類型是int*,因此var3被定義為了int**類型,這跟auto聲明中,*也可以是冗余的不同。在decltype后的*號,并不會被編譯器忽略。
var4中const可以被冗余的聲明,但會被編譯器忽略,同樣的情況也會發生在volatile限制符上。下面的追蹤返回類型的函數定義,則將融合auto、decltype,將C++11中的泛型能力提升到更高的水平。
使用追蹤放回類型的函數
追蹤返回類型的函數和普通函數的聲明最大的區別在于返回類型的后置。在一般情況下,普通函數的聲明方式會明顯簡單于最終返回類型。比如:
int func(char* a,int b);
//比 auto func(char* a,int b) ->int; 好很多
有的時候,追蹤返回類型聲明的函數也會帶來大家一些意外:
class OuterType{
struct InnerType{int i;};
InnerType GetInner(); //返回值是結構體:InnerType。
InnerType it; //變量是it結構體。
};
//可以不寫OuterType::InnerType
auto OuterType::GetInner() ->InnerType{
return it;
}
// 這里不太明白,為什么這里需要這樣寫;
InnerType 不必寫明其作用域。
因為返回類型后置,使模板中的一些類型推導就成為了可能。
#include <type_traits>
#include <iostream>
using namespace std;
template<typename T1,typename T2>
auto Sum(const T1&t1,const T2&t2)->decltype(t1+t2){
return t1+t2;
}
template<typename T1,typename T2>
auto Mul(const T1&t1,const T2&t2)->decltype(t1*t2){
return t1*t2;
}
int main(){
auto a=3;
auto b=4L;
auto pi=3.14;
auto c=Mul(Sum(a,b),pi);
cout<<c<<endl;//21.98
}
我們定義了兩個模板函數Sum和Mul,它們的參數的類型和返回值都是實例化時決定。而由于main函數中還使用了auto,整個例子中沒有看到一個"具體"的類型聲明。這一切都要歸功于類型推導幫助下的泛型編程。程序員在編寫時無需關心任何時段的類型選擇,編譯器會合理地進行推導,而簡單程序的書寫也由此得到了極大的簡化。
追蹤返回類型的另一個優勢是簡化函數的定義,提高代碼的可讀性。
#include <type_traits>
#include <iostream>
using namespace std;
//有的時候,你會發現這是面試題
int (*(*pf())())(){
return nullptr;
}
//auto(*)() ->int(*)() 一個返回函數指針的函數(假設為a函數)
//auto pf1() ->auto(*)() ->int(*)() 一個返回a函數的指針的函數
auto pf1() ->auto(*)() ->int(*)(){
return nullptr;
}
int main(){
cout<<is_same<decltype(pf),decltype(pf1)>:: value<<endl;//1
}
我們定義了兩個類型完全一樣的函數pf和pf1。其返回的都是一個函數指針。而該函數指針又指向一個返回函數指針的函數。這一點通過is_same的成員value已經能夠確定了。而仔細看一看函數類型的聲明,可以發現老式的聲明法可讀性非常差。而追蹤返回類型只需要依照從右向左的方式,就可以將嵌套的聲明解析出來。大大提高了嵌套函數這類代碼的可讀性。
除此之外,追蹤返回類型也被廣泛地應用在轉發函數中:
#include <type_traits>
#include <iostream>
using namespace std;
double foo(int a){
return(double)a+0.1;
}
int foo(double b){
return(int)b;
}
template<class T>
auto Forward(T t)->decltype(foo(t)){
return foo(t);
}
int main(){
cout<<Forward(2)<<endl;//2.1
cout<<Forward(0.5)<<endl;//0
}
由于使用了追蹤返回類型,可以實現參數和返回類型不同時的轉發。
追蹤返回類型還可以用于函數指針中,聲明方式與追蹤返回類型的函數比起來是一樣的。
auto(*fp)()->int; 和 int(*fp)();
函數引用也是一樣適用的:
auto(&fr)()->int; 和 int(&fr)(); 也是等價的
另外,沒有返回值得函數也可以被聲明為追蹤返回類型,程序員只需要將返回類型聲明為void即可。
基于范圍的for循環。
在C++98標準中,如果要遍歷一個數組,通常會需要如下代碼:
#include<iostream>
using namespace std;
int main(){
int arr[5]={1,2,3,4,5};
int *p;
for(p=arr;p<arr+sizeof(arr)/sizeof(arr[0]); ++p){
*p*=2;
}
for(p=arr;p<arr+sizeof(arr)/sizeof(arr[0]); ++p){
cout<<*p<<'\t';
}
}
用指針p來遍歷數組arr中的內容,兩個循環分別完成了每個元素自乘以2和打印工作。而C++的標準模板庫中,我們還可以找到形如for_each的模板函數。如果我們使用for_each來完成代碼的工作,代碼將會是:
#include<algorithm>
#include<iostream>
using namespace std;
int action1(int&e){
e*=2;
}
int action2(int&e){
cout<<e<<'\t';
}
int main(){
int arr[5]={1,2,3,4,5};
for_each(arr,arr+sizeof(arr)/sizeof(arr[0]),action1);
for_each(arr,arr+sizeof(arr)/sizeof(arr[0]),action2);
}
for_each使用了迭代器的概念,其迭代器就是指針。由于迭代器內含了自增操作的概念,所以,++p操作則可以不寫在for_each循環中了。不過都需要告訴循環體其界限的范圍,即arr到arr+sizeof(arr)/sizeof(arr[0])之間,才能按元素執行操作。
很多時候,對于一個有范圍的集合而言,由程序員來說明循環的范圍是多余的,也是容易犯錯誤的。而C++11也引入了基于范圍的for循環,就很好地解決了這個問題。
#include<algorithm>
#include<iostream>
using namespace std;
int main(){
int arr[5]={1,2,3,4,5};
for(int&e:arr) e*=2;
for(int&e:arr) cout<<e<<'\t';
}
這是個基于范圍的for循環的實例。for循環后的括號由冒號":"分為兩部分,第一部分是范圍內用于迭代的變量,第二部分則表示將被迭代的范圍。在上面的例子中,表示的是在數組arr中用迭代器e進行遍歷。這樣一來,遍歷數組和STL容器就非常容易了。
基于范圍的for循環中迭代的變量采用了引用的形式,如果迭代變量的值在循環中不會被改變,那我們完全可以不用引用的方式來做迭代變量。
for(int e:arr)
cout<<e<<'\t';
或者用auto類型指示符,循環會更加簡練。
for(auto e:arr)
cout<<e<<'\t';
基于范圍的for循環跟普通循環是一樣的,可以用continue語句跳出循環的本次迭代,而用break語句來跳出整個循環。
能否使用基于范圍的for循環,必須依賴于一些條件。首先,就是for循環迭代的范圍是可確定的。對于類來說,如果該類有begin和end函數,那么begin和end之間就是for循環迭代的范圍。對于數組而言,就是數組的第一個和最后一個元素間的范圍。其次,基于范圍的for循環還要求迭代的對象實現++和==等操作符。對于標準庫中的容器,如string,array,vector,deque,list,queue,map,set等,不會有問題,因為標準庫總是保證其容器定義了相關的操作。
如:
#include<iostream>
using namespace std;
int func(int a[]){
for(auto e:a)
cout<<e;
}
int main(){
int arr[] = {1,2,3,4,5};
func(arr);
}
上述代碼會報錯,因為作為參數傳遞而來的數組a的范圍不能確定,因此也就不能使用基于范圍循環for循環對其進行迭代的操作。
如果使用auto來聲明迭代的對象的話,那么這個對象不會是迭代器對象。
#include<vector>
#include<iostream>
using namespace std;
int main(){
vector<int> v={1,2,3,4,5};
for(auto i=v.begin();i!=v.end();++i)
cout<<*i<<endl;//i是迭代器對象
for(auto e:v)
cout<<e<<endl;//e是解引用后的對象
}
(補充內容)C/C++中volatile關鍵字詳解
用volatile的原因:
C/C++中的volatile關鍵字和const對應,用來修飾變量,通常用于建立語言級別的 memory barrier。
volatile 關鍵字是一種類型修飾符,用它聲明的類型變量表示可以被某些編譯器未知的因素更改,比如:操作系統、硬件或者其它線程等。遇到這個關鍵字聲明的變量,編譯器對訪問該變量的代碼就不再進行優化,從而可以提供對特殊地址的穩定訪問。聲明時語法:int volatile vInt; 當要求使用 volatile 聲明的變量的值的時候,系統總是重新從它所在的內存讀取數據,即使它前面的指令剛剛從該處讀取過數據。而且讀取的數據立刻被保存。
volatile 指出 i 是隨時可能發生變化的,每次使用它的時候必須從i的地址中讀取,因而編譯器生成的匯編代碼會重新從i的地址讀取數據放在 b 中。編譯器對訪問該變量的代碼就不再進行優化,而優化做法是,由于編譯器發現兩次從i讀數據的代碼之間的代碼沒有對 i 進行過操作,它會自動把上次讀的數據放在 b 中。而不是重新從 i 里面讀。這樣以來,如果 i是一個寄存器變量或者表示一個端口數據就容易出錯,所以說 volatile 可以保證對特殊地址的穩定訪問。
其實不只是“內嵌匯編操縱?!边@種方式屬于編譯無法識別的變量改變,另外更多的可能是多線程并發訪問共享變量時,一個線程改變了變量的值,怎樣讓改變后的值對其它線程 visible。一般說來,volatile用在如下的幾個地方:
- 中斷服務程序中修改的供其它程序檢測的變量需要加volatile;
- 多任務環境下各任務間共享的標志應該加volatile;
- 存儲器映射的硬件寄存器通常也要加volatile說明,因為每次對它的讀寫都可能由不同意義;
多線程下的volatile
有些變量是用volatile關鍵字聲明的。當兩個線程都要用到某一個變量且該變量的值會被改變時,應該用volatile聲明,該關鍵字的作用是防止優化編譯器把變量從內存裝入CPU寄存器中。如果變量被裝入寄存器,那么兩個線程有可能一個使用內存中的變量,一個使用寄存器中的變量,這會造成程序的錯誤執行。volatile的意思是讓編譯器每次操作該變量時一定要從內存中真正取出,而不是使用已經存在寄存器中的值,如下:
volatile BOOL bStop = FALSE;
(1) 在一個線程中:
while( !bStop ) { ... }
bStop = FALSE;
return;
(2) 在另外一個線程中,要終止上面的線程循環:
bStop = TRUE;
while( bStop ); //等待上面的線程終止,如果bStop不使用volatile申明,那么這個循環將是一個死循環,因為bStop已經讀取到了寄存器中,寄存器中bStop的值永遠不會變成FALSE,加上volatile,程序在執行時,每次均從內存中讀出bStop的值,就不會死循環了。
這個關鍵字是用來設定某個對象的存儲位置在內存中,而不是寄存器中。因為一般的對象編譯器可能會將其的拷貝放在寄存器中用以加快指令的執行速度,例如下段代碼中:
...
int nMyCounter = 0;
for(; nMyCounter<100;nMyCounter++)
{
...
}
...
在此段代碼中,nMyCounter的拷貝可能存放到某個寄存器中(循環中,對nMyCounter的測試及操作總是對此寄存器中的值進行),但是另外又有段代碼執行了這樣的操作:nMyCounter -= 1;這個操作中,對nMyCounter的改變是對內存中的nMyCounter進行操作,于是出現了這樣一個現象:nMyCounter的改變不同步。