跳到主要内容

C++ 17 std::optional

引言

在编程中,我们经常需要处理一些可能有值也可能没有值的情况。比如:函数可能会返回一个结果,但在某些条件下可能无法返回有效结果;或者某个对象的属性可能存在也可能不存在。在 C++17 之前,我们通常会使用指针(可能为 nullptr)、特殊值(如 -1 表示无效)或异常来处理这类情况。

C++17 引入了 std::optional 类模板,它提供了一种更加优雅和类型安全的方式来表示可能存在也可能不存在的值。

什么是 std::optional?

std::optional 是 C++17 标准库引入的一个类模板,定义在 <optional> 头文件中。它可以包含一个指定类型的值,或者不包含任何值(称为 "空 optional")。

提示

std::optional 解决了我们之前使用特殊值(如 -1、nullptr 等)来表示"没有值"时可能带来的问题,它提供了更加类型安全和语义明确的解决方案。

基本用法

让我们从 std::optional 的基本用法开始:

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

int main() {
// 创建一个包含值的 optional
std::optional<int> opt1 = 42;

// 创建一个空的 optional
std::optional<std::string> opt2;

// 检查 optional 是否包含值
if (opt1.has_value()) {
std::cout << "opt1 包含值: " << opt1.value() << std::endl;
}

if (opt2.has_value()) {
std::cout << "opt2 包含值: " << opt2.value() << std::endl;
} else {
std::cout << "opt2 不包含值" << std::endl;
}

return 0;
}

输出:

opt1 包含值: 42
opt2 不包含值

std::optional 的核心操作

创建 optional 对象

你可以通过多种方式创建 std::optional 对象:

cpp
// 默认构造,创建空 optional
std::optional<int> opt1;

// 使用值初始化
std::optional<int> opt2 = 42;
std::optional<int> opt3(42);
std::optional<int> opt4{42};

// 使用 std::nullopt 显式创建空 optional
std::optional<int> opt5 = std::nullopt;

// 拷贝构造
std::optional<int> opt6 = opt2;

检查 optional 是否包含值

cpp
std::optional<int> opt = 42;

// 使用 has_value() 方法
if (opt.has_value()) {
std::cout << "有值" << std::endl;
}

// 使用布尔转换操作符
if (opt) {
std::cout << "有值" << std::endl;
}

访问 optional 的值

cpp
std::optional<int> opt = 42;

// 使用 value() 方法 - 如果为空会抛出 std::bad_optional_access 异常
int val1 = opt.value();

// 使用 * 操作符 - 不检查是否为空,使用前应先检查
int val2 = *opt;

// 使用 value_or() 方法 - 如果为空则返回默认值
int val3 = opt.value_or(0);

修改 optional 的值

cpp
std::optional<int> opt;

// 赋值新值
opt = 42;

// 重置为空
opt = std::nullopt;
opt.reset();

// 原位构造新值(避免不必要的拷贝)
opt.emplace(42);

实际应用场景

场景一:函数可能失败的返回值

cpp
#include <iostream>
#include <optional>
#include <string>
#include <cctype>

// 尝试将字符串转换为整数,如果失败则返回空 optional
std::optional<int> convertToInt(const std::string& str) {
try {
// 检查字符串是否只包含数字
for (char c : str) {
if (!std::isdigit(c) && c != '-' && c != '+') {
return std::nullopt;
}
}
int value = std::stoi(str);
return value;
} catch (...) {
return std::nullopt;
}
}

int main() {
auto result1 = convertToInt("123");
auto result2 = convertToInt("abc");

if (result1) {
std::cout << "转换成功: " << *result1 << std::endl;
} else {
std::cout << "转换失败" << std::endl;
}

if (result2) {
std::cout << "转换成功: " << *result2 << std::endl;
} else {
std::cout << "转换失败" << std::endl;
}

// 使用 value_or 提供默认值
int value = result2.value_or(-1);
std::cout << "result2 的值(或默认值): " << value << std::endl;

return 0;
}

输出:

转换成功: 123
转换失败
result2 的值(或默认值): -1

场景二:配置选项可能存在也可能不存在

