Menu
CoddyTech

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。

challenge icon

挑战

简单

让我们构建一个配置系统,展示现代 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]
  • DynamicStore.h:使用 std::any 创建一个简单的键值存储。

    定义一个 DynamicStore 类,使其能够存储任意类型的值。它应包含:

    • 一个 set 方法,接收字符串键和 std::any 值,并在内部存储它们(使用 std::map)
    • 一个 get 方法,接收一个键并返回对应的 std::any 值(如果键不存在,就返回空的 std::any)
    • 一个 hasKey 方法,如果键存在就返回 true
  • main.cpp:读取三个输入:
    1. 一个可能是数字的字符串(用于测试 std::optional)
    2. 一个类型标识:int、double 或 string
    3. 一个与该类型对应的值

    演示全部三种词汇类型:

    首先,将第一个输入传给你的 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;
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: C++ 在线编译器