【Example】C++ 标准库 std::variant (类型安全的联合体)

Warning:

这是一篇试验性质文章,基于我以前的文章风格与相关资料由 LLM 生成,旨意是用作【快速入门】及 Copilot 等工具【辅助开发的 RAG 资料】。


 

 

在 C++ 的编程实践中,我们有时会遇到这样一种需求:一个变量在不同时刻可能需要存储不同类型的值。传统的 C 语言 union 可以实现这一点,但它缺乏类型安全,开发者需要自行追踪当前存储的究竟是哪种类型,稍有不慎就可能导致未定义行为。为了解决这个问题,C++17 标准库引入了 std::variant。你可以把它想象成一个“魔法盒子”,这个盒子在任何时刻只能精确地装着你预先定义好的一系列类型中的某一种,并且它自身清楚地知道里面装的是什么。

 

类模板 std::variant 表示一个类型安全的联合体。 std::variant 的一个实例在任意时刻要么保有其一个可选类型之一的值,要么在错误情况下无值(此状态难以达成,见 valueless_by_exception)。

----- C++ Reference

 

而这个盒子的大小由它可能要装的最大的那个东西决定。当你往里面放东西时,东西就直接放在盒子里,盒子本身不会为了这个东西再去动态申请额外的空间。而且,这个盒子对能放什么东西有一些规定(不能是引用、C 数组、void),也不能是“空盒子”(除非你用 std::monostate 特指一个空状态)。

 

头文件:

#include <variant>

 

构造 std::variant

std::variant 提供了多种构造方式,让初始化过程既灵活又安全。

 

1. 默认构造函数

默认构造的 variant 会保有其第一个模板参数类型的值,并进行值初始化。前提是第一个类型必须是可默认构造的。

// 默认构造,v1 将包含一个默认构造的 int (值为 0)
std::variant<int, float, std::string> v1;

// 如果第一个类型不能默认构造,编译会失败
// std::variant<MyNonDefaultConstructible, int> v_fail;

如果希望一个 variant 的第一个类型不可默认构造,但仍希望 variant 本身可以默认构造,可以使用 std::monostate 作为第一个类型:

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

struct MyNonDefaultConstructible {
    MyNonDefaultConstructible(int) {}
};

int main() {
    // 使用 std::monostate 使其可默认构造
    // v_mono 将处于持有 std::monostate 的状态
    std::variant<std::monostate, MyNonDefaultConstructible, std::string> v_mono;
    if (std::holds_alternative<std::monostate>(v_mono)) {
        std::cout << "v_mono holds monostate" << std::endl;
    }
    return 0;
}

 

2. 拷贝与移动构造函数

std::variant 支持标准的拷贝和移动构造。

std::variant<int, std::string> v_orig = "Hello";
std::variant<int, std::string> v_copy = v_orig; // 拷贝构造
std::variant<int, std::string> v_move = std::move(v_orig); // 移动构造

这要求所有备选类型都是可拷贝/移动构造的。

 

3. 转换构造函数 (直接用值初始化)

这是最常见的初始化方式之一。你可以直接用备选类型之一的值来初始化 variant。编译器会根据传入值的类型选择合适的备选类型。

std::variant<int, float, std::string> v_int = 10;          // v_int 持有 int
std::variant<int, float, std::string> v_float = 3.14f;     // v_float 持有 float
std::variant<int, float, std::string> v_str = "World";     // v_str 持有 std::string

// 注意歧义:
// std::variant<long, int> v_amb = 0; // 编译错误,0 可以转换为 long 或 int
std::variant<long, int> v_long = 0L; // 明确指定为 long
std::variant<const char*, std::string> v_char_ptr = "Raw C-string"; // 选择 const char*

转换构造函数在选择备选类型时有一套明确的规则,通常会选择“最佳匹配”。如果存在歧义,编译会失败。

 

4. std::in_place_type_t<T>  std::in_place_index_t<I> 精确构造

当你需要精确指定 variant 初始化为哪个类型,或者需要传递参数给备选类型的构造函数时,in_place_type  in_place_index 就派上用场了。

  • std::in_place_type_t<T>: 指定要构造的类型 T
  • std::in_place_index_t<I>: 指定要构造的类型的索引 I (从0开始)。
