std::optional (C++17)

std::optional 是 C++17 标准库中引入的一个模板类,用于表示一个可能包含值,也可能不包含值的对象。它解决了在 C++ 中处理“值可能缺失”这一常见问题。

核心思想:
在很多情况下,一个函数可能无法返回一个有效的结果,或者一个变量可能暂时没有被初始化。传统的 C++ 做法通常有:

  1. 返回特殊值: 例如,返回 nullptr(对于指针)、-10(对于整数),但这要求调用者记住这些特殊值,并且这些特殊值可能与有效值冲突。
  2. 抛出异常: 这会引入额外的开销,并且异常通常用于表示程序中的错误,而不是常规的“值缺失”状态。
  3. 通过输出参数: 函数通过引用参数来返回结果,并返回一个 bool 值表示是否成功。这使得函数签名复杂,并且不符合函数式编程的风格。

std::optional 提供了一种类型安全、明确且惯用的方式来表达这种“可选值”的概念,类似于其他语言中的 MaybeOption 类型。

特点:

  • 值或无值: optional 对象要么包含一个 T 类型的值,要么不包含任何值(处于“空”状态)。
  • 内存开销小: optional<T> 通常只比 T 多占用一个 bool 标志位(用于指示是否有值)的内存,加上可能的对齐填充。
  • 类型安全: 它避免了使用特殊值或 nullptr 带来的类型不匹配和潜在的运行时错误。
  • 明确性: 通过 std::optional,代码的意图更加清晰,即这个值可能不存在。
  • 值语义: std::optional 是一个值类型,支持拷贝、移动、赋值等操作。

头文件:
#include <optional>

基本用法:

  1. 创建 optional 对象:

    • 空状态:
      1
      2
      std::optional<int> opt1; // 默认构造,空状态
      std::optional<std::string> opt2 = std::nullopt; // 使用 std::nullopt 初始化为空
    • 包含值状态:
      1
      2
      3
      std::optional<int> opt3 = 42; // 直接赋值
      std::optional<std::string> opt4{"hello"}; // 列表初始化
      std::optional<double> opt5 = std::make_optional(3.14); // 使用 std::make_optional
  2. 检查是否有值:

    • has_value()
      1
      2
      3
      if (opt3.has_value()) {
      std::cout << "opt3 has value." << std::endl;
      }
    • 隐式转换为 bool
      1
      2
      3
      if (opt4) { // 等同于 opt4.has_value()
      std::cout << "opt4 has value." << std::endl;
      }
  3. 访问值:

    • value() 如果 optional 包含值,返回值的引用;否则抛出 std::bad_optional_access 异常。
      1
      2
      std::cout << "Value of opt3: " << opt3.value() << std::endl;
      // std::cout << "Value of opt1: " << opt1.value() << std::endl; // 运行时抛出异常
    • * 解引用运算符: 如果 optional 包含值,返回值的引用;否则行为未定义(慎用,除非你确定有值)。
      1
      std::cout << "Value of opt3 (dereference): " << *opt3 << std::endl;
    • -> 成员访问运算符: 如果 optional 包含值,用于访问其内部值的成员;否则行为未定义(慎用,除非你确定有值)。
      1
      2
      3
      4
      std::optional<std::string> name = "Alice";
      if (name) {
      std::cout << "Name length: " << name->length() << std::endl;
      }
  4. 提供默认值:value_or()
    如果 optional 包含值,返回该值;否则返回提供的默认值。

    1
    2
    3
    4
    5
    6
    7
    std::optional<int> maybe_age;
    int age = maybe_age.value_or(30); // age 将是 30
    std::cout << "Age: " << age << std::endl;

    std::optional<int> actual_age = 25;
    int real_age = actual_age.value_or(30); // real_age 将是 25
    std::cout << "Real Age: " << real_age << std::endl;
  5. 修改 optional 的状态:

    • 赋值:
      1
      2
      3
      std::optional<int> opt;
      opt = 10; // 现在包含值 10
      opt = std::nullopt; // 现在为空
    • reset()optional 设置为空状态。
      1
      2
      opt = 20;
      opt.reset(); // 现在为空
    • emplace() 构造一个新值到 optional 内部,避免不必要的拷贝/移动。
      1
      2
      3
      std::optional<std::vector<int>> opt_vec;
      opt_vec.emplace(1, 2, 3, 4, 5); // 构造一个包含 5 个元素的 vector
      // 等同于 opt_vec = std::vector<int>{1, 2, 3, 4, 5}; 但可能更高效

