跳轉到

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 切片零拷貝
}

相關條目#