#include <variant>
#include <string>
#include <iostream>

struct Point {
    int x, y;
    Point(int x_val, int y_val) : x(x_val), y(y_val) {
        std::cout << "Point(" << x << ", " << y << ") constructed" << std::endl;
    }
};

int main() {
    // 使用 std::in_place_type_t
    std::variant<int, std::string, Point> v_point(std::in_place_type_t<Point>{}, 5, 10);
    // 或者简写 (C++17 class template argument deduction):
    // std::variant v_point(std::in_place_type<Point>, 5, 10);


    // 使用 std::in_place_index_t
    std::variant<int, std::string, Point> v_str_idx(std::in_place_index_t<1>{}, "Hello from index");
    // 或者简写:
    // std::variant v_str_idx(std::in_place_index<1>, "Hello from index");

    if (std::holds_alternative<Point>(v_point)) {
        std::cout << "v_point holds Point" << std::endl;
    }
    if (std::holds_alternative<std::string>(v_str_idx)) {
        std::cout << "v_str_idx holds: " << std::get<std::string>(v_str_idx) << std::endl;
    }

    return 0;
}

这些构造方式消除了转换构造可能带来的歧义,并允许直接构造非拷贝/移动的类型。

 

 

成员函数

名称 说明
(constructor) 构造 variant 对象 (如上所述)
(destructor) 销毁 variant 及其所含的值
operator= 赋值给 variant
index() 返回 variant 所保有可选项的零基下标。若因异常无值,则返回 variant_npos
valueless_by_exception() 检查 variant 是否因异常而处于无值状态。
emplace<T>(args...) 原位构造类型为 T 的值。T 必须是备选类型之一且唯一。
emplace<I>(args...) 原位构造索引为 I 的备选类型的值。
swap(other) 与另一个 variant 交换内容。

 

 

非成员函数 (访问与操作 std::variant)

这些函数是与 std::variant 交互的核心。

名称 说明
std::holds_alternative<T>(v) 检查 variant v 是否当前持有类型 T
std::get<I>(v) 返回 variant v 中索引为 I 的值的引用。如果 v.index() != I,抛出 std::bad_variant_access
std::get<T>(v) 返回 variant v 中类型为 T 的值的引用。如果 v 不持有 T,抛出 std::bad_variant_accessT 必须唯一。
std::get_if<I>(&v) 如果 v->index() == I,返回指向 v 中索引为 I 的值的指针;否则返回 nullptr。不抛异常。
std::get_if<T>(&v) 如果 v 持有类型 T,返回指向 v 中类型为 T 的值的指针;否则返回 nullptrT 必须唯一。不抛异常。
std::visit(visitor, variants...) 对一个或多个 variant 中当前持有的值调用 visitor
std::swap(v1, v2) 特化 std::swap 算法,交换两个 variant 的内容。

 

 

辅助类与常量

名称 说明
std::monostate 用作非可默认构造类型的 variant 的首个可选项的占位符类型,或表示空状态。
std::bad_variant_access 非法地访问 variant 的值时(如 std::get 类型不匹配)抛出的异常。
std::variant_npos variant 处于 valueless_by_exception 状态时的索引值。
std::variant_size<VariantType> 编译时获取 variant 可选项的数量。
std::variant_size_v<VariantType> variant_size 的变量模板版本 (C++17)。
std::variant_alternative<I, VariantType> 编译时获取 variant 中索引为 I 的可选项的类型。
std::variant_alternative_t<I, VariantType> variant_alternative 的别名模板版本 (C++17)。
std::hash<std::variant<Types...>> std::hash 的特化,允许 std::variant 作为无序关联容器的键。

 

 

安全获取 std::variant 中的值

获取 std::variant 中的值时,类型安全是首要考虑的。

1. 使用 std::holds_alternative  std::get (需要注意异常)

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

