C++20 要來了!
C++的新標準又雙叒叕要到來了,是的,C++20要來了!
圖片來源:udemy.com
幾周前,C++標準委會歷史上規模最大的一次會議(180人蔘會)在美國San Diego召開,這次的會議上討論確定哪些特性要加入到C++20中,哪些特性可能加入到C++20中。在明年二月份的會議當中將正式確定所有的C++20特性。
這次會議討論的提案也是非常之多,達到了創紀錄的274份,C++20的新特性如果要一一列出的話將是一份長長的清單,因此本文將只評論大部分確定要加入和可能加入到C++20的重要特性,讓讀者對C++的未來和演進趨勢有一個基本的瞭解。
C++20中可能增加哪些重要特性,下面這個圖可以提供一個參考。
下面是本文將評論的將進入和可能進入C++20的重要特性:
-
Concepts
-
Ranges
-
Modules
-
Coroutines
-
Reflection
接下來讓我們慢慢揭開C++20的面紗,看看這些特性到底是什麼樣的,它們解決了什麼問題。
Line"/>
Concepts
在談Concepts之前我想先介紹一下Concepts提出的背景和原因。眾所周知,因為C++的模版和模版元具備非常強大的泛型抽象能力並且是zero overhead,所以模版在C++中備受推崇,大獲成功,在各種C++庫( 如STL )中被廣泛使用。
然而,模版程式設計還存在一些問題,比如有些模版的程式碼寫起來比較困難,讀起來比較難懂,尤其是編譯出錯的時候,那些糟糕的讓人摸不著頭腦的錯誤提示讓人頭疼。因此,C++之父Bjarne Stroustrup很早就希望對模版做一些改進,讓C++的模版程式設計變得簡單好寫,錯誤提示更明確。他早在1987年就開始做這方面的嘗試了。
C++之父Bjarne Stroustrup
具體思路就是給模版引數加一些約束,這些約束相比之前的寫法具有更強的表達能力和可讀性,會簡化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::enableif_t來對T進行限定,但是長長的enableift看起來比較冗長繁瑣,頭重腳輕。來看看用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一直存在兩個問題:
-
STL強制你必須傳一個begin和end迭代器用來遍歷一個容器;
-
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++23中正式加入到標準中。
在反射還沒有進入到C++標準之前,有很多人做了一些編譯期反射的庫,比如purecpp社群開源的序列化引擎iguana,以及ORM庫ormpp,都是基於編譯期反射實現的。然後,非語言層面支援的反射庫存在種種不足之處,比如在實現上需要大量使用模版元和巨集、不能訪問私有成員等問題。
現在C++終於要提供完備地編譯期反射功能了,為什麼是編譯期反射而不是像其它語言一樣提供執行期反射,因為C++的一個重要設計哲學就是zero-overhead,編譯期反射效率遠高於執行期反射。
那麼,通過C++20的編譯期反射我們能得到什麼呢?我們可以得到很多很多關於型別和物件的元資訊,主要有:
-
獲取物件型別或列舉型別的成員變數,成員函式的型別;
-
獲取型別和成員的名稱;
-
獲取成員變數是靜態的還是constexpr;
-
獲取方法是virtual、public、protect還是private;
-
獲取型別定義時的原始碼所在的行和列。
所以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++20的更多細節讀者可以在這裡檢視:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/。
總而言之,C++的新標準都是為了讓C++變得更簡單、更完善、更強大、更易學和使用,這也是C++之父希望未來C++演進的一個方向和目標。
C++20,一言以蔽之:Newer is Better!
在此呼籲現在仍然還在使用著20年前的標準C++98的公司儘早升級到最新的標準,跟上時代的發展,新標準意味這生產力和質量的提升,越早使用越早享受其帶來的好處!
作者簡介: 祁宇,modern c++開源社群purecpp.org創始人,《深入應用C++11》作者,開源庫cinatra、feather作者,熱愛開源,熱愛modern C++。樂於研究和分享技術,多次在國際C++大會(cppcon)做演講。
致謝:感謝purecpp社群的朋友:袁秩昊,吳詠煒和張軼對本文部分內容的review。
參考資料:
-
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/
-
https://isocpp.org/blog/2016/02/a-bit-of-background-for-concepts-and-cpp17-bjarne-stroustrup
-
https://www.reddit.com/r/cpp/comments/9vwvbz/2018sandiegoisoccommitteetripreportranges/
-
https://herbsutter.com/2018/11/13/trip-report-fall-iso-c-standards-meeting-san-diego/
-
http://www.jakubkonka.com/2017/09/02/type-traits-cpp.html
-
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4128.html
-
https://arne-mertz.de/2017/01/ranges-stl-next-level/
-
https://www.fluentcpp.com/2018/02/09/introduction-ranges-library/
-
http://wg21.link/p1103
-
https://medium.com/@wrongway4you/brief-article-on-c-modules-f58287a6c64
-
https://www.codeproject.com/Articles/1214398/Modules-for-Modern-Cplusplus
-
https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await
-
https://lewissbaker.github.io/2017/09/25/coroutine-theory
2018 中國大資料技術大會
◆
BDTC 2018
◆
BDTC 2018中國大資料技術大會攜主題“大資料新應用”再度強勢來襲。本次大會由 華東師範大學副校長、教授周傲英,百度商業智慧實驗室主任熊輝,阿里巴巴副總裁李飛飛 三位會議主席對大會內容把關,多位兩院院士參與指導,由最瞭解行業痛點的一線從業者為同行打造。
掃描下方二維碼或閱讀原文快速購票 。現在購票還有機會獲得大資料圖書一本(中國科學院院士梅巨集主編的《大資料導論》或華中科技大學教授金海主編的《大資料處理》),數量有限!
微信改版了,
想快速看到CSDN的熱乎文章,
趕快把CSDN公眾號 設為星標 吧,
開啟公眾號,點選“設為星標”就可以啦!
徵稿啦 ”
CSDN 公眾號秉持著「與千萬技術人共成長」理念,不僅以「極客頭條」、「暢言」欄目在第一時間以技術人的獨特視角描述技術人關心的行業焦點事件,更有「技術頭條」專欄,深度解讀行業內的熱門技術與場景應用,讓所有的開發者緊跟技術潮流,保持警醒的技術嗅覺,對行業趨勢、技術有更為全面的認知。
如果你有優質的文章,或是行業熱點事件、技術趨勢的真知灼見,或是深度的應用實踐、場景方案等的新見解,歡迎聯絡 CSDN 投稿, 聯絡方式:微信(guorui_1118,請備註投稿+姓名+公司職位),郵箱([email protected])。