C++迭代速度相對來說還是比較慢的,2010年以後,C++的新版本迭代速度有所加快,這一點,從C++標準版本的歷史釋出圖1就可以看出來:
筆者將這些特性大體上分為三類:語法糖、效能提升和型別系統。
語法糖
這裡所說的語法糖,並不是嚴格意義上程式語言級別的語法糖,還包括一些能讓程式碼更簡潔更具有可讀性的函式和庫:
結構化繫結
c++17 最便利的語法糖當屬結構化繫結。結構化繫結是指將 array、tuple 或 struct 的成員繫結到一組變數*上的語法,最常用的場景是在遍歷 map/unordered_map 時不用再宣告一箇中間變量了:
// pre c++17for(const auto& kv: map){ const auto& key = kv。first; const auto& value = kv。second; // 。。。}// 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
if constexpr
if constexpr 語句是編譯期的 if 判斷語句,在 C++17 以前做編譯期的條件判斷往往透過複雜
SFINAE
機制或模版過載實現,甚至嫌麻煩的時候直接放到執行時用 if 判斷,造成效能損耗,if constexpr 大大緩解了這個問題。比如我想實現一個函式將不同型別的輸入轉化為字串,在 c++17 之前需要寫三個函式去實現,而 c++17 只需要一個函式。
// pre c++17template
// c++17template
if 初始化語句
c++17 支援在 if 的判斷語句之前增加一個初始化語句,將僅用於 if 語句內部的變數宣告在 if 內,有助於提升程式碼的可讀性。且對於 lock/iterator 等涉及併發/RAII 的型別更容易保證程式的正確性。
// c++ 17std::map
效能提升
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 reset/write the counter‘s value。 void reset() { 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
同時,c++17 還給
std::map/unordered_map
加入了
insert_or_assign
函式,可以更方便地實現插入或修改語義。
型別系統
c++17 進一步完備了 c++的型別系統,終於加入了眾望所歸的型別擦除容器(
Type Erasure
)和代數資料型別(
Algebraic Data Type
)
std::any
std::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::optional
std::optional
代表一個可能存在的 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
std::variant
std::variant
代表一個多型別的容器,容器中的值是制定型別的一種,是通用的 Sum Type,對應 Rust 的
enum
。是一種型別安全的
union
,所以也叫做
tagged union
。與
union
相比有兩點優勢:
可以儲存複雜型別,而 union 只能直接儲存基礎的 POD 型別,對於如
std::vector
和
std::string
就等複雜型別則需要使用者手動管理記憶體。
型別安全,variant 儲存了內部的型別資訊,所以可以進行安全的型別轉換,c++17 之前往往透過
union
+
enum
來實現相同功能。
透過使用
std::variant
,使用者可以實現類似 Rust 的
std::result
,即在函式執行成功時返回結果,在失敗時返回錯誤資訊,上文的例子則可以改成:
std::variant
需要注意的是,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]]
等非常易用的特性,本文篇幅有限不做贅述,歡迎有興趣的讀者自行探索