概述
C++中常用的多态方案是基于虚函数的运行时多态方案,但是在一些要求高性能场景(或者有些强迫症),性能并不怎么好,因为涉及到查虚表跳转,寻址,还有一些更深层次的原因。性能表现不怎么好函。C++17提出了另一种运行时多态方案,基于std::variant
的方案。
std::variant
本质上是通过std::variant
提供了一种类型安全的联合容器,可以存储不同类型的值,类似于 C 语言中的联合体(union),但具有更强的类型安全性和语法支持。std::variant
的特点和用法如下:
- 存储多种类型的值:
std::variant
可以存储一组预定义的类型,这些类型可以是基本类型(例如整数、浮点数)、自定义的类类型、指针等。 - 类型安全:在编译时,
std::variant
会对可能存储的值进行类型检查,不允许存储超出预定义类型范围的值。这提供了更好的类型安全性,避免了潜在的类型错误。 - 轻量高效:
std::variant
的内存占用只有存储值的最大成员所需的内存大小,没有额外的空间开销。同时,由于类型是静态确定的,不需要运行时的类型信息,因此执行效率较高。 - 支持访问和操作:可以使用
std::get
函数或std::visit
函数来访问和操作std::variant
中的值。std::get
可以直接获取特定类型的值,而std::visit
可以通过访问器(visitor)函数来执行对应类型的操作。 - 支持默认构造和拷贝:
std::variant
支持默认构造、拷贝构造和赋值操作,以及其他与值类型相关的操作,如移动构造和析构函数等。
我们可以使用std::get
来从std::variant
中提取出真实的对象,前提是类型是匹配的(否则抛出异常)。std::get_if
与std::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
的工作原理如下:
- 首先,
std::visit
会根据传入的variant
对象,确定当前存储的成员类型。 - 然后,它会根据成员类型,选择合适的访问操作,即调用与当前成员类型相匹配的访问器。
- 最后,它会将当前成员的值作为参数传递给访问器,并执行相应的操作。
实战
基础实现
我们的最终目标是声明和实现的分离,同时要能够做到同步(保证修改支持的多态种类时能够少修改源文件),具有的头文件有:
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
实现动态多态的优点:
- 静态类型检查:
std::variant
在编译时就确定了可存储的类型,可以在编译阶段进行静态类型检查,减少运行时错误。 - 不需要动态内存分配:
std::variant
存储对象时,不需要进行动态内存分配,对象直接存储在std::variant
容器中,避免了频繁的内存分配和释放。 - 无需维护虚函数表:
std::variant
不需要维护虚函数表,减少了内存开销。 - 更高的性能:由于静态类型检查和减少了虚函数调用的开销,
std::variant
在某些情况下可以获得更高的性能。
使用虚函数实现动态多态的优点:
- 灵活性:虚函数机制允许在运行时动态绑定函数调用,根据对象的实际类型决定调用哪个函数,提供了更大的灵活性。
- 扩展性:可以通过派生类来扩展已有的基类,实现更多的功能,同时仍然保持多态性。
- 容易理解和维护:使用虚函数可以直观地表示对象之间的继承关系和多态特性,使代码更易理解和维护。
如果需要在编译时进行类型检查和消除动态内存分配的开销或者需要更高的性能,可以考虑使用 std::variant
。如果需要更大的灵活性和扩展性,以及表达对象继承关系的需求,使用虚函数是更好的选择。