C++17 中 std::variant 的实用技巧与典型场景

在 C++17 之后,std::variant 为多态值提供了类型安全的包装器。它可以用来替代传统的 boost::variant 或者自定义的 union,并结合 std::visit 提供了更简洁、更安全的访问方式。本文从基本使用、类型推断、错误处理、递归结构以及高效访问四个方面,阐述了 std::variant 的实用技巧,并给出代码示例,帮助读者在实际项目中快速上手。

1. 基本定义与初始化

#include <variant>
#include <string>
#include <iostream>

using VariantType = std::variant<int, double, std::string>;

VariantType v1 = 42;                 // 直接赋值
VariantType v2 = 3.14;               // 直接赋值
VariantType v3 = std::string("hello"); // 需要显式类型

如果你想在初始化时提供默认值,可以使用 std::variant 的构造函数:

VariantType v4(42); // 指定类型为 int
VariantType v5 = 3.14; // 自动推断为 double

2. 访问值:std::getstd::get_if

  • `std::get (v)` 在类型不匹配时抛出 `std::bad_variant_access` 异常。
  • `std::get_if (&v)` 返回指向 T 的指针,类型不匹配时返回 `nullptr`。
try {
    int i = std::get <int>(v1); // 成功
    double d = std::get <double>(v1); // 抛出异常
} catch (const std::bad_variant_access& e) {
    std::cerr << "访问错误: " << e.what() << '\n';
}

if (auto p = std::get_if<std::string>(&v3)) {
    std::cout << "字符串: " << *p << '\n';
}

3. 访问值:std::visit

使用 std::visit 可以一次性处理所有可能的类型,避免写多层 ifswitch

std::visit([](auto&& arg) {
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>)
        std::cout << "int: " << arg << '\n';
    else if constexpr (std::is_same_v<T, double>)
        std::cout << "double: " << arg << '\n';
    else if constexpr (std::is_same_v<T, std::string>)
        std::cout << "string: " << arg << '\n';
}, v1);

若需要返回值,可以使用 std::visit 的返回形式:

auto len = std::visit([](auto&& arg) -> size_t {
    return std::to_string(arg).length();
}, v1);

4. 递归 std::variant(树形结构)

在树形结构(如 AST、JSON)中,常用 std::variant 搭配 std::unique_ptrstd::shared_ptr 实现递归类型:

struct JsonValue; // 前向声明

using JsonArray = std::vector<std::unique_ptr<JsonValue>>;
using JsonObject = std::unordered_map<std::string, std::unique_ptr<JsonValue>>;

struct JsonValue {
    std::variant<
        std::nullptr_t,
        bool,
        double,
        std::string,
        JsonArray,
        JsonObject> value;
};

递归访问同样借助 std::visit

void printJson(const JsonValue& j, int indent = 0) {
    std::visit([&](auto&& arg) {
        using T = std::decay_t<decltype(arg)>;
        if constexpr (std::is_same_v<T, std::nullptr_t>) {
            std::cout << std::string(indent, ' ') << "null\n";
        } else if constexpr (std::is_same_v<T, bool>) {
            std::cout << std::string(indent, ' ') << (arg ? "true" : "false") << '\n';
        } else if constexpr (std::is_same_v<T, double> ||
                             std::is_same_v<T, std::string>) {
            std::cout << std::string(indent, ' ') << arg << '\n';
        } else if constexpr (std::is_same_v<T, JsonArray>) {
            std::cout << std::string(indent, ' ') << "[\n";
            for (const auto& el : arg) printJson(*el, indent + 2);
            std::cout << std::string(indent, ' ') << "]\n";
        } else if constexpr (std::is_same_v<T, JsonObject>) {
            std::cout << std::string(indent, ' ') << "{\n";
            for (const auto& [k, v] : arg) {
                std::cout << std::string(indent + 2, ' ') << k << ": ";
                printJson(*v, indent + 2);
            }
            std::cout << std::string(indent, ' ') << "}\n";
        }
    }, j.value);
}

5. 优化访问:使用 std::variant::indexstd::visit

当你只关心某些类型且不想写完整的 if constexpr,可以结合 index

auto& val = v1;
switch (val.index()) {
    case 0: std::cout << "int: " << std::get<int>(val); break;
    case 1: std::cout << "double: " << std::get<double>(val); break;
    case 2: std::cout << "string: " << std::get<std::string>(val); break;
}

6. 结合模板与 std::variant

你可以把 std::variant 放进模板类,以实现类型擦除:

template<typename... Ts>
class VariantHolder {
public:
    using Variant = std::variant<Ts...>;
    VariantHolder(Variant v) : var(std::move(v)) {}

    template<typename F>
    decltype(auto) apply(F&& f) {
        return std::visit(std::forward <F>(f), var);
    }
private:
    Variant var;
};

使用时:

VariantHolder<int, std::string> holder(42);
holder.apply([](auto&& arg){ std::cout << arg << '\n'; });

7. 错误信息与调试技巧

  • std::variantindex() 方法返回当前存储类型的下标,从 0 开始。
  • std::visit 的访问信息可以用 std::type_identity_t 获取编译期类型名(C++20 以上):
std::visit([](auto&& arg) {
    std::cout << __PRETTY_FUNCTION__ << '\n';
}, v1);

这对于调试时查看当前类型非常有用。

8. 性能注意

  • std::variant 内部使用 std::aligned_union 存储数据,大小等于最大成员的大小加上索引所需的字节。
  • 对于大对象,建议存放指针(如 std::shared_ptr)而不是对象本身,以避免拷贝成本。
using BigVariant = std::variant<
    std::nullptr_t,
    bool,
    std::shared_ptr<std::vector<int>>>; // 只存指针

9. 典型应用场景

  1. 网络协议解析:不同字段类型映射到 variant,易于处理可变结构。
  2. GUI 事件系统:事件携带不同类型的数据,使用 variant 简化事件处理。
  3. 日志系统:日志条目可包含整数、字符串、时间戳等多种类型。
  4. AST(抽象语法树):节点类型多样,递归 variant 结构天然匹配。

10. 小结

std::variant 是 C++17 引入的重要特性,它在类型安全、易用性和性能之间取得了良好的平衡。通过 std::getstd::get_ifstd::visit 等工具,你可以轻松地读写多态值。递归结构、模板组合以及性能优化等技巧,让 std::variant 成为构建现代 C++ 应用不可或缺的组件。希望本文的实用技巧能帮助你在项目中快速利用 std::variant,让代码更简洁、更安全。

发表评论