第 18.14 節
std::optional
0瀏覽次數0訪問次數--跳出率--平均停留
本節解決什麼問題
函數返回結果時,如何表示"沒有結果"?
常見的做法各有缺點:
- 返回
-1或nullptr:語義不清晰,且-1可能是合法值。 - 返回
bool+ 輸出參數:代碼冗長,需要先聲明變量。 - 拋出異常:異常有開銷,且"查不到"往往不算異常情況。
std::optional<T> 優雅地表達了"可能有值、也可能沒有值"的語義。
這個特性是什麼
std::optional<T> 是一個模板類,它要麼包含一個 T 類型的值,要麼為空(沒有值)。它讓"可能沒有值"這個信息成為類型的一部分,編譯器幫你檢查。
C++ 標準版本
C++17
需要的頭文件
#include <optional>
基本語法
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); // 若无值返回默认值
常用用法
| 操作 | 説明 |
|---|---|
opt.has_value() | 是否有值 |
opt.value() | 獲取值(無值拋 std::bad_optional_access) |
*opt | 獲取值(無值行為未定義,先檢查!) |
opt.value_or(default) | 獲取值,無值返回默認值 |
opt = std::nullopt | 清空 |
opt.emplace(args...) | 原地構造值 |
示例代碼
示例 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
示例 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
示例 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
---
運行結果
見上方每個示例的"運行結果"。
示例中的關鍵語法解釋
| 示例 | 講了什麼 | 新出現的語法 | 為什麼這樣寫 | 注意事項 |
|---|---|---|---|---|
| 示例 1 | optional 返回查找結果 | return i; / return std::nullopt;、if(opt)、*opt、value_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) |
optional 適合"可能沒有",不適合"錯誤詳情很多"
optional 表達的是:這個值可能存在,也可能不存在。它不負責説明“為什麼失敗”。
| 場景 | 推薦 |
|---|---|
| 查找名字,可能找不到 | std::optional<size_t> |
| 配置項可能沒填 | std::optional<std::string> |
| 字符串轉數字,只關心成不成功 | std::optional<int> |
| 打開文件失敗,需要知道權限/路徑/格式錯誤 | 錯誤碼、異常、或 expected 類工具 |
| 網絡請求失敗,需要錯誤類型和錯誤消息 | 錯誤對象,不要只用 optional |
示例 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 就不夠了。
常見錯誤
錯誤 1:不檢查直接 *opt
std::optional<int> opt;
std::cout << *opt; // ❌ 未定义行为!opt 为空
正確做法:先 if (opt) 檢查,或用 opt.value_or(default)。
錯誤 2:把 std::nullopt 和 nullptr 混淆
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) 或先檢查。
使用建議
- 函數可能"沒有結果"時返回
std::optional<T>:比返回 -1/nullptr 更安全。 - 結構體中"可選字段"用 optional:比 magic number 語義更清晰。
- 默認用
value_or()而不是*opt:value_or()更安全。 std::optional有額外空間開銷:等於sizeof(T) + sizeof(bool) + padding,小對象影響不大。- C++23 引入了
std::optional的 monadic 操作:.and_then(),.transform(),.or_else()鏈式調用更優雅。 - 需要錯誤詳情時別隻用 optional:optional 只表達有沒有值,不表達為什麼沒有。
小結
std::optional<T>要麼包含一個T,要麼為空。- 解決了函數"可能沒結果"的語義問題,替代 magic value。
- 用
has_value()或if(opt)檢查,用value_or(default)安全獲取值。 - 可用於函數返回值、結構體成員、函數參數(但參數不推薦用 optional)。