int main() {
    std::variant<int, std::string> var = "Hello";

    if (std::holds_alternative<std::string>(var)) {
        std::cout << "Holds string: " << std::get<std::string>(var) << std::endl;
    } else if (std::holds_alternative<int>(var)) {
        std::cout << "Holds int: " << std::get<int>(var) << std::endl;
    }

    var = 123;
    try {
        // 尝试获取错误的类型
        std::cout << "Trying to get string: " << std::get<std::string>(var) << std::endl;
    } catch (const std::bad_variant_access& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
        if (std::holds_alternative<int>(var)) {
             std::cout << "Actually holds int: " << std::get<int>(var) << std::endl;
        }
    }
    return 0;
}

虽然可行,但 std::get 在类型不匹配时会抛出异常,这可能不是最高效或最优雅的方式。

 

2. 使用 std::get_if (推荐的安全方式)

std::get_if 不会抛出异常,而是返回一个指针。如果类型匹配,返回指向值的指针;否则返回 nullptr

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

int main() {
    std::variant<int, std::string> var;
    var = "World";

    // 尝试获取 string
    if (auto* str_ptr = std::get_if<std::string>(&var)) {
        std::cout << "Holds string: " << *str_ptr << std::endl;
        *str_ptr += "!"; // 可以修改值
    } else {
        std::cout << "Does not hold string." << std::endl;
    }
    std::cout << "Current string value: " << std::get<std::string>(var) << std::endl;


    var = 42;
    // 尝试获取 int (使用索引)
    if (auto* int_ptr = std::get_if<0>(&var)) { // 0 是 int 的索引
        std::cout << "Holds int: " << *int_ptr << std::endl;
    } else {
        std::cout << "Does not hold int." << std::endl;
    }
    
    // 尝试获取 string (此时var持有int)
    if (auto* str_ptr_again = std::get_if<std::string>(&var)) {
         std::cout << "This won't be printed: " << *str_ptr_again << std::endl;
    } else {
        std::cout << "Still does not hold string, as expected." << std::endl;
    }

    return 0;
}

std::get_if 是进行条件访问的首选方法。

 

3. 使用 std::visit (最通用和强大的方式)

std::visit 接受一个可调用对象 (Visitor) 和一个或多个 variant 对象。它会根据 variant(或多个 variant 组合) 当前持有的类型,以类型安全的方式调用 Visitor。

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

// 辅助结构体,用于创建重载的 lambda 集合 (C++17 Overload Pattern)
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>; // 推导指引 (C++20 起可选)

int main() {
    std::variant<int, double, std::string> my_variant;
    my_variant = "Hello Variant!";

    // 使用 lambda 作为 visitor
    std::visit([](const auto& value) {
        // auto& 推断出 variant 中实际存储的类型
        using T = std::decay_t<decltype(value)>;
        if constexpr (std::is_same_v<T, int>) {
            std::cout << "It's an int: " << value << std::endl;
        } else if constexpr (std::is_same_v<T, double>) {
            std::cout << "It's a double: " << value << std::endl;
        } else if constexpr (std::is_same_v<T, std::string>) {
            std::cout << "It's a string: " << value << std::endl;
        }
    }, my_variant);

    my_variant = 100;

    // 使用重载的 lambda 集合
    std::visit(overloaded{
        [](int i) { std::cout << "Overloaded: int " << i << std::endl; },
        [](double d) { std::cout << "Overloaded: double " << d << std::endl; },
        [](const std::string& s) { std::cout << "Overloaded: string " << s << std::endl; }
    }, my_variant);

    // visit 也可以返回值
    std::string type_name = std::visit(overloaded{
        [](int) -> std::string { return "int"; },
        [](double) -> std::string { return "double"; },
        [](const std::string&) -> std::string { return "std::string"; }
    }, my_variant);
    std::cout << "Variant currently holds type: " << type_name << std::endl;


    // 处理 valueless_by_exception (虽然很难主动触发,但可以了解)
    // 假设 var_valueless 处于此状态
    // std::visit(visitor, var_valueless); // 会抛出 std::bad_variant_access

    return 0;
}

std::visit 非常强大,尤其是在需要对 variant 中所有可能的类型执行不同操作时。配合 if constexpr 或重载的函数对象/lambda,可以写出非常清晰和类型安全的代码。

 

 

