optional、variant 和 any
本课属于 Coddy C++ 学习旅程的“面向对象编程”单元。第 89 课,共 104 课。
现代 C++ 在 <optional>、<variant> 和 <any> 头文件中提供了三种词汇类型,可帮助你处理可能不存在的值、可以保存多种类型之一的值,或能够存储任意类型的值。
std::optional<T> 表示一个可能存在也可能不存在的值,非常适合用于可能失败但不会抛出异常的函数:
#include <iostream>
#include <optional>
std::optional<int> findIndex(const std::string& str, char c) {
for (size_t i = 0; i < str.size(); ++i) {
if (str[i] == c) return i;
}
return std::nullopt; // 没有值
}
int main() {
auto result = findIndex("hello", 'l');
if (result.has_value()) {
std::cout << "Found at: " << *result << "\n"; // 2
}
}std::variant<Types...> 是一种类型安全的联合体,它在任意时刻只保存指定类型中的一种:
#include <iostream>
#include <variant>
#include <string>
int main() {
std::variant<int, double, std::string> data;
data = 42;
std::cout << std::get<int>(data) << "\n";
data = "hello";
if (std::holds_alternative<std::string>(data)) {
std::cout << std::get<std::string>(data) << "\n";
}
}std::any 可以存储任意类型的值,具体类型在运行时确定。使用 std::any_cast 获取该值:
#include <iostream>
#include <any>
int main() {
std::any value = 10;
std::cout << std::any_cast<int>(value) << "\n";
value = std::string("text");
std::cout << std::any_cast<std::string>(value) << "\n";
}对于可为空的值,选择 std::optional;如果你在编译时就知道可能的类型,选择 std::variant;只有在确实需要运行时类型灵活性时,才选择 std::any。
挑战
简单让我们构建一个配置系统,展示现代 C++ 词汇类型的强大之处。你将创建一个灵活的设置管理器,用来处理可能不存在、可以保存不同类型,或需要在运行时灵活确定类型的值。
你将把代码分成三个文件:
ConfigTypes.h:定义配置值类型和辅助函数。创建一个名为
parseValue的函数,接收一个字符串,并尝试将其解析为整数。如果字符串表示有效整数,就返回封装在std::optional中的整数。如果解析失败(字符串包含非数字字符),就返回std::nullopt。为简单起见,只有字符串仅包含数字(并且可选地以负号开头)时,才将其视为有效字符串。还要使用
std::variant创建一个名为Setting的类型别名,使其可以保存int、double或std::string。最后,创建一个名为
describeSetting的函数,接收一个Setting,并返回一个字符串,说明其中保存的类型和值,格式如下:- 对于 int:
Integer: [value] - 对于 double:
Double: [value] - 对于 string:
String: [value]
- 对于 int:
DynamicStore.h:使用std::any创建一个简单的键值存储。定义一个
DynamicStore类,使其能够存储任意类型的值。它应包含:- 一个
set方法,接收字符串键和std::any值,并在内部存储它们(使用std::map) - 一个
get方法,接收一个键并返回对应的std::any值(如果键不存在,就返回空的std::any) - 一个
hasKey方法,如果键存在就返回 true
- 一个
main.cpp:读取三个输入:- 一个可能是数字的字符串(用于测试
std::optional) - 一个类型标识:
int、double或string - 一个与该类型对应的值
演示全部三种词汇类型:
首先,将第一个输入传给你的
parseValue函数。如果其中包含一个值,就打印Parsed: [value]。如果它为空,就打印Parse failed。其次,根据类型标识创建一个
Setting变体。如果类型是int,就将值存储为整数。如果是double,就将值存储为双精度浮点数。如果是string,就将值存储为字符串。然后调用describeSetting并打印结果。第三,创建一个
DynamicStore,将字符串"config_loaded"存储在键"status"下,然后取出它并打印:Status: [value]。使用std::any_cast<std::string>提取该值。- 一个可能是数字的字符串(用于测试
例如,输入 42、int 和 100 时:
Parsed: 42
Integer: 100
Status: config_loaded输入 hello、double 和 3.14 时:
Parse failed
Double: 3.14
Status: config_loaded输入 -15、string 和 username 时:
Parsed: -15
String: username
Status: config_loaded记得在需要的地方包含相应的头文件:<optional>、<variant>、<any>、<string> 和 <map>。使用 std::holds_alternative 和 std::get 来操作变体,并使用 has_value() 或直接进行布尔转换来检查 optional。
亲手试试
#include <iostream>
#include <string>
#include <any>
#include "ConfigTypes.h"
#include "DynamicStore.h"
int main() {
// 读取 3 个输入
std::string input1; // 可能是数字的字符串(用于测试 std::optional)
std::string typeIndicator; // 类型标识:int、double 或 string
std::string value; // 与该类型对应的值
std::cin >> input1;
std::cin >> typeIndicator;
std::cin >> value;
// TODO:第 1 部分:用 parseValue 测试 std::optional
// 将 input1 传给 parseValue 函数
// 如果其中有值,输出 "Parsed: [value]"
// 如果为空,输出 "Parse failed"
// TODO:第 2 部分:用 Setting 测试 std::variant
// 根据 typeIndicator 创建一个 Setting 变体
// 如果类型是 "int",将值存为整数
// 如果类型是 "double",将值存为 double 类型
// 如果类型是 "string",就将值存为字符串
// 然后调用 describeSetting 并输出结果
// TODO:第 3 部分:用 DynamicStore 测试 std::any
// 创建一个 DynamicStore
// 将字符串 "config_loaded" 存到键 "status" 下
// 取出它并输出:"Status: [value]"
// 用 std::any_cast<std::string> 提取值
return 0;
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
12现代 C++ 特性
移动语义与右值完美转发深入了解 Lambda 表达式std::function 与 std::bindconstexpr 与 consteval结构化绑定optional、variant 和 any自己练习: C++ 在线编译器