作者:jinshang,騰訊WXG后臺開發工程師
自從步入現代C++時代開始,C++語言標準形成了三年一個版本的慣例:C++11標志著現代C++的開端,C++14在11的基礎上查缺補漏,并未加入許多新特性,而C++17作為C++11后的第一個大版本,標志著現代C++逐漸走向成熟。WXG編譯器升級到gcc7.5已有一段時間,筆者所在項目組也已經將全部代碼升級到C++17。在使用了c++17一年多之后,筆者總結了C++17在業務代碼中最好用的十個特性。
注1:本文只包含wxg的gcc7.5支持的特性,Execution Policy, File System等暫不支持的特性不包含在內。
注2:本文只包含應用于業務邏輯的特性,Fold Expression, Mathematical Special Functions等適用于元編程和科學計算的特性并不包含。
筆者將這些特性大體上分為三類:語法糖、性能提升和類型系統
語法糖這里所說的語法糖,并不是嚴格意義上編程語言級別的語法糖,還包括一些能讓代碼更簡潔更具有可讀性的函數和庫:
結構化綁定c++17最便利的語法糖當屬結構化綁定。結構化綁定是指將array、tuple或struct的成員綁定到一組變量*上的語法,最常用的場景是在遍歷map/unordered_map時不用再聲明一個中間變量了:
// pre c++17for(const auto& kv: map){ const auto& key = kv.first; const auto& value = kv.cond; // ...}// c++17for(const auto& [key, value]: map){ // ...}
嚴格來說,結構化綁定的結果并不是變量,c++標準稱之為名字/別名,這也導致它們不允許被lambda捕獲,但是gcc并沒有遵循c++標準,所以以下代碼在gcc可以編譯,clang則編譯不過
for(const auto& [key, value]: map){ [&key, &value]{ std::cout << key << ": " << value << std::endl; }();}
在clang環境下,可以在lambda表達式捕獲時顯式引入一個引用變量通過編譯
for(const auto& [key, value]: map){ [&key = key, &value = value]{ std::cout << key << ": " << value << std::endl; }();}
另外這條限制在c++20中已經被刪除,所以在c++20標準中gcc和clang都可以捕獲結構化綁定的對象了。
std::tuple的隱式推導在c++17以前,構造std::pair/std::tuple時必須指定數據類型或使用std::make_pair/std::make_tuple函數,c++17為std::pair/std::tuple新增了推導規則,可以不再顯示指定類型。
// pre c++17std::pair<int, std::string> p1{3.14, "pi"s};auto p1 = std::make_pair(3.14, "pi"s);// c++17std::pair p3{3.14, "pi"s};if constexpr
if constexpr語句是編譯期的if判斷語句,在C++17以前做編譯期的條件判斷往往通過復雜SFINAE機制或模版重載實現,甚至嫌麻煩的時候直接放到運行時用if判斷,造成性能損耗,if constexpr大大緩解了這個問題。比如我想實現一個函數將不同類型的輸入轉化為字符串,在c++17之前需要寫三個函數去實現,而c++17只需要一個函數。
// pre c++17template <typename T>std::string convert(T input){ return std::to_string(input);}// const char*和string進行特殊處理std::string convert(const char* input){ return input;}std::string convert(std::string input){ return input;}
// c++17template <typename T>std::string convert(T input) { if constexpr (std::is_same_v<T, const char*> || std::is_same_v<T, std::string>) { return input; } el { return std::to_string(input); }}if初始化語句
c++17支持在if的判斷語句之前增加一個初始化語句,將僅用于if語句內部的變量聲明在if內,有助于提升代碼的可讀性。且對于lock/iterator等涉及并發/RAII的類型更容易保證程序的正確性。
// c++ 17std::map<int, std::string> m;std::mutex mx;extern bool shared_flag; // guarded by mx int demo(){ if (auto it = m.find(10); it != m.end()) { return it->cond.size(); } if (char buf[10]; std::fgets(buf, 10, stdin)) { m[0] += buf; } if (std::lock_guard lock(mx); shared_flag) { unsafe_ping(); shared_flag = fal; } if (int s; int count = ReadBytesWithSignal(&s)) { publish(count); rai(s); } if (const auto keywords = {"if", "for", "while"}; std::ranges::any_of(keywords, [&tok](const char* kw) { return tok == kw; })) { std::cerr << "Token must not be a keyword
"; }}性能提升std::shared_mutex
shared_mutex是c++的原生讀寫鎖實現,有共享和獨占兩種鎖模式,適用于并發高的讀場景下,通過reader之前共享鎖來提升性能。在c++17之前,只能自己通過獨占鎖和條件變量自己實現讀寫鎖或使用c++14加入的性能較差的std::shared_timed_mutex。以下是通過shared_mutex實現的線程安全計數器:
// c++17class ThreadSafeCounter { public: ThreadSafeCounter() = default; // Multiple threads/readers can read the counter's value at the same time. unsigned int get() const { std::shared_lock lock(mutex_); return value_; } // Only one thread/writer can increment/write the counter's value. unsigned int increment() { std::unique_lock lock(mutex_); return ++value_; } // Only one thread/writer can ret/write the counter's value. void ret() { std::unique_lock lock(mutex_); value_ = 0; } private: mutable std::shared_mutex mutex_; unsigned int value_ = 0;};std::string_view
std::string_view顧名思義是字符串的“視圖”,類成員變量包含兩個部分:字符串指針和字符串長度,std::string_view涵蓋了std::string的所有只讀接口。std::string_view對字符串不具有所有權,且兼容std::string和const char*兩種類型。
c++17之前,我們處理只讀字符串往往使用const std::string&,std::string有兩點性能優勢:
兼容兩種字符串類型,減少類型轉換和內存分配。如果傳入的是明文字符串const char*, const std::string&需要進行一次內存分配,將字符串拷貝到堆上,而std::string_view則可以避免。在處理子串時,std::string::substr也需要進行拷貝和分配內存,而std::string_view::substr則不需要,在處理大文件解析時,性能優勢非常明顯。// from https://stackoverflow.com/a/40129046// author: Pavel Davydov// string_view的remove_prefix比const std::string&的快了15倍string remove_prefix(const string &str) { return str.substr(3);}string_view remove_prefix(string_view str) { str.remove_prefix(3); return str;}static void BM_remove_prefix_string(benchmark::State& state) { std::string example{"asfaghdfgsghasfasg3423rfgasdg"}; while (state.KeepRunning()) { auto res = remove_prefix(example); // auto res = remove_prefix(string_view(example)); for string_view if (res != "aghdfgsghasfasg3423rfgasdg") { throw std::runtime_error("bad op"); } }}std::map/unordered_map try_emplace
在向std::map/unordered_map中插入元素時,我們往往使用emplace,emplace的操作是如果元素key不存在,則插入該元素,否則不插入。但是在元素已存在時,emplace仍會構造一次待插入的元素,在判斷不需要插入后,立即將該元素析構,因此進行了一次多余構造和析構操作。c++17加入了try_emplace,避免了這個問題。同時try_emplace在參數列表中將key和value分開,因此進行原地構造的語法比emplace更加簡潔
std::map<std::string, std::string> m;// emplace的原地構造需要使用std::piecewi_construct,因為是直接插入std::pair<key, value>m.emplace(std::piecewi_construct, std::forward_as_tuple("c"), std::forward_as_tuple(10, 'c'));// try_emplace可以直接原地構造,因為參數列表中key和value是分開的m.try_emplace("c", 10, 'c')
同時,c++17還給std::map/unordered_map加入了inrt_or_assign函數,可以更方便地實現插入或修改語義
類型系統c++17進一步完備了c++的類型系統,終于加入了眾望所歸的類型擦除容器(Type Erasure)和代數數據類型(Algebraic Data Type)
std::anystd::any是一個可以存儲任何可拷貝類型的容器,C語言中通常使用void*實現類似的功能,與void*相比,std::any具有兩點優勢:
std::any更安全:在類型T被轉換成void*時,T的類型信息就已經丟失了,在轉換回具體類型時程序無法判斷當前的void*的類型是否真的是T,容易帶來安全隱患。而std::any會存儲類型信息,std::any_cast是一個安全的類型轉換。std::any管理了對象的生命周期,在std::any析構時,會將存儲的對象析構,而void*則需要手動管理內存。std::any應當很少是程序員的第一選擇,在已知類型的情況下,std::optional, std::variant和繼承都是比它更高效、更合理的選擇。只有當對類型完全未知的情況下,才應當使用std::any,比如動態類型文本的解析或者業務邏輯的中間層信息傳遞。
std::optionalstd::optional<T>代表一個可能存在的T值,對應Haskell中的Maybe和Rust/OCaml中的option,實際上是一種Sum Type。常用于可能失敗的函數的返回值中,比如工廠函數。在C++17之前,往往使用T*作為返回值,如果為nullptr則代表函數失敗,否則T*指向了真正的返回值。但是這種寫法模糊了所有權,函數的調用方無法確定是否應該接管T*的內存管理,而且T*可能為空的假設,如果忘記檢查則會有SegFault的風險。
// pre c++17ReturnType* func(const std::string& in) { ReturnType* ret = new ReturnType; if (in.size() == 0) return nullptr; // ... return ret;}// c++17 更安全和直觀std::optional<ReturnType> func(const string& in) { ReturnType ret; if (in.size() == 0) return nullopt; // ... return ret;}std::variant
std::variant<T, U, ...>代表一個多類型的容器,容器中的值是制定類型的一種,是通用的Sum Type,對應Rust的enum。是一種類型安全的union,所以也叫做tagged union。與union相比有兩點優勢:
可以存儲復雜類型,而union只能直接存儲基礎的POD類型,對于如std::vector和std::string就等復雜類型則需要用戶手動管理內存。類型安全,variant存儲了內部的類型信息,所以可以進行安全的類型轉換,c++17之前往往通過union+enum來實現相同功能。通過使用std::variant<T, Err>,用戶可以實現類似Rust的std::result,即在函數執行成功時返回結果,在失敗時返回錯誤信息,上文的例子則可以改成:
std::variant<ReturnType, Err> func(const string& in) { ReturnType ret; if (in.size() == 0) return Err{"input is empty"}; // ... return {ret};}
需要注意的是,c++17只提供了一個庫級別的variant實現,沒有對應的模式匹配(Pattern Matching)機制,而最接近的std::visit又缺少編譯器的優化支持,所以在c++17中std::variant并不好用,跟Rust和函數式語言中出神入化的Sum Type還相去甚遠,但是已經有許多圍繞std::variant的提案被提交給c++委員會探討,包括模式匹配,std::expected等等。
總結一下,c++17新增的三種類型給c++帶來了更現代更安全的類型系統,它們對應的使用場景是:
std::any適用于之前使用void*作為通用類型的場景。std::optional適用于之前使用nullptr代表失敗狀態的場景。std::variant適用于之前使用union的場景。總結以上是筆者在生產環境中最常用的c++17特性,除了本文描述的十個特性外,c++17還添加了如lambda值捕獲*this, 鉗夾函數std::clamp(), 強制檢查返回值[[nodiscard]]等非常易用的特性,本文篇幅有限不做贅述,歡迎有興趣的讀者自行探索。
本文發布于:2023-02-28 20:58:00,感謝您對本站的認可!
本文鏈接:http://m.newhan.cn/zhishi/a/167771210698988.html
版權聲明:本站內容均來自互聯網,僅供演示用,請勿用于商業和其他非法用途。如果侵犯了您的權益請與我們聯系,我們將在24小時內刪除。
本文word下載地址:fgets(fgets(s,n,f)).doc
本文 PDF 下載地址:fgets(fgets(s,n,f)).pdf
| 留言與評論(共有 0 條評論) |