示例:一个查找函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <optional>
#include <string>
#include <map>

// 查找一个单词在字典中的定义
std::optional<std::string> find_definition(const std::string& word, const std::map<std::string, std::string>& dictionary) {
auto it = dictionary.find(word);
if (it != dictionary.end()) {
return it->second; // 找到,返回定义
} else {
return std::nullopt; // 没找到,返回空 optional
}
}

int main() {
std::map<std::string, std::string> my_dictionary = {
{"hello", "A greeting."},
{"world", "The earth, together with all of its countries and peoples."},
{"c++", "A powerful programming language."}
};

auto def1 = find_definition("c++", my_dictionary);
if (def1) { // 检查是否有值
std::cout << "Definition of 'c++': " << *def1 << std::endl; // 使用解引用访问
} else {
std::cout << "'c++' not found." << std::endl;
}

auto def2 = find_definition("java", my_dictionary);
// 使用 value_or 提供默认值
std::cout << "Definition of 'java': " << def2.value_or("Not found in dictionary.") << std::endl;

// 尝试访问一个空 optional 的值,会抛出异常
try {
std::optional<int> empty_opt;
std::cout << empty_opt.value() << std::endl;
} catch (const std::bad_optional_access& e) {
std::cerr << "Error: " << e.what() << std::endl;
}

return 0;
}

与指针/引用和特殊值的对比:

特性 std::optional<T> T* (指针) 特殊值 (如 -1, “”, etc.)
语义 明确表示“可能存在值” 表示“可能指向一个对象”或“没有指向任何对象” 需要约定特殊值代表“缺失”
所有权 拥有其内部值 (值语义) 通常不拥有所指对象 (引用语义) 值本身,不涉及所有权
内存开销 sizeof(T) + sizeof(bool) (大致) sizeof(T*) 无额外开销,但可能占用有效值范围
安全性 访问空值抛异常 (.value()) 或未定义行为 (*) 解引用 nullptr 导致未定义行为/崩溃 特殊值可能与有效值冲突,易出错
类型安全 强类型,避免类型混淆 弱类型,指针可以指向任何类型 依赖于约定,可能导致类型语义混淆
使用场景 函数返回可能缺失的值,类成员可能未初始化 动态内存管理,多态,共享/非共享资源 简单场景,但有局限性
可读性/意图 高,清晰表明可选性 中,需要额外文档或约定 低,需要查阅文档或约定

何时使用 std::optional

  • 函数返回可能失败的结果: 当一个函数可能无法计算出有效结果时,返回 std::optional<T> 比返回特殊值或抛出异常更清晰和安全。
  • 类成员可能未初始化: 当一个类的成员变量在构造时可能没有值,但在后续操作中可能会被赋值时,可以使用 std::optional
  • 配置参数: 当某些配置参数是可选的,并且没有默认值时。
  • 避免使用 nullptr 作为“无值”的标记: 对于非指针类型,std::optional 提供了更好的替代方案。

注意事项:

  • 开销: 尽管 std::optional 的内存开销很小,但在某些对性能和内存极度敏感的场景下(例如,大量小型对象组成的容器),可能需要权衡。
  • 异常: value() 方法在访问空 optional 时会抛出异常。如果频繁访问且不确定是否有值,最好先用 has_value()operator bool() 检查,或者使用 value_or() 提供默认值。
  • T 的要求: T 必须是可析构的。C++17 之前,T 还需要是可默认构造的,但 C++17 移除了这个限制。
  • 不要用于引用: std::optional<T&> 是不允许的。如果需要可选的引用,可以使用 std::optional<std::reference_wrapper<T>>

总而言之,std::optional 是 C++17 中一个非常实用的工具,它以类型安全和明确的方式解决了“值可能缺失”的问题,使得代码更健壮、更易读。在现代 C++ 编程中,它已经成为处理可选值的主流方式。