cpp
#include <iostream>
#include <optional>
#include <string>
#include <map>

class Configuration {
private:
std::map<std::string, std::string> settings;

public:
void setSetting(const std::string& key, const std::string& value) {
settings[key] = value;
}

std::optional<std::string> getSetting(const std::string& key) const {
auto it = settings.find(key);
if (it != settings.end()) {
return it->second;
}
return std::nullopt;
}
};

int main() {
Configuration config;
config.setSetting("server", "localhost");
config.setSetting("port", "8080");

auto server = config.getSetting("server");
auto username = config.getSetting("username");

std::cout << "服务器: " << server.value_or("未指定") << std::endl;
std::cout << "用户名: " << username.value_or("未指定") << std::endl;

// 基于配置条件进行操作
if (auto port = config.getSetting("port")) {
std::cout << "连接到端口: " << *port << std::endl;
} else {
std::cout << "使用默认端口" << std::endl;
}

return 0;
}

输出:

服务器: localhost
用户名: 未指定
连接到端口: 8080

场景三:缓存查询结果

cpp
#include <iostream>
#include <optional>
#include <string>
#include <unordered_map>

class Database {
private:
// 模拟数据库
std::unordered_map<int, std::string> data = {
{1, "Alice"},
{2, "Bob"},
{3, "Charlie"}
};

// 查询缓存
mutable std::unordered_map<int, std::optional<std::string>> cache;

public:
std::optional<std::string> getUserName(int userId) const {
// 检查缓存
auto cacheIt = cache.find(userId);
if (cacheIt != cache.end()) {
std::cout << "使用缓存结果" << std::endl;
return cacheIt->second;
}

// 查询"数据库"
std::cout << "查询数据库" << std::endl;
auto it = data.find(userId);
std::optional<std::string> result;

if (it != data.end()) {
result = it->second;
} else {
result = std::nullopt;
}

// 更新缓存(即使结果为空也缓存,避免重复查询)
cache[userId] = result;
return result;
}
};

int main() {
Database db;

// 第一次查询用户 1
if (auto name = db.getUserName(1)) {
std::cout << "用户 1: " << *name << std::endl;
}

// 再次查询用户 1(应该使用缓存)
if (auto name = db.getUserName(1)) {
std::cout << "用户 1: " << *name << std::endl;
}

// 查询不存在的用户
if (auto name = db.getUserName(4)) {
std::cout << "用户 4: " << *name << std::endl;
} else {
std::cout << "用户 4 不存在" << std::endl;
}

// 再次查询不存在的用户(应该使用缓存)
if (auto name = db.getUserName(4)) {
std::cout << "用户 4: " << *name << std::endl;
} else {
std::cout << "用户 4 不存在" << std::endl;
}

return 0;
}

输出:

查询数据库
用户 1: Alice
使用缓存结果
用户 1: Alice
查询数据库
用户 4 不存在
使用缓存结果
用户 4 不存在

std::optional 与其他方法的比较

让我们对比一下使用 std::optional 和其他替代方法的区别:

cpp
#include <iostream>
#include <optional>
#include <string>
#include <memory>

// 方法1: 使用指针表示可能为空的返回值
std::string* findByName1(const std::string& name) {
if (name == "admin") {
return new std::string("Administrator");
}
return nullptr;
}

// 方法2: 使用特殊值表示无效结果
std::string findByName2(const std::string& name) {
if (name == "admin") {
return "Administrator";
}
return ""; // 使用空字符串表示没找到
}

// 方法3: 使用 std::optional
std::optional<std::string> findByName3(const std::string& name) {
if (name == "admin") {
return "Administrator";
}
return std::nullopt;
}

int main() {
std::string searchName = "user";

// 使用指针的方式
std::string* result1 = findByName1(searchName);
if (result1) {
std::cout << "方法1找到: " << *result1 << std::endl;
delete result1; // 需要记住释放内存
} else {
std::cout << "方法1未找到" << std::endl;
}

// 使用特殊值的方式
std::string result2 = findByName2(searchName);
if (!result2.empty()) {
std::cout << "方法2找到: " << result2 << std::endl;
} else {
std::cout << "方法2未找到" << std::endl;
}

// 使用 std::optional 的方式
std::optional<std::string> result3 = findByName3(searchName);
if (result3) {
std::cout << "方法3找到: " << *result3 << std::endl;
} else {
std::cout << "方法3未找到" << std::endl;
}

return 0;
}

