带回调的定制仿函数类的惯用替代方案是什么?
背景
当你查看某个代码库时,如果看到如下代码:
class A { virtual ~A() = default; virtual void act() = 0; };
你可能会说:“这本质上就是一个无输入无输出的函数式对象(或仿函数)的接口。完全可以去掉这个类,把如下代码:
void foo(A& a) { a.act(); }
替换为:
template <std::invocable F> void foo(F& f) { f(); }
并将所有继承自A的类对象替换为任意可调用对象,用其operator()实现原act()方法的功能。
当然,在C++20之前的代码中也能实现相同效果,用
std::enable_if或static_assert()确保只有可调用对象被传入foo()。
实际问题
现在假设你看到如下基类定义:
class B { virtual ~B() = default; /// act()完成内部工作后会调用这些回调 void addCallback(const CallbackType& callback); virtual void act(); };
它不仅是一个(有状态的)仿函数,还包含一系列回调函数。
那么可以用哪种泛型惯用法替代如下函数:
void foo(B& b) { b.act(); }
同时避免使用定制类B?
注意事项:
- 可以假设
b的具体类型在编译期已知。 - 请不要建议使用所有
B子类的std::variant,这不是我想要的方案。
解决方案
我们可以通过编译期接口约束的泛型惯用法来替代基类B的继承约束,核心是要求传入的类型必须满足「拥有act()方法」和「拥有addCallback()方法」这两个条件,具体实现分C++20和更早版本两种情况:
C++20 版本(使用概念)
先定义一个概念来描述符合要求的类型:
#include <concepts> template<typename T> concept ActWithCallbacks = requires(T& t, const CallbackType& cb) { { t.act() } -> std::same_as<void>; { t.addCallback(cb) } -> std::same_as<void>; };
然后改写foo函数:
template<ActWithCallbacks T> void foo(T& b) { b.act(); }
只要传入的类型T同时拥有void act()和void addCallback(const CallbackType&)成员函数,就能通过编译,完全不需要依赖基类B。
C++20 之前版本(使用SFINAE或静态断言)
如果需要兼容更早的C++标准,可以用std::enable_if结合decltype做SFINAE约束:
#include <type_traits> template<typename T> auto foo(T& b) -> typename std::enable_if_t< std::is_same_v<decltype(std::declval<T&>().act()), void> && std::is_same_v<decltype(std::declval<T&>().addCallback(std::declval<const CallbackType&>())), void> > { b.act(); }
或者用static_assert做编译期断言,让错误提示更直观:
template<typename T> void foo(T& b) { static_assert(std::is_same_v<decltype(b.act()), void>, "传入类型必须包含void act()成员函数"); static_assert(std::is_same_v<decltype(b.addCallback(std::declval<const CallbackType&>())), void>, "传入类型必须包含void addCallback(const CallbackType&)成员函数"); b.act(); }
额外说明
这种方案的优势在于完全基于鸭子类型的编译期检查,不需要继承体系,同时保留了原基类B的接口约束能力。如果后续需要扩展接口要求,只需修改概念或断言条件即可。
内容的提问来源于stack exchange,提问作者einpoklum
相关产品推荐
相关产品推荐

