1) 概念速览

std::variant<Ts...> 是 C++17 的“类型安全的联合”(discriminated union / sum type):在任意时刻只保存 其中一个替代类型的值,并带有一个标签(index)说明当前是哪种替代。头文件:<variant>

特点要记住:

  • 存储是内嵌(对象直接放在 variant 内部),不会向堆上额外分配用于存储替代值(因此内部空间按最大替代类型大小和对齐来分配)。([SAPO 评估服务器][2], [C++参考文献][1])
  • 某些类型不能作为替代:引用、数组、void 等不被允许(可以用 std::reference_wrapper<T> 存引用语义)。([C++参考文献][1])

2) 基本构造与访问(核心 API)

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

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

int main(){
    Var v1; // 默认构造:构造 “第一个替代” 的 value-initialized 值(如果第一个是 int -> 0)
    v1 = 42;            // 将变为 int
    v1 = std::string{"hi"}; // 将变为 std::string

    // 访问:
    if (std::holds_alternative<int>(v1)) {
        int x = std::get<int>(v1);        // 若类型不匹配,会抛 std::bad_variant_access
        std::cout << x << '\n';
    }

    if (auto p = std::get_if<std::string>(&v1)) { // 非抛出检查,返回指针或 nullptr
        std::cout << *p << '\n';
    }
}
  • index() 返回当前替代的 0-based 索引;当 variant 处于 valueless_by_exception()(见下)时返回 variant_npos
  • std::get<T>(v):按类型取值,要求该类型在替代中唯一,否则编译出错;类型不匹配时运行时会抛 std::bad_variant_accessstd::get<I>(v) 按索引访问也有类似行为。([C++参考文献][3])

3) std::visit(模式匹配风格访问)—— 推荐首选

std::visit 将 visitor 应用于 variant 中实际持有的值。它比 get_if/holds_alternative 更简洁、安全,也方便实现“模式匹配”风格的处理(尤其同时访问多个 variant 时)。([TCS RWTH][4])

常见的 overloaded 助手(C++17):

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

std::variant<int,std::string> v = "hello";
std::visit(overloaded{
    [](int i){ std::cout << "int: " << i << '\n'; },
    [](const std::string& s){ std::cout << "str: " << s << '\n'; }
}, v);
  • std::visit 也支持 同时访问多个 variant:visitor 必须对所有可能的组合都有效(所有组合的调用表达式必须是可编译且返回同一类型或可推断的值)。([TCS RWTH][4])

4) 构造/赋值细节(常踩雷)

  • 默认构造:构造第一个替代(value-initialized)。若第一个类型不可默认构造,默认构造 variant 就不可用。([C++参考文献][5])

  • in-place 构造emplace<T>(args...)in_place_type_t<T> / in_place_index_t<I> 可以直接在 variant 内构造指定替代,避免某些临时。

  • 从某值构造/赋值variant 有模板构造函数与模板赋值运算符,会通过重载解析决定采用哪个替代(选择能从传入值构造的替代)。因此:

    • 如果多个替代都能从传入值构造,可能会产生二义或编译错误(避免替代类型重复或歧义)。
    • 某些隐式转换可能会选到“意料之外”的替代(例如布尔/整型混合时需小心顺序/类型)。([C++参考文献][6])

5) 异常安全 & valueless_by_exception

std::variant 在某些失败的类型变更(或 move/copy 过程中)可能变为无值(valueless_by_exception),此时 index()==variant_nposget/visit 会抛 std::bad_variant_access。常见触发场景包括:在移动赋值/拷贝赋值或类型变更构造时,被构造的新替代抛出异常且没有办法恢复旧值(因为 variant 不分配额外堆内存来做双缓冲),标准规定在若干情形下会(或可)变为无值。要避免:保证替代类型的移动/拷贝构造不抛异常,或使用替代方案(如 Boost.Variant2 可实现“永不 valueless”策略)。([C++参考文献][7], [boost.org][8])

6) 类型元工具(编译期操作)

  • std::variant_size_v<Variant>:替代数量。
  • std::variant_alternative_t<I, Variant>:索引 I 的替代类型(可用于型别萃取、static_assert 等)。([C++参考文献][9])

示例:

static_assert(std::variant_size_v<std::variant<int,double>> == 2);
using MyFirst = std::variant_alternative_t<0, std::variant<int,double>>; // int

