跳到正文

Wiki

std::optional

約 6 分鐘閱讀

本文由簡體中文內容確定性轉換,並受版本化術語表保護。

1.本節解決什麼問題

函式返回結果時,如何表示"沒有結果"?

常見的做法各有缺點:

  • 返回 -1nullptr:語義不清晰,且 -1 可能是合法值。
  • 返回 bool + 輸出引數:程式碼冗長,需要先宣告變數。
  • 丟擲異常:異常有開銷,且"查不到"往往不算異常情況。

std::optional<T> 優雅地表達了"可能有值、也可能沒有值"的語義。

2.這個特性是什麼

std::optional<T> 是一個模板類,它要麼包含一個 T 型別的值,要麼為空(沒有值)。它讓"可能沒有值"這個資訊成為型別的一部分,編譯器幫你檢查。

3.C++ 標準版本

C++17

4.需要的標頭檔案

#include <optional>

5.基本語法

std::optional<T> opt;                    // 默认空
std::optional<T> opt = value;            // 有值
std::optional<T> opt = std::nullopt;     // 显式空

// 检查是否有值
if (opt.has_value()) { ... }
if (opt) { ... }                         // 隐式转 bool

// 获取值
T val = opt.value();                     // 若无值抛异常
T val = *opt;                            // 若无值是未定义行为
T val = opt.value_or(default);           // 若无值返回默认值

6.常用用法

操作 說明
opt.has_value() 是否有值
opt.value() 獲取值(無值拋 std::bad_optional_access
*opt 獲取值(無值行為未定義,先檢查!)
opt.value_or(default) 獲取值,無值返回預設值
opt = std::nullopt 清空
opt.emplace(args...) 原地構造值

7.示例程式碼

7.1.示例 1:用 optional 表示"可能找不到"

#include <iostream>
#include <optional>
#include <string>
#include <vector>

// 在 vector 中查找元素,返回位置(可能找不到)
std::optional<size_t> find_index(const std::vector<std::string>& v,
                                  const std::string& target)
{
    for (size_t i = 0; i < v.size(); ++i)
    {
        if (v[i] == target)
        {
            return i;  // 找到了
        }
    }
    return std::nullopt;  // 没找到
}

int main()
{
    std::vector<std::string> names = {"Alice", "Bob", "Charlie"};

    auto idx = find_index(names, "Bob");
    if (idx)
    {
        std::cout << "Found Bob at index " << *idx << "\n";
    }

    auto idx2 = find_index(names, "David");
    if (!idx2)
    {
        std::cout << "David not found\n";
    }

    // 使用 value_or 提供默认值
    std::cout << "Result: " << idx.value_or(999) << "\n";
    std::cout << "Result2: " << idx2.value_or(999) << "\n";

    return 0;
}

執行結果

Found Bob at index 1
David not found
Result: 1
Result2: 999

7.2.示例 2:在示例 1 基礎上,用 optional 表示"可能失敗的轉換"

#include <iostream>
#include <optional>
#include <string>

// 字符串转整数,失败返回 nullopt
std::optional<int> to_int(const std::string& s)
{
    try
    {
        size_t pos;
        int val = std::stoi(s, &pos);
        if (pos == s.size())  // 整个字符串都被转换了
        {
            return val;
        }
    }
    catch (...) {}
    return std::nullopt;
}

int main()
{
    auto r1 = to_int("42");
    auto r2 = to_int("3.14");  // 带小数点
    auto r3 = to_int("hello"); // 不是数字

    std::cout << "\"42\" -> " << r1.value_or(-1) << "\n";
    std::cout << "\"3.14\" -> " << r2.value_or(-1) << "\n";
    std::cout << "\"hello\" -> " << r3.value_or(-1) << "\n";

    // 用 if 检查
    if (auto r = to_int("100"); r)
    {
        std::cout << "100 * 2 = " << (*r * 2) << "\n";
    }

    return 0;
}

執行結果

"42" -> 42
"3.14" -> -1
"hello" -> -1
100 * 2 = 200

這裡的std::stoi函式,他的轉換規則是:從字串開頭(必須數字為開頭)開始,儘可能解析一個整數;只要能解析出一部分,就算 stoi 本身成功。 例如:

  • 轉換"123"字串,結果是成功的,為123
  • 轉換"123abc"字串,結果是成功的,為123
  • 轉換"3.14"字串,結果是成功的,為3

而這個函式的pos參數列示std::stoi解析到了字串的哪個位置,也就是第一個沒有被轉換的字元的位置。 例如:

  • 轉換"123abc"字串,val是123,但pos是3。(序號從0開始,如下圖)
123abc
012345

   pos = 3
  • 轉換"3.14"字串,val是3,但pos是1。(序號從0開始,如下圖)
3.14
0123

 pos = 1

這裡的pos == s.size()是不成立的,所以返回std::nullopt

7.3.示例 3:在示例 2 基礎上,用 optional 作為結構體成員("可能沒有"的欄位)

#include <iostream>
#include <optional>
#include <string>

struct Person
{
    std::string name;
    int age;
    std::optional<std::string> nickname;  // 可能没有昵称
    std::optional<int> score;              // 成绩可能尚未录入
};

void print_person(const Person& p)
{
    std::cout << "Name: " << p.name << "\n";
    std::cout << "Age: " << p.age << "\n";

    std::cout << "Nickname: " << p.nickname.value_or("(none)") << "\n";
    std::cout << "Score: " << p.score.value_or(-1) << "\n";

    std::cout << "---\n";
}

int main()
{
    Person p1{"Alice", 20, "Ali", 95};
    Person p2{"Bob", 22, std::nullopt, std::nullopt};  // 没有昵称和成绩

    print_person(p1);
    print_person(p2);

    return 0;
}

執行結果

Name: Alice
Age: 20
Nickname: Ali
Score: 95
---
Name: Bob
Age: 22
Nickname: (none)
Score: -1
---

8.執行結果

見上方每個示例的"執行結果"。

9.示例中的關鍵語法解釋

示例 講了什麼 新出現的語法 為什麼這樣寫 注意事項
示例 1 optional 返回查詢結果 return i; / return std::nullopt;if(opt)*optvalue_or() optional 讓"沒有值"成為正常返回,語義清晰 解引用 *opt 前要確保有值
示例 2 optional 表示轉換失敗 try/catch 包裝 + optional 把異常轉換成空 optional,呼叫者更易處理 也可以用 std::from_chars(C++17)更高效
示例 3 optional 作為成員變數 std::optional<std::string> 成員 "可選欄位"用 optional 比用 magic value(如 -1)更清晰 optional 本身佔用額外空間(T 的大小 + bool + padding)

10.optional 適合"可能沒有",不適合"錯誤詳情很多"

optional 表達的是:這個值可能存在,也可能不存在。它不負責說明“為什麼失敗”。

場景 推薦
查詢名字,可能找不到 std::optional<size_t>
配置項可能沒填 std::optional<std::string>
字串轉數字,只關心成不成功 std::optional<int>
開啟檔案失敗,需要知道許可權/路徑/格式錯誤 錯誤碼、異常、或 expected 類工具
網路請求失敗,需要錯誤型別和錯誤訊息 錯誤物件,不要只用 optional

10.1.示例 4:optional 和錯誤資訊的區別

#include <iostream>
#include <optional>
#include <string>

// optional 表示“可能有值,也可能没有值”的结果。
std::optional<int> parse_port_simple(const std::string& text)
{
    try
    {
        int port = std::stoi(text);
        if (port >= 0 && port <= 65535)
        {
            return port;
        }
    }
    catch (...) {}
    return std::nullopt;
}

struct ParseResult
{
    bool ok;
    int value;
    std::string error;
};

ParseResult parse_port_with_error(const std::string& text)
{
    try
    {
        int port = std::stoi(text);
        if (port < 0 || port > 65535)
        {
            return {false, 0, "port out of range"};
        }
        return {true, port, ""};
    }
    catch (...)
    {
        return {false, 0, "not a number"};
    }
}

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    auto simple = parse_port_simple("70000");
    std::cout << "simple ok? " << (simple ? "yes" : "no") << "\n";

    auto detailed = parse_port_with_error("70000");
    if (!detailed.ok)
    {
        std::cout << "error: " << detailed.error << "\n";
    }

    return 0;
}

