C++14與C++17新特性,你想知道的都在這

這篇博文躺在草稿箱裏好久了,今天有點時間,整理下發出來

------------------------------------------C++14篇------------------------------------------

相對於C++11,C++14的改動可謂非常mini了,主要的改動一句話便是:擴大自動類型推斷的應用範圍。剩下的都是邊邊角角的小改動

這包括:

  • 函數返回值自動推斷
  • 泛型lambda
函數返回值推斷
以前要這樣:
int func(){
	return 10;
}
C++14後可以這樣
auto func(){
	return 10;
}

函數中假如有多條返回路徑,則程序員要保證各條路徑推斷出的結果必須是一致的,否則會編譯報錯。
另外,C++14還加入一個風騷的東西:decltype(auto)。我們知道decltype是用來提取表達式的類型的,寫個auto在裏面,能提取什麼東西出來?他與直接auto有啥區別呢?
首先得搞明白auto與decltype的細節與區別。
auto的推導規則基本源於C++模板的推導規則,先看下C++模板的推導規則:

template<typename T>
void func(ParamType param){...}
func(expr);
編譯器根據expr的類型推導出T的類型以及ParamType的類型(很多時候ParamType的類型不等於T)
template<typename T>void funcA(T param){}
template<typename T>void funcB(T& param){}
template<typename T>void funcC(T* param){}
template<typename T>void funcD(T&& param){}

int x=1;
int& xr = x;
int* xp = &x;
const int xc = 4;
const int& xcr = x;
const int* xcp = &xc;

funcA(x);   //funcA<int>(int param)
funcA(xr);  //funcA<int>(int param)
funcA(xp);  //funcA<int*>(int* param)
funcA(xc);  //funcA<int>(int param)
funcA(xcr); //funcA<int>(int param)
funcA(xcp); //funcA<const int*>(const int* param)

funcB(x);   //funcB<int>(int& param);
funcB(xr);  //funcB<int>(int& param);
funcB(xp);  //funcB<int*>(int*& param);
funcB(xc);  //funcB<const int>(const int& param);
funcB(xcr); //funcB<const int>(const int& param);
funcB(xcp); //funcB<const int*>(const int*& param);
funcB(getObj());//編譯不過(getObj返回一個右值)

funcC(x);   //編譯不過
funcC(xp);  //編譯不過
funcC(xp);  //func<int>(int* param)
funcC(xcp); //funcC<const int>(const int* param);

funcD(x);   //funcD<int&>(int& param);
funcD(xr);  //funcD<int&>(int& param);
funcD(xp);  //funcD<int*&>(int*& param);
funcD(xc);  //funcD<const int&>(const int& param);
funcD(xcr); //funcD<const int&>(const int& param);
funcD(xcp); //funcD<const int*&>(const int*& param);
funcD(getObj()); //funcD<A>(A&&)

我跟把ParamType的劃分成三種情況,來總結模板的類型推導規則。之後就可以用這些規則來解答上述代碼中的T和ParamType了

  • ParamType既不是引用也不是指針(按值傳遞,編譯器會複製一份expr傳遞給形參)
    1.假如expr是個引用,則忽略引用的部分
    2.假如expr有CV限定符(const和volatile),也忽略。特別的,如果expr是個指針,則指針指向的類型的CV限定符會被保留,指針本身的CV限定符會被忽略。
  • ParamType是一個普通引用或指針(普通引用是相對於通用引用而言)
    1.假如expr是個引用,則忽略引用的部分
    2.根據預處理後的expr的類型和ParamType 對比 ,確定T的類型。這裏 對比 的意思有點難以描述,但很容易理解: 假如ParamType = T& 而 expr = const int,則T=const int, ParamType = const int&。假如ParamType=const T& 而 expr=const int,則T=int, ParamType=const int&。假如ParamType=T*,expr=int*,則T=int,ParamType=int*
  • ParamType是一個通用引用(T&&).
    1.如果expr是一個左值,則T和ParamType都會被推導成左值引用,這是模板類型T被推導成引用的唯一情況。
    2.如果expr是一個右值,則依然通過 對比確定T的類型

auto的推導規則與模板的推導規則基本一模一樣,統一初始化這種情形下的推導是例外。auto默認大括號初始化的類型是std::initializer_list。另外auto在推導lambda參數和lambda返回值的時候用的有事模板的推導規則。

有了上面的基礎,我們再看C++14中新添加的用auto推導函數返回值的情況:

string getStr(){ return string("123");}
string& getStrRef(){ 
   string* pStr = new string("456");
   return *pStr;
}

auto funcStrA(){
   return getStr();
}

