在C++17之前,使用多态往往依赖虚函数和继承层次结构,但这会导致动态分派、运行时开销以及类层次不易维护的问题。C++17引入了std::variant,它提供了一个类型安全的“联合体”,可以在编译期保证每个值只属于预定义的几种类型之一。下面将通过一个简单的示例,演示如何使用std::variant来实现类似多态的行为,并且在保持类型安全的前提下避免虚函数调用。
1. 需求场景
假设我们正在编写一个日志系统,日志条目可以是:
- 普通文本日志(
std::string) - 错误码日志(
int) - 结构化日志(
std::map<std::string, std::string>)
我们需要一个统一的接口来处理这些不同类型的日志,而不想为每个类型写单独的处理函数。
2. 传统实现(使用继承)
class LogEntry {
public:
virtual ~LogEntry() = default;
virtual void process() const = 0;
};
class TextLog : public LogEntry {
std::string msg;
public:
TextLog(std::string m) : msg(std::move(m)) {}
void process() const override { std::cout << "Text: " << msg << '\n'; }
};
class ErrorLog : public LogEntry {
int code;
public:
ErrorLog(int c) : code(c) {}
void process() const override { std::cout << "Error code: " << code << '\n'; }
};
class StructuredLog : public LogEntry {
std::map<std::string, std::string> data;
public:
StructuredLog(std::map<std::string, std::string> d) : data(std::move(d)) {}
void process() const override {
std::cout << "Structured:\n";
for (auto &p : data) std::cout << " " << p.first << ": " << p.second << '\n';
}
};
每种日志类型都需要继承LogEntry并实现process,这导致代码膨胀,且每次添加新类型都要修改基类。
3. 使用std::variant实现
#include <iostream>
#include <string>
#include <map>
#include <variant>
#include <vector>
using LogData = std::map<std::string, std::string>;
using LogEntry = std::variant<std::string, int, LogData>;
3.1 定义处理函数
利用std::visit对variant进行访问。我们可以提供一个通用的处理函数,内部根据实际类型执行相应逻辑。
void processLog(const LogEntry& entry) {
std::visit([](auto&& arg){
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, std::string>) {
std::cout << "Text: " << arg << '\n';
} else if constexpr (std::is_same_v<T, int>) {
std::cout << "Error code: " << arg << '\n';
} else if constexpr (std::is_same_v<T, LogData>) {
std::cout << "Structured:\n";
for (const auto& [k, v] : arg) {
std::cout << " " << k << ": " << v << '\n';
}
}
}, entry);
}
3.2 使用示例
int main() {
std::vector <LogEntry> logs;
logs.emplace_back(std::string("系统启动"));
logs.emplace_back(404);
logs.emplace_back(LogData{{"key1","value1"},{"key2","value2"}});
for (const auto& log : logs) {
processLog(log);
}
}
运行结果:
Text: 系统启动
Error code: 404
Structured:
key1: value1
key2: value2
4. 优点总结
| 传统方式 | std::variant 方式 |
|---|---|
| 需要继承层次 | 仅用一个variant类型 |
| 运行时多态 | 编译时类型检查 |
| 维护成本高 | 代码简洁、易维护 |
| 可能存在空指针或虚函数表误用 | 安全、无运行时分派开销 |
| 添加新类型需改基类 | 只需在variant声明中加入新类型即可 |
5. 进阶:自定义访问器
如果处理逻辑较为复杂,可以自定义一个访问器结构体:
struct LogProcessor {
void operator()(const std::string& msg) const { /*...*/ }
void operator()(int code) const { /*...*/ }
void operator()(const LogData& data) const { /*...*/ }
};
然后:
std::visit(LogProcessor{}, entry);
这样可以将处理逻辑拆分到不同成员函数中,进一步提升可读性。
6. 结语
std::variant让我们在保持类型安全的前提下,摆脱传统多态带来的继承耦合和运行时开销。对于日志、事件系统、配置项等需要容纳多种类型值的场景,std::variant提供了一种简洁、可维护的实现方案。希望这篇文章能帮助你在项目中更好地运用C++17的新特性。