2017:C++17 結構化綁定與 if constexpr——實用主義之年#
事件#
2017 年 12 月,C++17(ISO/IEC 14882:2017)發佈。這一版以「讓日常程式碼更簡潔」為目標,被稱為實用主義之年。
關鍵語法/特性#
- 結構化綁定:
auto [x, y] = point;——一次解開 pair/tuple/struct。 if constexpr:編譯期分支——不成立的分支不會被實例化,樣板程式碼從此可讀。std::optional、std::variant、std::any:型別安全的「可空值」、「和型別」、「任意型別」。std::string_view:零拷貝的字串視圖。std::filesystem:標準檔案系統庫。- 摺疊運算式(fold expression):
(args + ...)——可變參數樣板的摺疊。 - 類別樣板參數推論(CTAD):
std::pair p(1, 2.0);不用寫pair<int,double>。 if帶初始化:if (auto it = m.find(k); it != m.end())。
為何重要#
optional/variant讓「可空性」進入型別系統:取代魔法值(-1、nullptr、EOF)回傳——與 Rust 的Option<T>/enum同一思想,但 C++ 選擇了向後相容的路線。if constexpr消滅了樣板元程式設計的 SFINAE 黑魔法:以前要用enable_if疊層層模板,現在普通的if constexpr即可。string_view與 CTAD:減少拷貝與冗長宣告——零成本抽象的日常化。
理論與實用原因#
- 理論原因:
optional/variant源自代數資料類型(algebraic data types)理論——Haskell 的Maybe/Either;if constexpr是「編譯期求值即分支消除」的理論落地。 - 實用原因:Boost 早已提供
boost::optional/boost::variant十年,標準只是收編成熟設計。
彌補了什麼缺陷#
彌補了 C++「無型別安全的可空值與和型別、樣板分支只能用 SFINAE 黑魔法、字串處理必須拷貝、無標準檔案系統」的缺陷。
程式範例#
結構化綁定:一次解開 pair/tuple/struct
#include <map>
#include <string>
#include <tuple>
std::map<std::string, int> ages;
void old_way() {
// 舊寫法:pair 要用 .first/.second,tuple 要用 get<>()
for (auto it = ages.begin(); it != ages.end(); ++it) {
std::string name = it->first; // 冗長又不直覺
int age = it->second;
}
std::tuple<int, double, std::string> t(1, 2.5, "x");
int a = std::get<0>(t); // 得靠索引記位置
}
void new_way() {
// C++17:結構化綁定,直接命名解開的成員
for (auto& [name, age] : ages) { /* ... */ }
auto [a, b, c] = std::tuple{1, 2.5, std::string("x")}; // 一次綁定三個值
}
struct Point { int x, y; };
auto [px, py] = Point{3, 4}; // struct 也可以
if constexpr:取代 SFINAE 黑魔法
#include <type_traits>
// C++14 舊寫法:要用 enable_if 疊層層模板(SFINAE),難讀難寫
template <class T>
auto size14(const T& v) -> std::enable_if_t<std::is_integral_v<T>, int> {
return v; // 整數版本
}
// C++17:if constexpr 不成立的分支直接不被實例化,普通程式碼即可
template <class T>
int size17(const T& v) {
if constexpr (std::is_integral_v<T>) {
return v; // 整數:回傳值本身
} else {
return v.size(); // 容器:回傳大小
}
}
std::optional:取代魔法值回傳
#include <optional>
// 舊寫法:用 -1 當「找不到」的魔法值,呼叫端可能忘記檢查
int find_index_old(int key) {
if (key == 42) return 7;
return -1; // 魔法值——型別系統無法提醒你
}
// C++17:optional 把「可空性」帶進型別系統
std::optional<int> find_index(int key) {
if (key == 42) return 7;
return std::nullopt; // 明確表示「沒有值」
}
void use() {
if (auto idx = find_index(42)) { // optional 可直接當條件
int v = *idx; // 有值才取用
}
}
if 帶初始化 與 string_view
#include <map>
#include <string>
#include <string_view>
std::map<std::string, int> m;
void f() {
// C++17:if 帶初始化——變數作用域限定在 if 內,不污染外層
if (auto it = m.find("key"); it != m.end()) {
int v = it->second;
} else {
// it 在這裡也可用
}
}
// string_view:零拷貝的字串視圖——不擁有資料,只借用
void parse(std::string_view sv);
void g() {
std::string big = "a very long string...";
parse(big); // 不拷貝
parse(big.substr(0, 5)); // 舊寫法 substr 會拷貝
parse(std::string_view(big).substr(0, 5)); // string_view 切片零拷貝
}