auto funcStrB(){
   return getStrRef();
}

我們期望 funcStrB的返回值是一個string&類型,但是不是,funcStrB的類型是string,爲啥呢?因爲auto用的是模板的那一套規則,會把引用符忽略掉。
decltype(auto)就是用來解決這個場景的

decltype(auto) funcStrC(){
   return getStrRef();
}
decltype(auto) funcStrD(string&& str){
	return std::forward<string>(str);
}

升級版的funcStrD,不管str是一個左值引用還是一個右值引用,都會完美返回給返回值。

泛型lambda

C++14還將自動類型推斷擴展到了lambda表達式的參數裏,我們知道函數因爲重載的原因,不能再參數裏用auto(可以用模板實現想要的效果),但lambda無需重載,C++14裏lambda的參數也能自動推斷了:

auto add = [](auto a, auto b){
	return a + b;
};
cout<< add(1, 2)<< add(string("abc"), string("def"))<<endl;

一個迷你版的模板函數有木有!
剩下的C++14特性都比較小,有:

變量模板

在C++之前的版本中,模板可以是函數模板或類模板,C++14現在也可以創建變量模板,他是下面這個樣子

template<typename T>
T var

var<int> a = 5;
var<string> b = 2.0;
....
在lambda捕獲列表中使用初始化表達式

媽蛋其他的小改動不想寫了嘿嘿嘿嘿

------------------------------------------C++17篇------------------------------------------

C++17的動作幅度比C++14稍微大了點,重磅的改動以下幾點:

結構化綁定

先直觀地看下啥是結構化綁定。例如在C++11中新推出的tuple元組,在解包的時候有點麻煩,我們得像這樣來操作:

auto tup = make_tuple("123", 12, 7.0);
string str;
int i;
double d;
std::tie(str, i, d) = tup;

C++17一行搞定
auto [x,y,z] = tup;

不光是tuple能夠蒙受恩澤,數組、結構體、全是public數據成員且沒有static數據的類、統統雨露均沾!!!看如下代碼:

double myArray[3] = { 1.0, 2.0, 3.0 };  
auto [a, b, c] = myArray;
auto& [ra, rb, rc] = myArray;
struct S { int x1 : 2; double y1; };
S f();
const auto [ x, y ] = f; //備註1:

備註1:gcc版本的編譯器對此項特性的支持有bug,詳見這裏
不僅如此,還有如下騷操作:

std::map myMap;    
for (const auto & [k,v] : myMap) 
{  
    // k - key
    // v - value
} 
std::variant

tuple相當於struct的延伸,variant則是union的延伸

std::variant<int, double, std::wstring> var{ 1.0 };
var = 1;
var = "str";
用於可變參數模板的摺疊表達式

有一個讓代碼變得簡潔的大利器(用在可變參數模板中)
例如,在C++11中我們使用可變參數模板實現一個多參數累加器,要這麼搞:

template<typename T>
auto myAdd(const T& a,const T& b){
    return a + b;
}
template<typename T, typename... RestT>
auto myAdd(const T& a, const RestT&... restArgs){
    return a + myAdd(restArgs...);
}

在C++17中,我們可以一個模板搞定

template<typename ...Args> 
auto myAddEx(const Args& ...args) { 
    return (args + ...); //編譯器會這樣幹:1+(2+(3+(4)))
    或者
    return (... + args); //編譯器會這樣幹:((1+2)+3)+4
    對於加法上述兩種表達是等效的,但是減法就不是了。另外,括號是不能省略的
}
cout<<myAddEx(1,2,3,4)<<endl;

不光加法,其他好多操作符也可以使用上述特性,這還不算啥,總共有4種摺疊方式:

名稱 表達式 展開式
一元右摺疊 (pack op …) pack1 op (… op (packN-1 op packN))
一元左摺疊 (… op pack) ((pack1 op pack2) op …) op packN
二元左摺疊 (init op … op pack) (((init op pack1) op pack2) op …) op packN
二元右摺疊 (pack op … op init) pack1 op (… op (packN-1 op (packN op init)))

名稱可以這樣記: 符號在pack的左邊就是左摺疊。

第一種第二種我們已經在myAddEx裏體驗過了,第三個第四看下面的代碼:


template<typename ...Args> 
auto weirdSub(const Args& ...args) { 
    return ( 1000 - ... - args ); // (((1000-1)-2)-3)-4 = 990
}

template<typename ...Args> 
auto weirdSub2(const Args& ...args) { 
    return ( args - ... - 1000 ); // 1-(2-(3-(4-1000))) = 998
}
cout<<weirdSub(1,2,3,4)<<endl; //990
cout<<weirdSub2(1,2,3,4)<<endl;//998

