C++ 20新特性
目錄
Concepts
Ranges
Modules
Coroutines?(協程)
Reflection
總結
前言
本文主體內容源自公眾號 CSDN(ID:CSDNnews),作者:祁宇
本文將只評論大部分確定要加入和可能加入到 C++20 的重要特性,讓讀者對 C++ 的未來和演進趨勢有一個基本的了解。這篇文章創作時間較早,所以一些內容和今年發布的 C++20 具體實現有些許不同。
?
C++20 中可能增加哪些重要特性,下面這個圖可以提供一個參考:
下面是本文將評論的將進入和可能進入 C++20 的重要特性:Concepts、Ranges、Modules、Coroutines?和?Reflection。
接下來讓我們慢慢揭開 C++20 的面紗,看看這些特性到底是什么樣的,它們解決了什么問題。
?
?
Concepts
在談 Concepts 之前我想先介紹一下 Concepts 提出的背景和原因。眾所周知,因為 C++ 的模版和模版元具備非常強大的泛型抽象能力并且是 zero overhead,所以模版在 C++ 中備受推崇,大獲成功,在各種 C++ 庫(如STL)中被廣泛使用。
然而,模版編程還存在一些問題,比如有些模版的代碼寫起來比較困難,讀起來比較難懂,尤其是編譯出錯的時候,那些糟糕的讓人摸不著頭腦的錯誤提示讓人頭疼。因此,C++ 之父 Bjarne Stroustrup 很早就希望對模版做一些改進,讓 C++ 的模版編程變得簡單好寫,錯誤提示更明確。他早在 1987 年就開始做這方面的嘗試了。
具體思路就是給模版參數加一些約束,這些約束相比之前的寫法具有更強的表達能力和可讀性,會簡化 C++ 的泛型模版代碼的編寫。所以 Concepts 的出現主要是為了簡化泛型編程,一個 Concept 就是一個編譯期判斷,用于約束模版參數,Concepts 則是這些編譯期判斷的合集。下面通過一個例子來展示 Concepts 是如何簡化模版編程的:
template<typename?T> class?B?{ public:template<typename?ToString?=?T>typename?std::enable_if_t<std::is_convertible<ToString,?std::string>::value,?std::string>to_string()?const?{return?"Class?B<>";} };B<size_t>?b1;?????????????????????????????//?OK std::cout?<<?b1.to_string()?<<?std::endl;?//?Compile?ERROR!B<std::string>?b2;????????????????????????//?OK std::cout?<<?b2.to_string()?<<?std::endl;?//?OK!比如有這樣一個類 B,我們調用它的成員函數 tostring 時,對 T 類型進行限定,即限定 T 類型是 std::string 的可轉換類型,這樣做的目的是為了更安全,能在編譯期就能檢查錯誤。這里通過 C++14 的 std::enable_if_t 來對 T 進行限定,但是長長的 enable_if_t 看起來比較冗長繁瑣,頭重腳輕。來看看用 Concepts 怎么寫這個代碼的:
template<typename?T>concept?CastableToString?=?requires(T?a)?{{?a?}?->?std::string; };template<typename?T> class?D?{ public:std::string?to_string()?const?requires?CastableToString<T>?{return?"Class?D<>";} };可以看到,requires CastableToString 比之前長長的 enableift 要簡潔不少,代碼可讀性也更好,CastableToString 就是一個 Concept,一個限定 T 為能被轉換為 std::string 類型的 Concept,通過 requires 相連接,語義上也更明確了,而且這個 Concept 還可以復用。Concepts 的這個語法也可能在最終的 C++20 中有少許不同,有可能還會變得更簡潔,現在語法有幾個候選版本,還沒最終投票確定。
?
?
Ranges
相比 STL,Ranges 是更高一層的抽象,Ranges 對 STL 做了改進,它是STL的下一代。為什么說 Ranges 是 STL 的未來?雖然 STL 在 C++ 中提供的容器和算法備受推崇和廣泛被使用,但?STL一直存在兩個問題:?
(1) STL 強制你必須傳一個 begin 和 end 迭代器用來遍歷一個容器;
(2) STL 算法不方便組合在一起。
STL 必須傳迭代器,這個迭代器僅僅是輔助你完成遍歷序列的技術細節,和我們的函數功能無關,大部分時候我們需要的是一個 range,代表的是一個比迭代器更高層的抽象。那么 Ranges 到底是什么呢?Ranges 是一個引用元素序列的對象,在概念上類似于一對迭代器。這意味著所有的 STL 容器都是 Ranges。在 Ranges 里我們不再傳迭代器了,而是傳 range。比如下面的代碼:
STL寫法: std::vector<int>?v{1,?2}; std::sort(v.begin(),?v.end());Ranges寫法: std::sort(v);STL 有時候不方便將一些算法組合在一起,來看一個例子:
std::vector<int>?v{1,?2,?3,?4,?5};std::vector<int>?event_numbers; std::copy_if(v.begin(),?v.end(),?std::back_inserter(event_numbers),?[](int?i){?return?i?%?2?==?0;});std::vector<int>?results; std::transform(event_numbers.begin(),?event_numbers.end(),?std::back_inserter(event_numbers),?[](int?i){?return?i?*?2;});for(int?n?:?results){std::cout<<n<<'?'; } //最終會輸出?4?8上面這個例子希望得到 vector 中的偶數乘以 2 的結果,需求很簡單,但是用 STL 寫起來還是有些冗長繁瑣,中間還定義了兩個臨時變量。如果用 Ranges 來實現這個需求,代碼就會簡單得多。
auto?results?=?v?|?ranges::view::filter([](int?i){?return?i?%?2?==?0;?})|?ranges::view::transform([](int?i){?return?i?*?2;?});用 Concetps 我們可以很方便地將算法組合在一起,寫法更簡單,語義更清晰,并且還可以實現延遲計算避免了中間的臨時變量,性能也會更好。Concepts 從設計上改進了之前 STL 的兩個問題,讓我們的容器和算法變得更加簡單好用,還容易組合。
?
?
Modules
一直以來 C++ 一直通過引用頭文件方式使用庫,而其他90年代以后的語言比如 Java、C#、Go 等語言都是通過 import 包的方式來使用庫。現在 C++決 定改變這種情況了,在 C++20 中將引入 Modules,它和 Java、Go 等語言的包的概念是類似的,直接通過 import 包來使用庫,再也看不到頭文件了。
為什么 C++20 不再希望使用 #include 方式了?因為使用頭文件方式存在不少問題,比如有 include 很多模版的頭文件將大大增加編譯時間,代碼生成物也會變大。而且引用頭文件方式不利于做一些 C++ 庫和組件的管理工具,尤其是對于一些云環境和分布式環境下不方便管理,C++ 一直缺一個包管理工具,這也是 C++ 被吐槽得很多的地方,現在 C++20 Modules 將改變這一切。?
Modules 在程序中的結構如下圖:
上面的圖中,每個方框表示一個翻譯單元,存放在一個文件里并且可以被獨立編譯。每個 Module 由 Module 接口和實現組成,接口只有一份,實現可以有多份。?
Modules 接口和實現的語法:
export?module?module_name; module?module_name;使用 Modules:
import?module_name;Modules 允許你導出類,函數,變量,常量和模版等等。接下來看一個使用 Modules 的例子:
import?std.vector;?//?#include?<vector> import?std.string;?//?#include?<string> import?std.iostream;?//?#include?<iostream> import?std.iterator;?//?#include?<iterator?> int?main()?{using?namespace?std;vector<string>?v?=?{"Socrates",?"Plato",?"Descartes",?"Kant",?"Bacon"};copy(begin(v),?end(v),?ostream_iterator<string>(cout,?"\n")); }可以看到不用再 include 了,直接去 import 需要用到的 Modules 即可,是不是有種似曾相識的感覺呢。曾看到一個人說如果 C++ 支持了 Modules 他就會從 Java 回歸到 C++,也說明這個特性也是非常受關注和期待的。
?
?
Coroutines?(協程)
很多語言提供了 Coroutine 機制,因為 Coroutine 可以大大簡化異步網絡程序的編寫,現在 C++20 中也要加入協程了(樂觀估計 C++20 加入,悲觀估計在 C++23 中加入)。如果不用協程,寫一個異步的網絡程序是不那么容易的,以 boost.asio 的異步網絡編程為例,我們需要注意的地方很多,比如異步事件完成的回調函數中需要保證調用對象仍然存在,如何構建異步回調鏈條等等,代碼比較復雜,而且出了問題也不容易調試。而協程給我們提供了對異步編程優雅而高效的抽象,讓異步編程變得簡單!
C++ Courotines 中增加了三個新的關鍵字:co_await,co_yield?和?co_return,如果一個函數體中有這三個關鍵字之一就變成 Coroutine 了。co_await 用來掛起和恢復一個協程,co_return 用來返回協程的結果,co_yield 返回一個值并且掛起協程。
下面來看看如何使用它們,寫一個 lazy sequence:
generator<int>?get_integers(?int?start=0,?int?step=1?)?{for?(int?current=start;?current+=?step)co_yield?current; }for(auto?n?:?get_integers(0,?5)){std::cout<<n<<"?"; } std::cout<<'\n';上面的例子每次調用 get_integers,只返回一個整數,然后協程掛起,下次調用再返回一個整數,因此這個序列不是即時生成的,而是延遲生成的。
接下來再看一下 co_wait 是如何簡化異步網絡程序的編寫的:
char?data[1024]; for?(;;) {std::size_t?n?=?co_await?socket.async_read_some(boost::asio::buffer(data),?token);co_await?async_write(socket,?boost::asio::buffer(data,?n),?token); }這個例子僅僅用了四行代碼就完成了異步的 echo,非常簡潔!co_await 會在異步讀完成之前掛起協程,在異步完成之后恢復協程繼續執行,執行到 async_write 時又會掛起協程直到異步寫完成,異步寫完成之后繼續異步讀,如此循環。如果不用協程代碼會比較繁瑣,需要像這樣寫:
????void?do_read(){auto?self(shared_from_this());socket_.async_read_some(boost::asio::buffer(data_,?max_length),[this,?self](boost::system::error_code?ec,?std::size_t?length){if?(!ec){do_write(length);}});}void?do_write(std::size_t?length){auto?self(shared_from_this());boost::asio::async_write(socket_,?boost::asio::buffer(data_,?length),[this,?self](boost::system::error_code?ec,?std::size_t?/*length*/){if?(!ec){do_read();}});}可以看到,不使用協程來寫異步代碼的話,需要構建異步的回調鏈,需要保持異步回調的安全性等等。而使用協程可以大大簡化異步網絡程序的編寫。
?
?
Reflection
C++ 中一直缺少反射功能,其他很多語言如 Java、C# 都具備運行期反射功能。反射可以用來做很多事情:比如做對象的序列化,把對象序列化為 JSON、XML 等格式,以及 ORM 中的實體映射,還有 RPC 遠程過程(方法)調用等,反射是應用程序中非常需要的基礎功能。
現在 C++ 終于要提供反射功能了,C++20 中可會將反射作為實驗庫,最晚在 C++26?中正式加入到標準中。
在反射還沒有進入到C++標準之前,有很多人做了一些編譯期反射的庫,比如purecpp社區開源的序列化引擎 iguana,以及 ORM 庫 ormpp,都是基于編譯期反射實現的。然后,非語言層面支持的反射庫存在種種不足之處,比如在實現上需要大量使用模版元和宏、不能訪問私有成員等問題。
現在 C++ 終于要提供完備地編譯期反射功能了,為什么是編譯期反射而不是像其它語言一樣提供運行期反射,因為 C++ 的一個重要設計哲學就是 zero-overhead,編譯期反射效率遠高于運行期反射。那么,通過 C++20 的編譯期反射我們能得到什么呢?我們可以得到很多很多關于類型和對象的元信息,主要有:
(1) 獲取對象類型或枚舉類型的成員變量,成員函數的類型;
(2) 獲取類型和成員的名稱;
(3) 獲取成員變量是靜態的還是 constexpr;
(4) 獲取方法是 virtual、public、protect 還是 private;
(5) 獲取類型定義時的源代碼所在的行和列。
?
所以 C++20 的反射其實是提供了一些可以編譯期向編譯器查詢目標類型 "元數據" 的 API,下面來看看 C++20 的反射用法:
struct?person{int?id;std::string?name; };using?MetaPerson?=?reflexpr(person); using?Members?=?std::reflect::get_data_members_t<MetaPerson>;using?Metax?=?std::reflect::get_data_members_t<Members>; constexpr?bool?is_public?=?std::reflect::is_public_v<Metax>;using?Field0?=?std::reflect::get_reflected_type_t<Metax>;//?int上面的例子中,C++20 新增關鍵字 reflexpr 返回的是 person 的元數據類型,接下來我們就可以查詢這個元數據類型了, std::reflect::getdatamembers_t 返回的是對象成員的元數據序列,我們可以像訪問 tuple 一樣訪問這個序列,得到某一個字段的元數據之后我們就可以獲取它的具體信息了,比如它的具體類型是什么,它的字段名是什么,它是公有還是私有的等等。
注意:C++20 的反射語法還沒有最終確定,這只是一種候選的語法實現,還有一種沒有元編程的語法版本,該版本通過編譯期容器和字符串來存放元數據,比如 constexpr std::vector,constexpr std::map,constexpr?std::string 等 ,這樣就可以像普通的 C++ 程序那樣來操作元數據了,用起來可能更簡單。
C++20 的編譯期反射實際上提供了一些編譯期查詢 AST 信息的接口,功能完備而強大。
?
總結
Concepts 讓 C++ 的模版程序的編寫變得更簡單和容易理解;
Ranges 讓我們使用 STL 容器和算法更加簡單,并且更容易組合算法及延遲計算;
Modules 幫助我們大大加快編譯速度,同時彌補了 C++ 使用庫和缺乏包管理的缺陷;
Coroutines 幫助我們簡化異步程序的編寫;
Reflection 給我們提供強大的編譯期 AST 元數據查詢能力;
總而言之,C++ 的新標準都是為了讓 C++ 變得更簡單、更完善、更強大、更易學和使用,這也是 C++ 之父希望未來 C++ 演進的一個方向和目標。C++20,一言以蔽之:Newer is Better!?
總結
- 上一篇: C++ 基础概念、语法和易错点整理
- 下一篇: 浅谈分布式存储系统数据分布算法