C++17 std::invoke std::variant
当然可以!std::invoke 和 std::variant 是 C++17 中两个非常实用但用途迥异的标准库组件:
std::invoke:统一调用机制,解决“如何以一致方式调用各种可调用对象”的问题。
std::variant:类型安全的联合体(tagged union),用于在多个类型中安全地存储一个值。
下面分别对二者进行深入使用总结,包含核心原理、典型场景、代码示例与最佳实践。
一、std::invoke —— 统一调用一切可调用对象
✅ 核心作用
提供一个通用接口,可以统一调用以下任意形式的“可调用物”:
-
普通函数
-
函数指针
-
Lambda 表达式
-
函数对象(Functor)
- 成员函数指针
- 成员变量指针
💡 在 C++17 之前,要写一个能同时调用
f()和obj.mem_fn()的泛型代码非常麻烦。std::invoke解决了这个问题。
🔧 基本语法
#include <functional>
std::invoke(f, args...);
📌 典型使用场景与示例
1. 调用普通函数 / Lambda / Functor
void foo(int x) { std::cout << x; }
auto lambda = [](int x) { return x * 2; };
struct Adder {
int operator()(int a, int b) { return a + b; }
};
std::invoke(foo, 42); // 调用 foo(42)
std::invoke(lambda, 5); // 返回 10
std::invoke(Adder{}, 3, 4); // 返回 7
2. 调用成员函数(关键优势!)
struct Person {
void greet() const { std::cout << "Hello\n"; }
int age = 25;
};
Person p;
// 传统方式: (p.*&Person::greet)()
// 使用 invoke:
std::invoke(&Person::greet, p); // 对象
std::invoke(&Person::greet, &p); // 指针(自动解引用)
// 带参数的成员函数
struct Math {
int add(int a, int b) { return a + b; }
};
Math m;
int result = std::invoke(&Math::add, m, 10, 20); // 30
3. 访问成员变量(常被忽略但很有用)
std::invoke(&Person::age, p); // 返回 25(等价于 p.age)
std::invoke(&Person::age, &p); // 同样返回 25
4. 在泛型代码中使用(如包装器、适配器)
template<typename Callable, typename... Args>
auto safe_invoke(Callable&& f, Args&&... args) {
try {
return std::invoke(std::forward<Callable>(f), std::forward<Args>(args)...);
} catch (...) {
std::cerr << "Call failed!\n";
throw;
}
}
5. 与 std::apply 配合解包 tuple 调用
auto f = [](int a, double b, std::string c) {
std::cout << a << ", " << b << ", " << c << "\n";
};
std::tuple<int, double, std::string> args{42, 3.14, "hello"};
std::apply(f, args); // 内部使用 invoke 实现
🔍 std::apply 的实现本质上是:
template<typename F, typename Tuple>
decltype(auto) apply(F&& f, Tuple&& t) {
return std::invoke(f, std::get<Is>(std::forward<Tuple>(t))...);
}
⚠️ 注意事项
std::invoke不处理异常,调用失败会直接抛出。
-
成员函数调用时,第一个参数必须是对象或指针,不能是引用(但指针会被自动解引用)。
-
性能:零开销抽象,编译后通常内联为直接调用。
二、std::variant —— 类型安全的联合体
✅ 核心作用
在有限的一组类型中,安全地存储其中一个类型的值。
替代传统的 union(无类型信息、不能有非平凡析构)和 void*(完全丧失类型安全)。
🛑
union无法自动调用析构函数,std::variant可以!
🔧 基本语法
#include <variant>
std::variant<T1, T2, ..., Tn> v;
📌 核心特性
|
特性 |
说明 |
|---|---|
| 类型安全 |
编译期知道可能类型,运行时知道当前类型 |
| 自动析构 |
切换值时自动析构旧对象 |
| 无堆分配 |
所有数据在栈上(大小 = max(sizeof(Ti)) + tag) |
| 支持访问者模式 |
通过 |
🧪 常见操作与示例
1. 构造与赋值
std::variant<int, std::string, double> v;
v = 42; // holds int
v = "hello"s; // holds std::string(注意用 "s" 避免 char*)
v = 3.14; // holds double
2. 检查当前类型
if (std::holds_alternative<int>(v)) {
std::cout << "It's an int: " << std::get<int>(v) << "\n";
}
3. 安全获取值(带异常)
try {
int x = std::get<int>(v); // 若不是 int,抛出 std::bad_variant_access
} catch (const std::bad_variant_access& e) {
// handle error
}
4. 使用 std::visit 安全访问(推荐方式!)
std::visit([](const auto& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "int: " << val << "\n";
} else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "string: " << val << "\n";
} else {
std::cout << "other: " << val << "\n";
}
}, v);
✅
std::visit是访问 variant 的首选方式,避免手动类型检查。
5. 自定义访问者(Visitor Pattern)
struct Printer {
void operator()(int i) const { std::cout << "int: " << i; }
void operator()(const std::string& s) const { std::cout << "str: " << s; }
void operator()(double d) const { std::cout << "dbl: " << d; }
};
std::visit(Printer{}, v);
6. 返回值的 visit(C++17 支持)
auto result = std::visit([](const auto& val) -> std::string {
return std::to_string(val);
}, v);
⚠️ 所有分支必须返回相同类型(或可隐式转换)。
🚫 常见陷阱与规避
|
陷阱 |
解决方案 |
|---|---|
| 空 variant | std::variant
永远不会为空(除非 |
| 异常导致 valueless |
析构/移动抛异常时进入 |
| 忘记处理所有类型 |
使用 |
| 性能误解 | sizeof(variant)
= 最大类型 + 标记(通常 1 字节),无指针开销 |
🏗️ 典型应用场景
- 配置解析
:配置项可能是 int / string / bool
- AST(抽象语法树)节点
:不同节点类型
- 状态机状态
:每个状态携带不同类型数据
- API 返回值
:成功返回数据,失败返回错误码(可结合
expected,C++23 引入)
// 示例:JSON 值类型
using JsonValue = std::variant<
std::monostate, // null
bool,
int64_t,
double,
std::string,
std::vector<JsonValue>,
std::map<std::string, JsonValue>
>;
三、std::invoke 与 std::variant 的结合使用
二者常配合实现动态分发:
std::variant<int, std::string> v = "hello";
// 假设我们有一个函数表
struct Handler {
void handle(int x) { std::cout << "Handling int: " << x; }
void handle(const std::string& s) { std::cout << "Handling str: " << s; }
};
Handler h;
//使用 visit + invoke 实现成员函数分发
std::visit([&h](const auto& val) {
std::invoke(&Handler::handle, h, val);
}, v);
这种模式在事件系统、命令分发、序列化中非常常见。
四、总结对比表
|
特性 |
std::invoke |
std::variant |
|---|---|---|
| 目的 |
统一调用可调用对象 |
安全存储多类型之一 |
| 头文件 | <functional> |
<variant> |
| 是否拥有数据 |
❌ |
✅ |
| 关键优势 |
简化泛型调用逻辑 |
替代 unsafe union |
| 典型用法 |
包装器、适配器、apply |
状态机、AST、配置 |
| 性能 |
零开销(内联) |
栈存储,无堆分配 |
五、最佳实践建议
✅ std::invoke
-
在泛型库(如 task system、event dispatcher)中使用。
-
优先于手写
(obj.*ptr)(args...)。 -
与
std::apply、完美转发结合使用。
✅ std::variant
- 永远优先使用
std::visit访问,而非
get<T>。 -
避免在 variant 中放入可能抛异常的移动操作类型。
-
若需要“可能无值”,考虑
std::optional<std::variant<...>>。 -
对于错误处理,未来可迁移到 C++23 的
std::expected。
如需进一步探讨:
-
如何实现
variant的递归访问(如 JSON 打印)? std::visit的多 variant 同时访问(C++17 支持)?
-
与
std::any的选型对比?
欢迎扫描关注,持续交流学习!!

更多推荐

所有评论(0)