C++最小可调用对象包装器实现相关技术问题咨询
无堆分配可调用对象包装器PipelineStep实现答疑
现有实现(已修正原代码笔误:构造函数初始化列表缺少闭合括号)
#include <utility> #include <functional> template <class F> class PipelineStep { F _action; public: template <class J> PipelineStep(J&& operation) : _action(std::forward<J>(operation)) {} template <class... Args> decltype(auto) operator()(Args&&... args) { return std::invoke(_action, std::forward<Args>(args)...); } }; // 类模板推导指引 template <class T> PipelineStep(T&&) -> PipelineStep<std::decay_t<T>>;
该实现目标为避免std::function带来的内存分配开销,传入左值可调用对象时执行拷贝构造,传入右值时执行移动构造,作为强类型支持operator|链式调用。以下针对四个问题逐一解答:
1. 无推导指引时无法处理非临时对象,是否存在更优实现?
不提供推导指引时编译失败是类模板隐式推导的规则决定的:转发引用构造函数的参数J&&在传入左值时,会将J推导为左值引用类型,隐式推导出的类模板参数F就会是引用类型,不符合存储值的设计预期。
你当前手写std::decay_t推导指引的写法已经是性能最优的实现,没有额外运行时开销。如果不想手写推导指引,可以将构造函数改为值参数接收的形式:
PipelineStep(F operation) : _action(std::move(operation)) {}
这种写法依赖类模板隐式推导自动对传入参数做decay,不需要额外写推导指引,但会多一次移动构造开销:传入右值时先移动构造形参,再移动构造成员;传入左值时先拷贝构造形参,再移动构造成员。对lambda、普通函数对象、函数指针这类移动成本极低的类型来说该开销可以忽略,但如果需要支持不可移动、移动成本极高的可调用类型,转发引用构造+手写decay推导指引的方案仍然是首选。
2. _action成员生命周期是否安全?是否永远持有值类型不持有引用?
在当前带std::decay_t推导指引的实现下,你的假设完全成立:
- 推导指引强制类模板参数
F永远是decay后的纯值类型,不会出现引用、cv限定数组/函数类型等无法作为值成员存储的类型 - 构造函数通过完美转发将传入的可调用对象要么拷贝、要么移动到
_action成员中,_action是PipelineStep实例自身持有的值成员,生命周期和实例完全绑定,不会绑定外部对象的引用,不存在悬垂引用风险
如果移除推导指引,传入左值时F会被推导为左值引用类型,此时_action会成为引用成员,绑定到外部传入的对象,一旦外部对象提前销毁就会触发悬垂引用,生命周期不安全。
3. decltype(auto)作为operator()返回类型是否最优?能否正确保留返回值的值类别?
decltype(auto)是该场景下的最优选择,可以完整保留std::invoke调用结果的值类别,不会引入额外拷贝或值类别丢失问题:
- 被包装的可调用对象返回左值引用时,
decltype(auto)会推导为左值引用 - 被包装的可调用对象返回右值引用时,
decltype(auto)会推导为右值引用 - 被包装的可调用对象返回纯值类型时,
decltype(auto)会推导为对应值类型,不会发生不必要的拷贝
如果需要支持const实例调用、右值实例调用的场景,可以补充对应cv/引用限定的重载版本,返回值同样使用decltype(auto)即可。
4. 推导指引中使用std::decay_t是否必要?
std::decay_t在推导指引中是必要的,没有可省略的空间:
- 转发引用的推导规则决定了,传入左值时模板参数
T会被推导为左值引用,传入数组、函数类型时T会被推导为数组引用、函数引用,这些类型都无法作为值成员存储 std::decay_t会按顺序执行类型转换:移除引用限定、移除顶层cv限定符、将数组类型转换为对应指针、将函数类型转换为对应函数指针,最终得到可以合法作为值成员存储的纯值类型- 如果替换
std::decay_t为std::remove_cvref_t,遇到数组、函数类型传入时会直接触发编译错误,无法兼容所有可调用对象场景。
内容的提问来源于stack exchange,提问作者Lorah Attkins
相关产品推荐
相关产品推荐