執行結果

simple ok? no
error: port out of range

如果呼叫者只關心“有沒有值”,optional 很合適;如果呼叫者需要知道失敗原因,單獨的 optional 就不夠了。

11.常見錯誤

錯誤 1:不檢查直接 *opt

std::optional<int> opt;
std::cout << *opt;  // ❌ 未定义行为!opt 为空

正確做法:先 if (opt) 檢查,或用 opt.value_or(default)

錯誤 2:把 std::nulloptnullptr 混淆

std::optional<int> opt = nullptr;  // ❌ 编译错误!应该用 std::nullopt

正確做法:opt = std::nullopt;

錯誤 3:忘記 value() 會在無值時拋異常

std::optional<int> opt;
std::cout << opt.value();  // ❌ 抛出 std::bad_optional_access

正確做法:用 value_or(default) 或先檢查。

12.使用建議

  1. 函式可能"沒有結果"時返回 std::optional<T>:比返回 -1/nullptr 更安全。
  2. 結構體中"可選欄位"用 optional:比 magic number 語義更清晰。
  3. 預設用 value_or() 而不是 *optvalue_or() 更安全。
  4. std::optional 有額外空間開銷:等於 sizeof(T) + sizeof(bool) + padding,小物件影響不大。
  5. C++23 引入了 std::optional 的 monadic 操作.and_then(), .transform(), .or_else() 鏈式呼叫更優雅。
  6. 需要錯誤詳情時別隻用 optional:optional 只表達有沒有值,不表達為什麼沒有。

13.小結

  • std::optional<T> 要麼包含一個 T,要麼為空。
  • 解決了函式"可能沒結果"的語義問題,替代 magic value。
  • has_value()if(opt) 檢查,用 value_or(default) 安全獲取值。
  • 可用於函式返回值、結構體成員、函式引數(但引數不推薦用 optional)。