使用std::variant实现非虚运行时多态实现


概述

C++中常用的多态方案是基于虚函数的运行时多态方案,但是在一些要求高性能场景(或者有些强迫症),性能并不怎么好,因为涉及到查虚表跳转,寻址,还有一些更深层次的原因。性能表现不怎么好函。C++17提出了另一种运行时多态方案,基于std::variant的方案。

std::variant

本质上是通过std::variant提供了一种类型安全的联合容器,可以存储不同类型的值,类似于 C 语言中的联合体(union),但具有更强的类型安全性和语法支持。std::variant 的特点和用法如下:

  1. 存储多种类型的值:std::variant 可以存储一组预定义的类型,这些类型可以是基本类型(例如整数、浮点数)、自定义的类类型、指针等。
  2. 类型安全:在编译时,std::variant 会对可能存储的值进行类型检查,不允许存储超出预定义类型范围的值。这提供了更好的类型安全性,避免了潜在的类型错误。
  3. 轻量高效:std::variant 的内存占用只有存储值的最大成员所需的内存大小,没有额外的空间开销。同时,由于类型是静态确定的,不需要运行时的类型信息,因此执行效率较高。
  4. 支持访问和操作:可以使用 std::get 函数或 std::visit 函数来访问和操作 std::variant 中的值。std::get 可以直接获取特定类型的值,而 std::visit 可以通过访问器(visitor)函数来执行对应类型的操作。
  5. 支持默认构造和拷贝:std::variant 支持默认构造、拷贝构造和赋值操作,以及其他与值类型相关的操作,如移动构造和析构函数等。

我们可以使用std::get来从std::variant中提取出真实的对象,前提是类型是匹配的(否则抛出异常)。std::get_ifstd::get功能类似,区别是std::get_if接收指针并且返回指针(类型不匹配返回std::nullptr)。

std::visit

std::visit 是 C++17 引入的标准库函数,用于访问 std::variant 中存储的值。它提供了一种统一的方式来处理 std::variant 的不同类型成员,允许我们根据成员类型执行相应的操作,std::visit 的基本语法:

std::visit(visitor, variant);

其中,visitor 是一个可调用对象,可以是函数指针、函数对象、Lambda 表达式等,用于指定不同类型的访问操作。variant 是要访问的 std::variant 对象。std::visit 的工作原理如下:

  1. 首先,std::visit 会根据传入的 variant 对象,确定当前存储的成员类型。
  2. 然后,它会根据成员类型,选择合适的访问操作,即调用与当前成员类型相匹配的访问器。
  3. 最后,它会将当前成员的值作为参数传递给访问器,并执行相应的操作。

实战

基础实现

我们的最终目标是声明和实现的分离,同时要能够做到同步(保证修改支持的多态种类时能够少修改源文件),具有的头文件有:

void show(const std::variant<std::string, int, double> &v);

在源文件中我们使用对应的visit就能够快捷访问了:

void show(const std::variant<std::string, int, double> &v) {
    std::visit(
        [](auto &&t) {
            using T = std::decay_t<decltype(t)>;
            if constexpr (std::is_same_v<int, T>) {
                std::cout << "int: " << t << std::endl;
            } else if constexpr (std::is_same_v<double, T>) {
                std::cout << "double: " << t << std::endl;
            } else if constexpr (std::is_same_v<std::string, T>) {
                std::cout << "string: " << t << std::endl;
            }
        },
        v);
    /** equal to the above code
    auto visitor = [](auto arg) {
        std::cout << arg << std::endl;
    };
    if (std::holds_alternative<int>(v)) {
        visitor(std::get<int>(v));
    } else if (std::holds_alternative<double>(v)) {
        visitor(std::get<double>(v));
    } else if (std::holds_alternative<std::string>(v)) {
        visitor(std::get<std::string>(v));
    }
    **/
}

进阶实现

可以发现基础实现的编码是比较死的,其次当我们将std::variant里的参数修改时,会发现提示参数不匹配,这个问题其实是很严重的,能不能像之前利用宏函数转化enum一样实现呢?

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

void add_object(Object o);
void print_objects();

首先使用类型别名简化我们的输入,重点在于实现上,利用模板元编程来展开进行循环:

template<class V>
struct variant_to_tuple_of_vector;

template<class... Ts>
struct variant_to_tuple_of_vector<std::variant<Ts...>> {
    using type = std::tuple<std::vector<Ts>...>;
};

template<size_t N, class Lambda>
auto static_for(Lambda &&lambda) {
    return [&]<size_t... Is>(std::index_sequence<Is...>) {
        return (lambda(std::integral_constant<size_t, Is>{}), ...);
    }(std::make_index_sequence<N>{});
}

template<size_t N, class Lambda>
auto static_for_break_if_false(Lambda &&lambda) {
    return [&]<size_t... Is>(std::index_sequence<Is...>) {
        return (lambda(std::integral_constant<size_t, Is>{}) && ...);
    }(std::make_index_sequence<N>{});
}

template<size_t N, class Lambda>
auto static_for_break_if_true(Lambda &&lambda) {
    return [&]<size_t... Is>(std::index_sequence<Is...>) {
        return (lambda(std::integral_constant<size_t, Is>{}) || ...);
    }(std::make_index_sequence<N>{});
}

static variant_to_tuple_of_vector<Object>::type objects; // 帮助自动识别

逻辑其实不复杂,主要是语法上需要理解,利用静态for展开编写我们需要的函数即可。

void add_object(Object o) {
    static_for_break_if_false<std::variant_size_v<Object>>([&](auto ic) {
        if (o.index() == ic) {
            get<ic>(objects).push_back(get<ic>(o));
            return false;
        }
        return true;
    });
}

void print_objects() {
    static_for<std::variant_size_v<Object>>([&](auto ic) {
        for (auto const &o : get<ic>(objects)) { std::cout << o << std::endl; }
    });
}

跟虚函数运行时多态有哪些优缺点

使用 std::variant 实现动态多态的优点:

  1. 静态类型检查:std::variant 在编译时就确定了可存储的类型,可以在编译阶段进行静态类型检查,减少运行时错误。
  2. 不需要动态内存分配:std::variant 存储对象时,不需要进行动态内存分配,对象直接存储在 std::variant 容器中,避免了频繁的内存分配和释放。
  3. 无需维护虚函数表:std::variant 不需要维护虚函数表,减少了内存开销。
  4. 更高的性能:由于静态类型检查和减少了虚函数调用的开销,std::variant 在某些情况下可以获得更高的性能。

使用虚函数实现动态多态的优点:

  1. 灵活性:虚函数机制允许在运行时动态绑定函数调用,根据对象的实际类型决定调用哪个函数,提供了更大的灵活性。
  2. 扩展性:可以通过派生类来扩展已有的基类,实现更多的功能,同时仍然保持多态性。
  3. 容易理解和维护:使用虚函数可以直观地表示对象之间的继承关系和多态特性,使代码更易理解和维护。

如果需要在编译时进行类型检查和消除动态内存分配的开销或者需要更高的性能,可以考虑使用 std::variant。如果需要更大的灵活性和扩展性,以及表达对象继承关系的需求,使用虚函数是更好的选择。


文章作者: JoyTsing
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 JoyTsing !
评论
  目录