valueless_by_exception 状态

在极少数情况下(通常是赋值或 emplace 过程中,新值的构造函数抛出异常,且旧值的析构和新值的构造不是原子操作时),std::variant 可能会进入一个“因异常而无值”的状态。此时,valueless_by_exception() 返回 trueindex() 返回 std::variant_npos。任何尝试通过 std::get  std::visit 访问其值的行为都会抛出 std::bad_variant_access
这种状态比较罕见,因为 std::variant 的操作通常被设计为具有强异常安全保证。

 

 

最简单示例

#include <iostream>
#include <variant>
#include <string>
#include <vector> // For a more complex visitor if needed

// Helper for visit (overload pattern)
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

struct Circle { double radius; };
struct Square { double side; };
struct Triangle { double base, height; };

using Shape = std::variant<Circle, Square, Triangle, std::monostate>;

void print_shape_info(const Shape& shape) {
    std::visit(overloaded{
        [](const Circle& c) { std::cout << "Circle with radius: " << c.radius << std::endl; },
        [](const Square& s) { std::cout << "Square with side: " << s.side << std::endl; },
        [](const Triangle& t) { std::cout << "Triangle with base " << t.base << " and height " << t.height << std::endl; },
        [](std::monostate) { std::cout << "Shape is empty (monostate)." << std::endl; }
    }, shape);
}

int main() {
    Shape shape1 = Circle{5.0};
    Shape shape2 = Square{3.0};
    Shape shape3; // Defaults to std::monostate if it's the first type, or if Circle was not default constructible
                 // Here, if Circle was first and not default constructible, this would be an error
                 // unless monostate was first. Let's assume monostate is first for default construction.
    
    // To be explicit for default construction with monostate as an option:
    Shape shape_empty(std::in_place_type_t<std::monostate>{});


    print_shape_info(shape1);
    print_shape_info(shape2);
    print_shape_info(shape_empty);

    // Modifying a shape
    shape1 = Triangle{4.0, 6.0};
    print_shape_info(shape1);

    // Safe access using get_if
    if (auto* p_circle = std::get_if<Circle>(&shape2)) { // shape2 is Square
        std::cout << "This will not print, shape2 is not a Circle." << std::endl;
    } else if (auto* p_square = std::get_if<Square>(&shape2)) {
        std::cout << "Safely got Square, side: " << p_square->side << std::endl;
        p_square->side = 3.5; // Modify it
    }
    print_shape_info(shape2); // See modified value

    // Using index
    std::cout << "Index of current type in shape1: " << shape1.index(); // Triangle is likely index 2
    if (shape1.index() == std::variant_npos) {
        std::cout << " (valueless by exception!)" << std::endl;
    } else {
        std::cout << std::endl;
    }


    return EXIT_SUCCESS;
}

 

何时使用 std::variant

  • 当你需要在编译时确定一个对象可以存储的有限几种类型之一时。
  • 当你需要比 union 更类型安全的选择时。
  •  std::any 不同,std::any 可以存储任何类型(在运行时确定),而 std::variant 的类型集在编译时固定。这使得 std::variant 通常更快,因为不需要类型擦除的开销。
  •  std::optional 不同,std::optional<T> 表示一个 T 类型的值可能存在也可能不存在,而 std::variant 总是持有其备选类型之一的值(除非是 valueless_by_exception 状态,或者你用 std::monostate 来显式表示“空”状态)。

std::variant 是现代 C++ 中处理和类型值的一个强大工具,它通过编译时检查和丰富的 API 提供了优秀的类型安全性和表达能力。掌握它的使用,能让你的代码更加健壮和易于维护。

 

 

版权声明:
作者:芯片烤电池
链接:https://www.airchip.org.cn/index.php/2025/05/18/cpp-example-variant/
来源:芯片烤电池
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
海报
【Example】C++ 标准库 std::variant (类型安全的联合体)
Warning: 这是一篇试验性质文章,基于我以前的文章风格与相关资料由 LLM 生成,旨意是用作【快速入门】及 Copilot 等工具【辅助开发的 ……
<<上一篇
下一篇>>