输出:

方法1未找到
方法2未找到
方法3未找到

比较分析

  1. 使用指针:

    • 需要手动管理内存(可能忘记释放)
    • 不能直接表示值类型的缺失
    • 可能导致空指针解引用
  2. 使用特殊值:

    • 对于某些类型难以找到合适的"特殊值"
    • 语义不明确,调用者需要知道空字符串表示"没找到"
    • 对原始类型(如 int)尤其困难
  3. 使用 std::optional

    • 语义明确
    • 不需要手动管理内存
    • 可以用于任何类型
    • 提供了丰富的 API 进行安全操作

std::optional 的性能考虑

std::optional<T> 内部大致相当于一个 T 加一个布尔标志,所以对于小型对象,它可能会增加存储开销。如果 T 是一个非常大的类型,通常最好使用 std::optional<T&> 或者指针。

备注

std::optional 不支持引用作为模板参数,因为引用总是指向某个对象,不能为"空"。如果要实现类似功能,可以考虑使用 std::reference_wrapper 或智能指针。

高级用法

与 std::nullopt_t 结合使用

std::nulloptstd::nullopt_t 类型的常量,用于表示空的 optional:

cpp
std::optional<int> opt = 42;
// ...
opt = std::nullopt; // 显式地将 opt 设置为空

构造就地修改

cpp
std::optional<std::vector<int>> opt;
// 在 optional 内部直接构造对象,避免不必要的拷贝
opt.emplace({1, 2, 3, 4, 5});
std::cout << "向量大小: " << opt->size() << std::endl;

结合 if 语句使用

C++17 允许在 if 语句中初始化变量,结合 std::optional 使用非常方便:

cpp
std::optional<int> getAnswer() {
return 42;
}

// 在 if 语句中初始化并检查
if (auto answer = getAnswer(); answer) {
std::cout << "答案是: " << *answer << std::endl;
} else {
std::cout << "没有答案" << std::endl;
}

链式调用与 map 操作

虽然 C++ 标准库中的 std::optional 没有提供类似 Rust 或函数式编程的 map 操作,但我们可以自己实现类似的功能:

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

template<typename T, typename F>
auto transform(const std::optional<T>& opt, F func) -> std::optional<decltype(func(std::declval<T>()))> {
if (opt) {
return func(*opt);
}
return std::nullopt;
}

int main() {
std::optional<std::string> name = "Alice";

auto nameLength = transform(name, [](const std::string& s) { return s.length(); });

if (nameLength) {
std::cout << "名字长度: " << *nameLength << std::endl;
}

return 0;
}

输出:

名字长度: 5

总结

std::optional 是 C++17 引入的一个非常实用的工具,它提供了一种类型安全、语义明确的方式来表示可能存在也可能不存在的值。它的主要优点包括:

  1. 显式地表达了值可能不存在的语义
  2. 不需要使用特殊值或指针来表示缺失的值
  3. 提供了丰富的 API 进行安全操作
  4. 可以与任何类型一起使用
  5. 不需要手动管理内存

通过本教程,我们已经了解了 std::optional 的基本用法、常见操作、实际应用场景以及与其他方法的比较。掌握 std::optional 可以使你的代码更加清晰、安全,并且更好地表达意图。

练习

  1. 编写一个函数,接受一个字符串,如果该字符串是有效的浮点数,则返回其值的平方,否则返回空 optional。
  2. 实现一个简单的缓存系统,使用 std::optional 来表示缓存命中或未命中。
  3. 修改上面的 transform 函数,使其支持链式调用(即 transform(transform(opt, func1), func2))。
  4. 创建一个用户配置类,使用 std::optional 来表示可选的配置项。

额外资源

提示

当你不确定一个函数是否会返回有意义的值时,考虑使用 std::optional 作为返回类型,而不是使用特殊值或指针。这样可以使你的代码意图更加明确,并且更加安全。