7) 常见陷阱与实践建议(实战)

  • 不要把相同类型重复列在替代列表里std::variant<int,int> 虽然有编译器接受,但 get<T> 会变成 ill-formed,因为 T 不是唯一替代)。若需要“同一底层类型的两种语义”,用 wrapper 类型或强类型别名。([Stack Overflow][10], [C++参考文献][3])
  • 不要直接用引用类型作为替代:若需要引用语义,用 std::reference_wrapper<T> 或指针。([C++参考文献][1])
  • 注意隐式转换选择:如 variant<int,bool> 对相同字面量赋值可能被某一替代选中。要避免歧义,要么显式 std::in_place_type_t,要么调整替代顺序/类型。
  • 性能std::variant 的内存开销是固定的(取决于最大替代大小 + 标签),没有堆分配;但类型切换(destroy + construct)会有开销。在对性能极为敏感的场景可考虑手工优化或其它设计(例如使用 tagged union 或专门内存布局)。([C++参考文献][1])

8) 进阶用法

完整示例(包括多 variant 访问、in-place、get_if、overloaded):

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

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

int main(){
    using V1 = std::variant<std::monostate, int, std::string>;
    V1 a{}; // holds std::monostate (第一个替代)
    a.emplace<std::string>("hello");

    // 非抛出读取
    if (auto p = std::get_if<std::string>(&a)) std::cout << *p << '\n';

    // visit
    std::visit(overloaded{
        [](std::monostate){ std::cout << "empty\n"; },
        [](int i){ std::cout << "int " << i << '\n'; },
        [](const std::string& s){ std::cout << "str " << s << '\n'; }
    }, a);

    // 两个 variant 同时 visit
    std::variant<int,double> x = 3;
    std::variant<int,double> y = 2.5;
    std::visit([](auto&& lhs, auto&& rhs){
        // 这里要求对所有可能组合 lhs+rhs 都成立。用 if constexpr 可做分支。
        using L = std::decay_t<decltype(lhs)>;
        using R = std::decay_t<decltype(rhs)>;
        if constexpr(std::is_same_v<L,std::string> || std::is_same_v<R,std::string>){
            std::cout << "string involved\n";
        } else {
            std::cout << (lhs + rhs) << '\n';
        }
    }, x, y);
}

9) 替代方案与补充

  • 如果你需要永不 valueless 的 variant,考虑 Boost.Variant2(通过 double-buffering 等策略避免 valueless)。([boost.org][8])
  • 若需要递归变体(variant 中含有自己类型),std::variant 无直接递归包装器(Boost 有 recursive_wrapper),常见做法是用 std::unique_ptr/std::shared_ptr 或自定义 wrapper 来打破递归。

10) 快速备忘(常用 API 汇总)

  • 构造:variant<Ts...>, in_place_type_t<T>, in_place_index_t<I>, emplace<T>(args...)。([C++参考文献][5])
  • 查询:index(), valueless_by_exception(), holds_alternative<T>()。([SAPO 评估服务器][2])
  • 访问:std::get<T/I>(v)(抛异常),std::get_if<T/I>(&v)(返回指针或 nullptr),std::visit(visitor, v, ...)。([C++参考文献][3], [TCS RWTH][4])
  • 元编程:std::variant_size, std::variant_alternative_t。([C++参考文献][9])

11) 综合示例

在 SLAM 系统里,我们常常要处理多种类型的消息(图像帧、IMU 数据、LiDAR 扫描、点云、控制事件等)。这类场景天然适合用 std::variant 做事件 / 数据包的统一封装,这样可以写一个通用队列、总线或序列化层。


1. 典型需求建模

在 SLAM 系统中,可能会有:

  • ImageFrame(相机数据)
  • IMUFrame(惯性测量单元数据)
  • LiDARFrame(点云数据)
  • ControlEvent(系统状态切换、回环检测触发等)

长可能会用:

using Message = std::variant<ImageFrame, IMUFrame, LiDARFrame, ControlEvent>;

这就是“统一消息类型”。系统中所有队列、事件分发器、网络传输层都只处理 Message,而具体怎么访问,交给 std::visit


2. 代码综合示例

#include <variant>
#include <string>
#include <vector>
#include <iostream>
#include <chrono>
#include <cstdint>
#include <nlohmann/json.hpp>  // 假设用 JSON 序列化

// -------------------- 各类消息定义 --------------------
struct ImageFrame {
    int id;
    int width, height;
    std::vector<uint8_t> data; // 假设灰度图
};

struct IMUFrame {
    int id;
    double gx, gy, gz;
    double ax, ay, az;
};

struct LiDARFrame {
    int id;
    std::vector<float> points; // 简化:xyzxyz...
};