用此特性可以這樣實現print函數:(注意括號的位置)
template<typename ...Args>
void FoldPrint(Args&&... args) {
    (cout << ... << forward<Args>(args)) << '\n';
}
if constexpr

這是個大殺器!
在模板元編程中,我們經常使用模板特化、SFNAE、C++14的 std::enable_if 等特性實現條件判斷,有了if constexpr,就很爽了
例如,在以前,我們要實現一個編譯期的fibonacci函數,需要這樣:

template<int  N>
constexpr int fibonacci() {return fibonacci<N-1>() + fibonacci<N-2>(); }
template<>
constexpr int fibonacci<1>() { return 1; }
template<>
constexpr int fibonacci<0>() { return 0; }

這個例子就是利用編譯器對模板的特化來實現編譯期的if else,有了if constexpr之後,代碼就可以簡潔多了:

template<int N>
constexpr int fibonacci(){
	if constexpr(N <= 1)
		return N;
	else
		return fibonacci<N-1>() + fibonacci<N-2>;
}

那些經常用模板寫代碼的人估計要愛死這個改進了

類模板的實參推演

在之前版本的C++中,函數模板可以有顯式實例化和隱式實例化兩種實例化方式,隱式實例化,編譯器會根據實參的類型,推導,然後自動實例化函數模板。而顯式的則需程序員指定類型,來讓編譯器實例化。在類模板中,則只有顯式實例化——類模板的構造函數不支持實參推演,例如:

std::pair<int, int> p(12,3);
爲了方便,於是STL提供了下面這樣的函數模板:
auto p = std::make_pair(12,3);
實際上make_pair只是做了這樣一件事:
template<typename _T1, typename _T2>
inline pair<_T1, _T2> make_pair(_T1 __x, _T2 __y){ 
	return pair<_T1, _T2>(__x, __y); 
}
利用函數模板的實參推演,確定pair的類型,C++17之後,類模板也支持實參推演了,上述代碼就可以這樣寫了:
std::pair p(10, 0.0);
或者
auto p = std::pair(1,1);

有了這個改進,STL中很多make_XXX都不需要了,比如make_tuple,make_pair。。。。

在非類型模板形參中使用auto

所謂的“非類型類模板形參”是指這樣的情況:

template<int N>
constexpr int fibonacci(){...}
有了這個特性,我們的fibonacci函數模板可以這樣寫了:
template<auto N>
constexpr int fibonacci(){...}
調用:
fibonacci<5>();
嵌套的namespace定義

一個語法層面的改進,之前定義嵌套的命名空間要這樣:、

namespace X{
	namespace Y{
		namespace X{

		}
	}
}

C++17:
namespace X::Y::Z{

}
if/swtich語句內支持初始化
if (auto p = getValue(); p==XXX) {   
    //...
} else {
    //...

個人感覺,讀起來有點怪異。但是這種改進,使得p的作用域限制在了if語句的範圍內,考慮如下代碼:

const std::string myString = "My Hello World Wow";

const auto it = myString.find("Hello");
if (it != std::string::npos)
    std::cout << it << " Hello\n"

const auto it2 = myString.find("World");
if (it2 != std::string::npos)
    std::cout << it2 << " World\n"
    
####################################################################
if (const auto it = myString.find("Hello"); it != std::string::npos)
    std::cout << it << " Hello\n";

if (const auto it = myString.find("World"); it != std::string::npos)
    std::cout << it << " World\n";
內聯變量

我們知道內聯函數:在函數定義前面加上inline關鍵字,編譯器會根據情況,將函數處理成內聯,在調用的地方直接替換,省去一次函數調用的開銷。在C++17中,inline關鍵字也可以用來修飾變量了:

struct MyClass
{
    static const int sValue;
};

inline int const MyClass::sValue = 777;

struct MyClass2
{
    inline static const int sValue = 777;
};
----------------------------------------------------------------------總結--------------------------------------------------------------

總體上講,C++17的改動要大於C++14,C++14將auto的作用範圍擴展到了函數返回值,而C++17則極大地改進了模板元編程(if constexpr、摺疊表達式、非類型模板參數支持auto、類模板實參推演等等),同時結構化綁定也大大方便了類tuple語意的使用。這兩者相對C++11來看,都只能算小改動了,即將到來的C++20應該會有個大動作吧,concept、range、contract、module、coroutine、reflection、executor、networking。。。。。想想都雞動啊

發佈了34 篇原創文章 · 獲贊 6 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章