C++17中那些值得關注的特性(上)
C++17標準在2017上半年已經討論確定,正在形成ISO標準文件,今年晚些時候會正式釋出。本文將介紹最新標準中值得開發者關注的新特新和基本用法。
總的來說C++17相比C++11的新特性來說新特性不算多,做了一些小幅改進。C++17增加了數十項新特性,值得關注的特性大概有下面這些:
- constexpr if
- constexpr lambda
- fold expression
- void_t
- structured binding
- std::apply, std::invoke
- string_view
- parallel STL
- inline variable
剩下的有一些來自於boost庫,比如variant,any、optional和filesystem等特性,string_view其實在boost裡也有。還有一些是語法糖,比如if init、deduction guide、guaranteed copy Elision、template、nested namespace、single param static_assert等特性。我接下來會介紹C++17主要的一些特性,介紹它們的基本用法和作用,讓讀者對C++17的新特性有一個基本的瞭解。
fold expression
C++11增加了一個新特性可變模版引數(variadic template),它可以接受任意個模版引數在引數包中,引數包是三個點…,它不能直接展開,需要通過一些特殊的方法才能展開,導致在使用的時候有點難度。現在C++17解決了這個問題,讓引數包的展開變得容易了,Fold expression就是方便展開引數包的。
fold expression的語義
fold expression有4種語義:
- 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種操作符:
+ - * / % ^ & | = < > << >> += -= *= /= %= ^= &= |= <<= >>= == != <= >= && || , .* ->*
unary right fold的含義
fold (E op …) 意味著 E1 op (… op (EN-1 op EN)).
顧名思義,從右邊開始fold,看它是left fold還是right fold我們可以根據引數包…所在的位置來判斷,當引數包…在操作符右邊的時候就是right fold,在左邊的時候就是left fold。我們來看一個具體的例子:
template<typename... Args>
auto add_val(Args&&... args) {
return (args + ...);
}
auto t = add_val(1,2,3,4); //10
right fold的過程是這樣的:(1+(2+(3+4))),從右邊開始fold。
unary left fold的含義
fold (… op E) 意味著 ((E1 op E2) op …) op EN。
對於+這種滿足交換律的操作符來說left fold和right fold是一樣的,比如上面的例子你也可以寫成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 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的結果就不一樣。
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代表一個初始變數。
二元fold的語義和一元fold的語義是相同的,看一個二元操作符的例子:
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
相信通過這個例子大家應該對C++17的fold expression有了基本的瞭解。
comma fold
在C++17之前,我們經常使用逗號表示式和std::initializer_list來將變參一個個傳入一個函式。比如像下面這個例子:
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)...};
}
這種寫法比較繁瑣,用fold expression就會變得很簡單了。
template<typename... Args>
void print3(Args... args)
{
(print_arg(args), ...);
}
這是right fold,你也可以寫成left fold,對於comma來說兩種寫法是一樣的,引數都是從左至右傳入print_arg函式。
template<typename... Args>
void print3(Args... args)
{
(..., print_arg(args));
}
你也可以通過binary fold這樣寫:
template<typename ...Args>
void printer(Args&&... args) {
(std::cout << ... << args) << '\n';
}
也許你會覺得能寫成這樣:
template<typename ...Args>
void printer(Args&&... args) {
(std::cout << args << ...) << '\n';
}
但這樣寫是不合法的,根據binary fold的語法,引數包…必須在操作符中間,因此上面的這種寫法不符合語法要求。
藉助comma fold我們可以簡化程式碼,假如我們希望實現tuple的for_each演算法,像這樣:
for_each(std::make_tuple(2.5, 10, 'a'),[](auto e) { std::cout << e<< '\n'; });
這個for_each將會遍歷tuple的元素並列印出來。在C++17之前我們如果要實現這個演算法的話,需要藉助逗號表示式和std::initializer_list來實現,類似於這樣:
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)...};
}
這樣寫比較繁瑣不直觀,現在藉助fold expression我們可以簡化程式碼了。
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我們可以寫很簡潔的程式碼了。
constexpr if
constexpr標記一個表示式或一個函式的返回結果是編譯期常量,它保證函式會在編譯期執行。相比模版來說,實現編譯期迴圈或遞迴,C++17中的constexpr if會讓程式碼變得更簡潔易懂。比如實現一個編譯期整數加法:
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之前你可能需要像上面這樣寫,但是現在你可以寫更簡潔的程式碼了。
template <int N, int... Ns>
constexpr auto sum17()
{
if constexpr (sizeof...(Ns) == 0)
return N;
else
return N + sum17<Ns...>();
}
當然,你也可以用C++17的fold expression:
template<typename ...Args>
constexpr int sum(Args... args) {
return (0 + ... + args);
}
constexpr還可以用來消除enable_if了,對於討厭寫一長串enable_if的人來說會非常開心。比如我需要根據型別來選擇函式的時候:
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;
}
經常不得不分開幾個函式來寫,還需要寫長長的enable_if,比較繁瑣,通過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;
}
constexpr if讓C++的模版具備if-else if-else功能了,是不是很酷,C++程式設計師的好日子來了。
不過需要注意的是下面這種寫法是有問題的。
template<typename T>
auto to_str17(T t)
{
if constexpr(std::is_integral<T>::value)
return std::to_string(t);
return t;
}
這個程式碼把else去掉了,當輸入如果是非數字型別時程式碼可以編譯過,以為if constexpr在模版例項化的時候會丟棄不滿足條件的部分,因此函式體中的前兩行程式碼將失效,只有最後一句有效。當輸入的為數字的時候就會產生編譯錯誤了,因為if constexpr滿足條件了,這時候就會有兩個return了,就會導致編譯錯誤。
constexpr if還可以用來替換#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
}
//備註:這個例子摘自https://blog.tartanllama.xyz/c++/2016/12/12/if-constexpr/
程式碼變得更清爽了,再也不需要像以前一樣寫#ifdef那樣難看的程式碼塊了。
constexpr lambda
constexpr lambda其實很簡單,它的意思就是可以在constexpr 函式中用lambda表示式了,這在C++17之前是不允許的。這樣使用constexpr函式和普通函式沒多大區別了,使用起來非常舒服。下面是constexpr lambda的例子:
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是一個char資料的檢視或者說引用,它並不擁有該資料,是為了避免拷貝,因此使用string_view可以用來做效能優化。你應該用string_view來代替const char和const string了。string_view的方法和string類似,用法很簡單:
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());
構造string_view的時候用char*和長度來構造,這個長度可以自由確定,它表示string_view希望引用的字串的長度。因為它只是引用其他字串,所以它不會分配記憶體,不會像string那樣容易產生臨時變數。我們通過一個測試程式來看看string_view如何來幫我們優化效能的。
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
我們可以通過字面量””sv來初始化string_view。string_view的substr和string的substr相比,快了50多倍,根本原因是它不會分配記憶體。
string_view的生命週期
由於string_vew並不擁有鎖引用的字串,所以它也不會去關注被引用字串的生命週期,使用者在使用的時候需要注意,不要將一個臨時變數給一個string_view,那樣會導致string_view引用的內容也失效。
std::string_view str_v;
{
std::string temp = "test";
str_v = {temp};
}
這樣的程式碼是有問題的,因為出了作用域之後,string_view引用的內容已經失效了。
總結
本文介紹了C++17的fold expression、constexpr if、constexpr lambda和string_view。fold expression為了簡化可變模板引數的展開,讓可以模板引數的使用變得更簡單直觀;constexpr if讓模板具備if-else功能,非常強大。它也避免了寫冗長的enable_if程式碼,讓程式碼變得簡潔易懂了;string_view則是用來做效能優化的,應該用它來代替const char*和const string。
這些特性對之前的C++14和C++11做了改進和增強,非常酷,歡迎訂閱《程式設計師》,後續系列文章會接著介紹其他C++17中值得關注的新特性。
相關文章
- C++17中那些值得關注的特性C++
- C++17 中那些值得關注的特性C++
- 掘金上值得關注的 iOS 開發者iOS
- Swift 5.0 值得關注的特性:更強大的 Raw StringSwift
- Flink 1.11.0 釋出,有哪些值得關注的新特性?
- Go 1.17中值得關注的幾個變化Go
- Swift 5.0 值得關注的特性:增加 Result<T, E: Error> 列舉型別SwiftError型別
- Swift 5.0 值得關注的特性:Handle unknown values using "@unknown default"Swift
- Struts 1.1的Indexed Properties 值得關注Index
- 20位最值得關注的 JS 開發者JS
- PHP 7 值得期待的新特性(上)PHP
- 認為值得重點關注的技術
- 8個值得關注的PHP安全函式PHP函式
- 值得關注的開源軟體推薦
- Java開發者值得關注的7款新工具Java
- Python3.10第二個alpha版本來了!最新特性值得關注Python
- 值得關注的十個優秀的CSS框架CSS框架
- 我關注的那些程式設計師大佬程式設計師
- 7月資料庫圈值得關注的事資料庫
- 8月資料庫圈值得關注的事資料庫
- 10月資料庫圈值得關注的事資料庫
- 9月資料庫圈值得關注的事資料庫
- 4月資料庫圈值得關注的事資料庫
- 5月資料庫圈值得關注的事資料庫
- 6月資料庫圈值得關注的事資料庫
- 7 個值得關注的開源雲原生工具
- 12月資料庫圈值得關注的事資料庫
- 11月資料庫圈值得關注的事資料庫
- 6月份最值得關注的安全事件事件
- 2018前端值得關注的技術前端
- 8個值得關注的SQL-on-Hadoop框架SQLHadoop框架
- 值得開發者關注的5個新興平臺
- 哪些物聯網預測值得關注?
- 2020年Steam夏季遊戲節值得關注的137款遊戲(上)遊戲
- Windows上那些值得推薦的良心軟體-整理Windows
- GitHub上那些值得一試的JAVA開源庫GithubJava
- 值得關注的網際網路行業趨勢行業
- Go 1.12中值得關注的幾個變化Go