總的來(lái)說(shuō)C++17相比C++11的新特性來(lái)說(shuō)新特性不算多,做了一些小幅改進(jìn)。C++17增加了數(shù)十項(xiàng)新特性,值得關(guān)注的特性大概有下面這些:
constexpr if
constexpr lambda
fold expression
void_t
structured binding
std::apply, std::invoke
string_view
parallel STL
inline variable
剩下的有一些來(lái)自于boost庫(kù),比如variant,any、optional和filesystem等特性,string_view其實(shí)在boost里也有。還有一些是語(yǔ)法糖,比如if init、deduction guide、guaranteed copy Elision、template、nested namespace、single param static_assert等特性。我接下來(lái)會(huì)介紹C++17主要的一些特性,介紹它們的基本用法和作用,讓讀者對(duì)C++17的新特性有一個(gè)基本的了解。
fold expression
C++11增加了一個(gè)新特性可變模版參數(shù)(variadic template),它可以接受任意個(gè)模版參數(shù)在參數(shù)包中,參數(shù)包是三個(gè)點(diǎn)…,它不能直接展開(kāi),需要通過(guò)一些特殊的方法才能展開(kāi),導(dǎo)致在使用的時(shí)候有點(diǎn)難度。現(xiàn)在C++17解決了這個(gè)問(wèn)題,讓參數(shù)包的展開(kāi)變得容易了,F(xiàn)old expression就是方便展開(kāi)參數(shù)包的。
fold expression的語(yǔ)義
fold expression有4種語(yǔ)義:
unary right fold (pack op …)
unary left fold (… op pack)
binary right fold (pack op … op init)
binary left fold (init op … op pack)
其中pack代表變參,比如args,op代表操作符,fold expression支持32種操作符:
引用
+ - * / % ^ & | = > += -= *= /= %= ^= &= |= >= == != = && || , .* ->*
1
+ - * / % ^ & | = > += -= *= /= %= ^= &= |= >= == != = && || , .* ->*
unary right fold的含義
fold (E op …) 意味著 E1 op (… op (EN-1 op EN)).
顧名思義,從右邊開(kāi)始fold,看它是left fold還是right fold我們可以根據(jù)參數(shù)包…所在的位置來(lái)判斷,當(dāng)參數(shù)包…在操作符右邊的時(shí)候就是right fold,在左邊的時(shí)候就是left fold。我們來(lái)看一個(gè)具體的例子:
template<typename... Args> auto add_val(Args&&... args) { return (args + ...); } auto t = add_val(1,2,3,4); //10
template<typename... Args> auto add_val(Args&&... args) { return (args + ...); }
auto t = add_val(1,2,3,4); //10
right fold的過(guò)程是這樣的:(1+(2+(3+4))),從右邊開(kāi)始fold。
unary left fold的含義
fold (… op E) 意味著 ((E1 op E2) op …) op EN。
對(duì)于+這種滿足交換律的操作符來(lái)說(shuō)left fold和right fold是一樣的,比如上面的例子你也可以寫(xiě)成left fold。
template<typename... Args> auto add_val(Args&&... args) { return (... + args); } auto t = add_val(1,2,3,4); //10
template<typename... Args>
auto add_val(Args&&... args) {
return (... + args);
}
auto t = add_val(1,2,3,4); //10
對(duì)于不滿足交換律的操作符來(lái)說(shuō)就要注意了,比如減法。
template<typename... Args> auto sub_val_right(Args&&... args) { return (args - ...); } template<typename... Args> auto sub_val_left(Args&&... args) { return (... - args); } auto t = sub_val_right(2,3,4); //(2-(3-4)) = 3 auto t1 = sub_val_left(2,3,4); //((2-3)-4) = -5
template<typename... Args>
auto sub_val_right(Args&&... args) {
return (args - ...);
}
template<typename... Args>
auto sub_val_left(Args&&... args) {
return (... - args);
}
auto t = sub_val_right(2,3,4); //(2-(3-4)) = 3
auto t1 = sub_val_left(2,3,4); //((2-3)-4) = -5
這次right fold和left fold的結(jié)果就不一樣。
binary fold的含義
Binary right fold (E op … op I) 意味著 E1 op (… op (EN-1 op (EN op I)))。
Binary left fold (I op … op E) 意味著 (((I op E1) op E2) op …) op E2。
其中E代表變參,比如args,op代表操作符,I代表一個(gè)初始變量。
二元fold的語(yǔ)義和一元fold的語(yǔ)義是相同的,看一個(gè)二元操作符的例子:
template<typename... Args> auto sub_one_left(Args&&... args) { return (1 - ... - args); } template<typename... Args> auto sub_one_right(Args&&... args) { return (args - ... - 1); } auto t = sub_one_left(2,3,4);// (((1-2)-3)-4) = -8 auto t1 = sub_one_right(2,3,4);//(2-(3-(4-1))) = 2
template<typename... Args>
auto sub_one_left(Args&&... args) {
return (1 - ... - args);
}
template<typename... Args>
auto sub_one_right(Args&&... args) {
return (args - ... - 1);
}
auto t = sub_one_left(2,3,4);// (((1-2)-3)-4) = -8
auto t1 = sub_one_right(2,3,4);//(2-(3-(4-1))) = 2
相信通過(guò)這個(gè)例子大家應(yīng)該對(duì)C++17的fold expression有了基本的了解。
comma fold
在C++17之前,我們經(jīng)常使用逗號(hào)表達(dá)式和std::initializer_list來(lái)將變參一個(gè)個(gè)傳入一個(gè)函數(shù)。比如像下面這個(gè)例子:
template<typename T> void print_arg(T t) { std::cout << t << std::endl; } template<typename... Args> void print2(Args... args) { //int a[] = { (printarg(args), 0)... }; std::initializer_list<int>{(print_arg(args), 0)...}; }
template<typename T>
void print_arg(T t)
{
std::cout << t << std::endl;
}
template<typename... Args>
void print2(Args... args)
{
//int a[] = { (printarg(args), 0)... };
std::initializer_list<int>{(print_arg(args), 0)...};
}
這種寫(xiě)法比較繁瑣,用fold expression就會(huì)變得很簡(jiǎn)單了。
template<typename... Args> void print3(Args... args) { (print_arg(args), ...); }
template<typename... Args>
void print3(Args... args)
{
(print_arg(args), ...);
}
這是right fold,你也可以寫(xiě)成left fold,對(duì)于comma來(lái)說(shuō)兩種寫(xiě)法是一樣的,參數(shù)都是從左至右傳入print_arg函數(shù)。
template<typename... Args> void print3(Args... args) { (..., print_arg(args)); }
template<typename... Args>
void print3(Args... args)
{
(..., print_arg(args));
}
你也可以通過(guò)binary fold這樣寫(xiě):
template<typename ...Args> void printer(Args&&... args) { (std::cout << ... << args) << 'n'; }
template<typename ...Args>
void printer(Args&&... args) {
(std::cout << ... << args) << 'n';
}
也許你會(huì)覺(jué)得能寫(xiě)成這樣:
template<typename ...Args> void printer(Args&&... args) { (std::cout << args << ...) << 'n'; }
template<typename ...Args>
void printer(Args&&... args) {
(std::cout << args << ...) << 'n';
}
但這樣寫(xiě)是不合法的,根據(jù)binary fold的語(yǔ)法,參數(shù)包…必須在操作符中間,因此上面的這種寫(xiě)法不符合語(yǔ)法要求。
借助comma fold我們可以簡(jiǎn)化代碼,假如我們希望實(shí)現(xiàn)tuple的for_each算法,像這樣:
for_each(std::make_tuple(2.5, 10, 'a'),[](auto e) { std::cout << e<< 'n'; });
這個(gè)for_each將會(huì)遍歷tuple的元素并打印出來(lái)。在C++17之前我們?nèi)绻獙?shí)現(xiàn)這個(gè)算法的話,需要借助逗號(hào)表達(dá)式和std::initializer_list來(lái)實(shí)現(xiàn),類似于這樣:
template <typename... Args, typename Func, std::size_t... Idx> void for_each(const std::tuple& t, Func&& f, std::index_sequence<Idx...>) { (void)std::initializer_list<int> { (f(std::get<Idx>(t)), void(), 0)...}; }
template <typename... Args, typename Func, std::size_t... Idx>
void for_each(const std::tuple& t, Func&& f, std::index_sequence<Idx...>) {
(void)std::initializer_list<int> { (f(std::get<Idx>(t)), void(), 0)...};
}
這樣寫(xiě)比較繁瑣不直觀,現(xiàn)在借助fold expression我們可以簡(jiǎn)化代碼了。
template <typename... Args, typename Func, std::size_t... Idx> void for_each(const std::tuple<Args...>& t, Func&& f, std::index_sequence<Idx...>) { (f(std::get<Idx>(t)), ...); }
template <typename... Args, typename Func, std::size_t... Idx>
void for_each(const std::tuple<Args...>& t, Func&& f, std::index_sequence<Idx...>) {
(f(std::get<Idx>(t)), ...);
}
借助coma fold我們可以寫(xiě)很簡(jiǎn)潔的代碼了。
constexpr if
constexpr標(biāo)記一個(gè)表達(dá)式或一個(gè)函數(shù)的返回結(jié)果是編譯期常量,它保證函數(shù)會(huì)在編譯期執(zhí)行。相比模版來(lái)說(shuō),實(shí)現(xiàn)編譯期循環(huán)或遞歸,C++17中的constexpr if會(huì)讓代碼變得更簡(jiǎn)潔易懂。比如實(shí)現(xiàn)一個(gè)編譯期整數(shù)加法:
template<int N> constexpr int sum() { return N; } template <int N, int N2, int... Ns> constexpr int sum() { return N + sum<N2, Ns...>(); }
template<int N>
constexpr int sum()
{
return N;
}
template <int N, int N2, int... Ns>
constexpr int sum()
{
return N + sum<N2, Ns...>();
}
C++17之前你可能需要像上面這樣寫(xiě),但是現(xiàn)在你可以寫(xiě)更簡(jiǎn)潔的代碼了。
template <int N, int... Ns> constexpr auto sum17() { if constexpr (sizeof...(Ns) == 0) return N; else return N + sum17<Ns...>(); }
template <int N, int... Ns>
constexpr auto sum17()
{
if constexpr (sizeof...(Ns) == 0)
return N;
else
return N + sum17<Ns...>();
}
當(dāng)然,你也可以用C++17的fold expression:
template<typename ...Args> constexpr int sum(Args... args) { return (0 + ... + args); }
template<typename ...Args>
constexpr int sum(Args... args) {
return (0 + ... + args);
}
constexpr還可以用來(lái)消除enable_if了,對(duì)于討厭寫(xiě)一長(zhǎng)串enable_if的人來(lái)說(shuō)會(huì)非常開(kāi)心。比如我需要根據(jù)類型來(lái)選擇函數(shù)的時(shí)候:
template<typename T> std::enable_if_t<std::is_integral<T>::value, std::string> to_str(T t) { return std::to_string(t); } template<typename T> std::enable_if_t<!std::is_integral<T>::value, std::string> to_str(T t) { return t; }
template<typename T>
std::enable_if_t<std::is_integral<T>::value, std::string> to_str(T t)
{
return std::to_string(t);
}
template<typename T>
std::enable_if_t<!std::is_integral<T>::value, std::string> to_str(T t)
{
return t;
}
經(jīng)常不得不分開(kāi)幾個(gè)函數(shù)來(lái)寫(xiě),還需要寫(xiě)長(zhǎng)長(zhǎng)的enable_if,比較繁瑣,通過(guò)if constexpr可以消除enable_if了。
template<typename T> auto to_str17(T t) { if constexpr(std::is_integral<T>::value) return std::to_string(t); else return t; }
template<typename T>
auto to_str17(T t)
{
if constexpr(std::is_integral<T>::value)
return std::to_string(t);
else
return t;
}
constexpr if讓C++的模版具備if-else if-else功能了,是不是很酷,C++程序員的好日子來(lái)了。
不過(guò)需要注意的是下面這種寫(xiě)法是有問(wèn)題的。
template<typename T> auto to_str17(T t) { if constexpr(std::is_integral<T>::value) return std::to_string(t); return t; }
template<typename T>
auto to_str17(T t)
{
if constexpr(std::is_integral<T>::value)
return std::to_string(t);
return t;
}
這個(gè)代碼把else去掉了,當(dāng)輸入如果是非數(shù)字類型時(shí)代碼可以編譯過(guò),以為if constexpr在模版實(shí)例化的時(shí)候會(huì)丟棄不滿足條件的部分,因此函數(shù)體中的前兩行代碼將失效,只有一句有效。當(dāng)輸入的為數(shù)字的時(shí)候就會(huì)產(chǎn)生編譯錯(cuò)誤了,因?yàn)閕f constexpr滿足條件了,這時(shí)候就會(huì)有兩個(gè)return了,就會(huì)導(dǎo)致編譯錯(cuò)誤。
constexpr if還可以用來(lái)替換#ifdef宏,看下面的例子:
enum class OS { Linux, Mac, Windows }; //Translate the macros to C++ at a single point in the application #ifdef __linux__ constexpr OS the_os = OS::Linux; #elif __APPLE__ constexpr OS the_os = OS::Mac; #elif __WIN32 constexpr OS the_os = OS::Windows; #endif void do_something() { //do something general if constexpr (the_os == OS::Linux) { //do something Linuxy } else if constexpr (the_os == OS::Mac) { //do something Appley } else if constexpr (the_os == OS::Windows) { //do something Windowsy } //do something general }
enum class OS { Linux, Mac, Windows };
//Translate the macros to C++ at a single point in the application
#ifdef __linux__
constexpr OS the_os = OS::Linux;
#elif __APPLE__
constexpr OS the_os = OS::Mac;
#elif __WIN32
constexpr OS the_os = OS::Windows;
#endif
void do_something() {
//do something general
if constexpr (the_os == OS::Linux) {
//do something Linuxy
}
else if constexpr (the_os == OS::Mac) {
//do something Appley
}
else if constexpr (the_os == OS::Windows) {
//do something Windowsy
}
//do something general
}
代碼變得更清爽了,再也不需要像以前一樣寫(xiě)#ifdef那樣難看的代碼塊了。
constexpr lambda
constexpr lambda其實(shí)很簡(jiǎn)單,它的意思就是可以在constexpr 函數(shù)中用lambda表達(dá)式了,這在C++17之前是不允許的。這樣使用constexpr函數(shù)和普通函數(shù)沒(méi)多大區(qū)別了,使用起來(lái)非常舒服。下面是constexpr lambda的例子:
template <typename I> constexpr auto func(I i) { //use a lambda in constexpr context return [i](auto j){ return i + j; }; }
template <typename I>
constexpr auto func(I i) {
//use a lambda in constexpr context
return [i](auto j){ return i + j; };
}
constexpr if和constexpr lambda是C++17提供的非常棒的特性,enjoy it.
string_view
string_view的基本用法
C++17中的string_view是一個(gè)char數(shù)據(jù)的視圖或者說(shuō)引用,它并不擁有該數(shù)據(jù),是為了避免拷貝,因此使用string_view可以用來(lái)做性能優(yōu)化。你應(yīng)該用string_view來(lái)代替const char和const string了。string_view的方法和string類似,用法很簡(jiǎn)單:
const char* data = "test"; std::string_view str1(data, 4); std::cout<<str1.length()<<'n'; //4 if(data==str1) std::cout<<"ok"<<'n'; const std::string str2 = "test"; std::string_view str3(str2, str2.size());
const char* data = "test";
std::string_view str1(data, 4);
std::cout<<str1.length()<<'n'; //4
if(data==str1)
std::cout<<"ok"<<'n';
const std::string str2 = "test";
std::string_view str3(str2, str2.size());
構(gòu)造string_view的時(shí)候用char*和長(zhǎng)度來(lái)構(gòu)造,這個(gè)長(zhǎng)度可以自由確定,它表示string_view希望引用的字符串的長(zhǎng)度。因?yàn)樗皇且闷渌址运粫?huì)分配內(nèi)存,不會(huì)像string那樣容易產(chǎn)生臨時(shí)變量。我們通過(guò)一個(gè)測(cè)試程序來(lái)看看string_view如何來(lái)幫我們優(yōu)化性能的。
using namespace std::literals; constexpr auto s = "it is a test"sv; auto str = "it is a test"s; constexpr int LEN = 1000000; boost::timer t; for (int i = 0; i < LEN; ++i) { constexpr auto s1 = s.substr(3); } std::cout<<t.elapsed()<<'n'; t.restart(); for (int i = 0; i < LEN; ++i) { auto s2 = str.substr(3); } std::cout<<t.elapsed()<<'n'; //output 0.004197 0.231505
using namespace std::literals;
constexpr auto s = "it is a test"sv;
auto str = "it is a test"s;
constexpr int LEN = 1000000;
boost::timer t;
for (int i = 0; i < LEN; ++i) {
constexpr auto s1 = s.substr(3);
}
std::cout<<t.elapsed()<<'n';
t.restart();
for (int i = 0; i < LEN; ++i) {
auto s2 = str.substr(3);
}
std::cout<<t.elapsed()<<'n';
//output
0.004197
0.231505
我們可以通過(guò)字面量””sv來(lái)初始化string_view。string_view的substr和string的substr相比,快了50多倍,根本原因是它不會(huì)分配內(nèi)存。
string_view的生命周期
由于string_vew并不擁有鎖引用的字符串,所以它也不會(huì)去關(guān)注被引用字符串的生命周期,用戶在使用的時(shí)候需要注意,不要將一個(gè)臨時(shí)變量給一個(gè)string_view,那樣會(huì)導(dǎo)致string_view引用的內(nèi)容也失效。
std::string_view str_v; { std::string temp = "test"; str_v = {temp}; }
std::string_view str_v;
{
std::string temp = "test";
str_v = {temp};
}
這樣的代碼是有問(wèn)題的,因?yàn)槌隽俗饔糜蛑螅瑂tring_view引用的內(nèi)容已經(jīng)失效了。
總結(jié)
本文介紹了C++17的fold expression、constexpr if、constexpr lambda和string_view。fold expression為了簡(jiǎn)化可變模板參數(shù)的展開(kāi),讓可以模板參數(shù)的使用變得更簡(jiǎn)單直觀;constexpr if讓模板具備if-else功能,非常強(qiáng)大。它也避免了寫(xiě)冗長(zhǎng)的enable_if代碼,讓代碼變得簡(jiǎn)潔易懂了;string_view則是用來(lái)做性能優(yōu)化的,應(yīng)該用它來(lái)代替const char*和const string。 這些特性對(duì)之前的C++14和C++11做了改進(jìn)和增強(qiáng),非常酷。