tag_invoke
在看libunifex的代码,里面用了比较多的tag_invoke来连接receiver和sender,主要记一下tag_invoke的用法,太jb傻逼难读了。
1. 为什么需要它?(痛点)
在 C++ 中,如果你想让不同的类支持同一个操作(比如 start 一个异步任务),通常有两种做法:
- 做法 A:虚函数(多态)。
- 缺点:有虚函数表指针开销,不能在编译期优化(内联),且你无法给
std::vector这种现有的类添加虚函数。
- 缺点:有虚函数表指针开销,不能在编译期优化(内联),且你无法给
- 做法 B:普通的函数重载(ADL 机制)。
- 缺点:查找规则极其复杂,容易起冲突,报错信息难以理解。
tag_invoke 诞生了: 它要把所有这些操作统一到一个入口下。
2. tag_invoke 的直观理解
你可以把 tag_invoke 想象成一个中转站。
假设我们要实现一个 connect 操作:
- 定义一个标签(Tag):
struct connect_t {}; - 定义一个统一入口:不管你是谁,只要你想调用
connect,你就调用tag_invoke(connect_t{}, obj, ...)。 - 用户实现:具体的类只需要在自己的命名空间里实现一个
tag_invoke函数,第一个参数是connect_t。
3. 代码长什么样?
我们可以拆解成三个角色:
第一步:库的作者定义“操作”(定义插座)
1
2
3
4
5
6
7
8
9
10
11
// 在 unifex 库里
struct connect_t {
// 这是一个简化版的入口
template<typename S, typename R>
auto operator()(S&& s, R&& r) const {
// 它会去寻找匹配的 tag_invoke 函数
return tag_invoke(*this, (S&&)s, (R&&)r);
}
};
inline constexpr connect_t connect{}; // 定义全局常量
第二步:你定义自己的类(实现插头)
1
2
3
4
5
6
7
8
9
10
11
struct MyReceiver {
// Receiver 的实现
};
struct MySender {
// 注意:这是一个友元函数,第一个参数是标签类型
friend auto tag_invoke(connect_t, MySender& self, MyReceiver& r) {
std::cout << "MySender connected!\n";
return MyOperationState{};
}
};
第三步:使用方(插上电源)
1
2
3
4
5
6
MySender s;
MyReceiver r;
// 统一调用方式
unifex::connect(s, r);
// 实际上内部执行了 tag_invoke(connect_t{}, s, r)
4. 为什么 libunifex 疯狂使用它?
tag_invoke主要优势:
非侵入性 (Non-intrusive): 你可以在不修改原始类定义的情况下,为第三方库的类添加功能。只要定义一个对应的
tag_invoke重载即可。完美的泛型支持: 在模板代码里,我只需要写
unifex::connect(sender, receiver),编译器就能自动根据sender的类型找到正确的实现。如果sender没实现这个操作,编译报错会非常明确。零成本抽象 (Zero-overhead): 它在编译期就完成了分发,没有虚函数的性能损失,编译器可以轻松地进行内联优化。
总结
tag_invoke 就像是 C++ 里的 Trait (Rust) 或 Interface (Go) 的静态实现版本。它把“做什么操作”(Tag)和“谁来做”(Type)解耦了,然后通过一个全局函数把它们撮合在一起。
这里就把逻辑拆散到了各个 tag_invoke 的重载里,而不是集中在一个类的方法里。 你需要通过 Tag 去寻找真正的逻辑实现。
This post is licensed under CC BY 4.0 by the author.