C++17 中的 std::variant 与 std::visit 的高级用法

在 C++17 之前,处理多态数据结构通常依赖于继承和虚函数,或者使用 std::any/boost::variant 之类的工具。C++17 引入的 std::variant 为这些方案提供了一种更安全、更高效、且更易维护的替代方案。本文将深入探讨 std::variantstd::visit 的高级用法,并通过实例演示如何在实际项目中灵活运用。


1. 基本概念回顾

  • std::variant:一个类型安全的和(Union),它可以保存指定类型之一。
  • std::visit:用于访问 variant 当前持有的值的函数模板。
#include <variant>
#include <iostream>
#include <string>

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

int main() {
    Var v = 42;
    std::visit([](auto&& arg){ std::cout << arg << std::endl; }, v);
}

2. 访问多种类型的技巧

2.1 递归访问

variant 的成员类型本身是 variant 时,递归访问很有用。下面演示了一个多层嵌套 variant 的解析器。

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

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

void print(const Nested& v);

struct Visitor {
    void operator()(int i) const { std::cout << "int: " << i << '\n'; }
    void operator()(const std::string& s) const { std::cout << "string: " << s << '\n'; }
    void operator()(const Nested& nested) const { print(nested); }  // 递归调用
};

void print(const Nested& v) {
    std::visit(Visitor{}, v);
}

2.2 重载对象

C++17 里 std::visit 支持传递一个重载集合(overloaded functor),这使得代码更简洁。可以用下面的辅助结构实现:

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

使用示例:

std::visit(overloaded{
    [](int i){ std::cout << "int: " << i << '\n'; },
    [](double d){ std::cout << "double: " << d << '\n'; },
    [](const std::string& s){ std::cout << "string: " << s << '\n'; }
}, v);

3. 结合 std::monostate 处理空状态

在某些场景中,你可能想让 variant 代表“无值”状态。std::monostate 就是一个无数据占位符。

using MaybeInt = std::variant<std::monostate, int>;

void handle(MaybeInt mi) {
    std::visit(overloaded{
        [](std::monostate){ std::cout << "None\n"; },
        [](int v){ std::cout << "Value: " << v << '\n'; }
    }, mi);
}

4. 高级模式:类型擦除(Type Erasure)

有时候你需要把 variant 用作“类型擦除容器”。下面示例展示了一个轻量级的 AnyContainer,内部使用 std::variant 存储多种类型。

#include <variant>
#include <functional>
#include <memory>

class AnyContainer {
public:
    template<class T>
    AnyContainer(T val) : holder_(std::make_shared<Holder<T>>(std::move(val))) {}

    template<class T>
    T get() const {
        if(auto p = std::dynamic_pointer_cast<Holder<T>>(holder_))
            return p->value_;
        throw std::bad_cast{};
    }

private:
    struct Base { virtual ~Base() = default; };
    template<class T>
    struct Holder : Base { Holder(T v): value_(std::move(v)){} T value_; };

    std::shared_ptr <Base> holder_;
};

5. 结合 std::variant 与 std::optional

在某些业务场景中,你需要表示“可能存在某种特定类型”。将 std::variantstd::optional 组合可以实现更细粒度的语义。

using OptVariant = std::optional<std::variant<int, std::string>>;

void process(OptVariant ov) {
    if (!ov) {
        std::cout << "No value\n";
        return;
    }
    std::visit(overloaded{
        [](int i){ std::cout << "int: " << i << '\n'; },
        [](const std::string& s){ std::cout << "string: " << s << '\n'; }
    }, *ov);
}

6. 性能考虑

  • variant 的实现通常采用联合 + 类型索引,访问时仅有一次类型检查;
  • std::visit 通过模板展开,避免了运行时的 switch 语句;
  • 对于大对象,建议使用 std::variant<std::reference_wrapper<T>> 或包装为 std::shared_ptr<T>,避免复制。

在性能敏感的代码里,使用 std::visit 的重载对象可避免重复模板实例化,从而减少二进制体积。


7. 实战案例:解析 JSON 的轻量实现

假设我们有一个极简 JSON 解析器,只支持数字、字符串和布尔值。可以用 std::variant 表示 JSON 值:

using JsonValue = std::variant<std::nullptr_t, bool, int, double, std::string>;

JsonValue parse(const std::string& token); // 简化实现

然后使用 std::visit 进行序列化:

std::string toString(const JsonValue& val) {
    return std::visit(overloaded{
        [](std::nullptr_t){ return std::string("null"); },
        [](bool b){ return b ? "true" : "false"; },
        [](int i){ return std::to_string(i); },
        [](double d){ return std::to_string(d); },
        [](const std::string& s){ return '"' + s + '"'; }
    }, val);
}

8. 小结

  • std::variantstd::visit 为多态数据提供了类型安全、零运行时开销的解决方案。
  • 通过重载对象、递归访问、std::monostate 等技术,可以构造出高度灵活且易维护的代码。
  • 在性能与可读性之间做权衡,合理使用 std::variant 的特性(如移动语义、引用包装)能大幅提升项目质量。

掌握这些高级用法后,你就能在 C++17 及更高版本的项目中自如地处理复杂的类型组合,从而写出更健壮、更易维护的代码。

发表评论