C++中如何用std::variant和std::visit实现访问者模式处理异构对象

来源:建站作者:深圳GEO公司头衔:草根站长
导读:本期聚焦于小伙伴创作的《C++中如何用std::variant和std::visit实现访问者模式处理异构对象》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《C++中如何用std::variant和std::visit实现访问者模式处理异构对象》有用,将其分享出去将是对创作者最好的鼓励。

在C++17标准之前,处理多种不同类型的异构对象往往需要通过继承体系配合虚函数实现访问者模式,这种方式需要定义统一的基类,所有异构类型都要继承该基类,耦合度较高且扩展不够灵活。C++17引入的std::variant和std::visit提供了更轻量的异构对象处理方案,不需要依赖继承体系就能实现访问者模式,大幅降低了代码复杂度。

C++中如何用std::variant和std::visit实现访问者模式处理异构对象

std::variant基础用法

std::variant是一个类型安全的联合体,它可以存储指定类型列表中的任意一种类型的值,同一时间只能存储其中一种类型的数据,并且会自动管理对象的生命周期。

首先看std::variant的基本定义和使用示例:

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

// 定义可以存储int、double、string三种类型的variant
using MyVariant = std::variant<int, double, std::string>;

int main() {
    // 初始化为int类型
    MyVariant v1 = 10;
    // 初始化为double类型
    MyVariant v2 = 3.14;
    // 初始化为string类型
    MyVariant v3 = "hello variant";

    // 查看当前存储的类型索引
    std::cout << "v1类型索引: " << v1.index() << std::endl; // 输出0,对应int的索引
    std::cout << "v2类型索引: " << v2.index() << std::endl; // 输出1,对应double的索引
    std::cout << "v3类型索引: " << v3.index() << std::endl; // 输出2,对应string的索引

    return 0;
}

std::variant的类型索引从0开始,按照定义时的类型顺序依次排列。如果需要获取variant中存储的具体值,可以使用<code>std::get</code>函数,如果获取的类型和当前存储的类型不匹配,会抛出<code>std::bad_variant_access</code>异常:

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

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

int main() {
    MyVariant v = 10;
    // 正确获取int类型值
    int int_val = std::get<int>(v);
    std::cout << "int值: " << int_val << std::endl;

    try {
        // 尝试获取double类型,当前存储的是int,会抛异常
        double double_val = std::get<double>(v);
    } catch (const std::bad_variant_access& e) {
        std::cout << "获取失败: " << e.what() << std::endl;
    }

    return 0;
}

std::visit的工作原理

std::visit是专门用于访问std::variant存储值的工具,它接受一个可调用对象(访客)和一个或多个variant对象,会根据variant当前存储的类型自动调用可调用对象对应的重载版本,不需要手动判断类型。

std::visit的核心逻辑是编译期类型分发,可调用对象需要为variant所有可能的类型都提供对应的重载,这样编译器就能在编译阶段确定不同variant类型对应的处理逻辑,避免运行时的类型判断开销。

结合std::variant和std::visit实现访问者模式

传统的访问者模式需要定义抽象基类,所有被访问的元素都要继承该基类并实现accept方法,新增访问操作时还需要修改基类接口,扩展性较差。使用std::variant和std::visit实现访问者模式则不需要这些依赖,只需要定义好variant的类型列表和对应的访客可调用对象即可。

步骤1:定义异构类型列表

首先将所有需要处理的异构类型统一放到std::variant的类型列表中:

#include <variant>
#include <string>

// 定义三种不同的异构类型
struct Circle {
    double radius;
};

struct Rectangle {
    double width;
    double height;
};

struct Triangle {
    double base;
    double height;
};

// 定义variant类型,包含所有异构类型
using Shape = std::variant<Circle, Rectangle, Triangle>;

步骤2:定义访客可调用对象

访客可以是一个重载了调用运算符的结构体,也可以是lambda表达式的组合,只要能为variant的所有可能类型提供对应的处理逻辑即可。这里以计算不同形状的面积为例:

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

struct Circle {
    double radius;
};

struct Rectangle {
    double width;
    double height;
};

struct Triangle {
    double base;
    double height;
};

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

// 定义访客结构体,重载operator()处理所有variant可能的类型
struct AreaCalculator {
    double operator()(const Circle& c) const {
        return 3.14159 * c.radius * c.radius;
    }
    double operator()(const Rectangle& r) const {
        return r.width * r.height;
    }
    double operator()(const Triangle& t) const {
        return 0.5 * t.base * t.height;
    }
};

步骤3:使用std::visit调用访客

定义好variant和访客之后,就可以用std::visit将二者结合起来,自动根据variant存储的类型调用对应的访客逻辑:

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

struct Circle {
    double radius;
};

struct Rectangle {
    double width;
    double height;
};

struct Triangle {
    double base;
    double height;
};

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

struct AreaCalculator {
    double operator()(const Circle& c) const {
        return 3.14159 * c.radius * c.radius;
    }
    double operator()(const Rectangle& r) const {
        return r.width * r.height;
    }
    double operator()(const Triangle& t) const {
        return 0.5 * t.base * t.height;
    }
};

int main() {
    // 存储多个异构形状对象
    std::vector<Shape> shapes = {
        Circle{2.0},
        Rectangle{3.0, 4.0},
        Triangle{5.0, 6.0}
    };

    AreaCalculator calculator;
    for (const auto& shape : shapes) {
        // 调用std::visit,自动匹配对应的处理逻辑
        double area = std::visit(calculator, shape);
        std::cout << "形状面积: " << area << std::endl;
    }

    return 0;
}

如果需要新增一种形状类型,只需要在Shape的variant定义中新增类型,然后在AreaCalculator中新增对应的重载即可,不需要修改已有的类型定义和调用逻辑,扩展性远优于传统访问者模式。

使用lambda表达式作为访客

除了定义结构体作为访客,也可以使用重载的lambda表达式组合作为访客,适合逻辑比较简单的场景:

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

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

int main() {
    MyVariant v = "test";

    // 使用lambda组合作为访客
    std::visit([](auto&& val) {
        using T = std::decay_t<decltype(val)>;
        if constexpr (std::is_same_v<T, int>) {
            std::cout << "int值: " << val << std::endl;
        } else if constexpr (std::is_same_v<T, double>) {
            std::cout << "double值: " << val << std::endl;
        } else if constexpr (std::is_same_v<T, std::string>) {
            std::cout << "string值: " << val << std::endl;
        }
    }, v);

    return 0;
}

这种方式利用了C++17的if constexpr特性,在编译期判断类型并执行对应的逻辑,同样能实现类型自动分发的效果。

两种实现方式对比

下表对比了传统访问者模式和std::variant+std::visit实现方式的差异:

对比维度传统访问者模式std::variant+std::visit
耦合度高,需要继承统一基类低,无需继承关系
扩展性新增访问操作需要修改基类接口新增类型只需修改variant定义和访客重载
类型安全依赖虚函数,运行期类型判断编译期类型检查,更安全
代码复杂度需要定义多个类和接口,代码量大代码简洁,逻辑集中

注意事项

  • std::variant默认不能存储引用类型、数组类型和void类型,如果需要存储这些类型需要做额外处理。
  • 访客必须为std::variant的所有可能类型都提供对应的处理逻辑,否则编译会报错。
  • 如果variant可能存储的类型较多,访客的重载也会相应变多,需要合理组织代码结构避免冗余。
  • std::variant是C++17引入的特性,使用时需要确保编译器支持C++17及以上标准。

std::variantstd::visit访问者模式异构对象C++修改时间:2026-07-07 03:21:43

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。