struct ControlEvent {
    std::string command;
};

// -------------------- Variant 类型 --------------------
using Message = std::variant<ImageFrame, IMUFrame, LiDARFrame, ControlEvent>;

// -------------------- JSON 序列化 --------------------
// 用 visitor 实现统一序列化
nlohmann::json to_json(const Message& msg) {
    return std::visit([&](auto&& m) -> nlohmann::json {
        using T = std::decay_t<decltype(m)>;
        if constexpr (std::is_same_v<T, ImageFrame>) {
            return {{"type","image"},{"id",m.id},{"w",m.width},{"h",m.height},
                    {"data_size", m.data.size()}};
        } else if constexpr (std::is_same_v<T, IMUFrame>) {
            return {{"type","imu"},{"id",m.id},
                    {"gyro",{m.gx,m.gy,m.gz}},{"acc",{m.ax,m.ay,m.az}}};
        } else if constexpr (std::is_same_v<T, LiDARFrame>) {
            return {{"type","lidar"},{"id",m.id},{"point_count", m.points.size()/3}};
        } else if constexpr (std::is_same_v<T, ControlEvent>) {
            return {{"type","control"},{"cmd",m.command}};
        }
    }, msg);
}

// -------------------- 示例主函数 --------------------
int main(){
    // 构造一些消息
    ImageFrame img{1,640,480,std::vector<uint8_t>(640*480, 123)};
    IMUFrame imu{2,0.1,0.2,0.3,9.8,0.0,0.1};
    LiDARFrame lidar{3,{0.0f,1.0f,2.0f, 1.0f,2.0f,3.0f}};
    ControlEvent ev{"RESET"};

    std::vector<Message> bus;
    bus.push_back(img);
    bus.push_back(imu);
    bus.push_back(lidar);
    bus.push_back(ev);

    // 统一处理
    for (auto& m : bus) {
        std::cout << to_json(m).dump() << "\n";
        std::visit([](auto&& msg){
            using T = std::decay_t<decltype(msg)>;
            if constexpr (std::is_same_v<T, ImageFrame>)
                std::cout << "Image: " << msg.width << "x" << msg.height << "\n";
            else if constexpr (std::is_same_v<T, IMUFrame>)
                std::cout << "IMU acc=" << msg.ax << "," << msg.ay << "\n";
            else if constexpr (std::is_same_v<T, LiDARFrame>)
                std::cout << "LiDAR points: " << msg.points.size()/3 << "\n";
            else if constexpr (std::is_same_v<T, ControlEvent>)
                std::cout << "Event: " << msg.command << "\n";
        }, m);
    }
}

输出示例:

{"data_size":307200,"h":480,"id":1,"type":"image","w":640}
Image: 640x480
{"acc":[9.8,0,0.1],"gyro":[0.1,0.2,0.3],"id":2,"type":"imu"}
IMU acc=9.8,0
{"id":3,"point_count":2,"type":"lidar"}
LiDAR points: 2
{"cmd":"RESET","type":"control"}
Event: RESET

3. 设计建议

优点

  1. 类型安全:不用写 void*union,编译器帮你保证访问安全。
  2. 统一总线:事件队列、网络消息只处理一个 Message 类型,模块解耦。
  3. 高效:variant 不会动态分配额外堆空间,所有数据内嵌(除了容器内部自己管理的)。

️ 注意事项

  • 大数据存储(图像/点云):尽量避免拷贝。variant 本身不会 heap 分配,但 std::vector 内部数据会分配 → 可结合 引用计数智能指针(shared_ptr零拷贝 buffer(span / memory pool) 来优化。
  • 网络序列化std::visit 非常适合统一“打包/解包”逻辑(可以 JSON、flatbuffers、protobuf 等)。
  • 队列性能:在高频 IMU/点云流里,variant 可能成为队列泛型的瓶颈(频繁构造/析构),可用 对象池/预分配队列 避免频繁 new/delete。
  • 调度策略:可以在消息上挂 timestamp 字段,做时间同步(variant 类型里带一个基类/公共 header struct)。

4. 进阶扩展

  • 多机器人/分布式 SLAM:你可以把 Message 打包成网络包传输(JSON/Protobuf),在另一台机器上解开并用 std::visit 处理。
  • 消息过滤/订阅:variant 可与 std::holds_alternative 结合,用于 topic 路由。
  • 异步处理:结合线程池(你之前提到过优先级队列),variant 消息就是任务参数。

更多推荐