使用hana::compose结合shared_ptr时的内存管理问题解析
组合函数调用中的临时对象生命周期问题
我们有以下几个核心组件:
makeDocument:创建临时资源并返回std::shared_ptr<Document>deref:封装解引用运算符*的lambda表达式print:接收Document引用并输出其内容的函数- 自定义
compose工具(简化自boost::hana::compose)
在组合调用时发现两种写法行为完全不同:
compose(print, deref, makeDocument)("good"); // 正常输出 compose(print, compose(deref, makeDocument))("bad"); // 触发未定义行为(UB)
核心疑问:为什么后者中传递给deref的临时std::shared_ptr会在print处理其指向的对象前就销毁,而前者不会?
简化复现代码
#include <iostream> #include <memory> #include <string> #include <type_traits> #include <utility> template <typename F, typename G> struct compose { F f; G g; compose(F f, G g) : f{f}, g{g} {} template <typename X> decltype(auto) operator()(X const& x) const& { return f(g(x)); } }; struct Document { std::string str; Document(std::string&& str) : str{std::move(str)} {} ~Document() { str = "bad"; }; }; void print(Document const& doc1) { std::cout << doc1.str << std::endl; } auto deref = [](auto&& x) -> decltype(auto) { return *std::forward<decltype(x)>(x); }; auto makeDocument = [](std::string&& str) { return std::make_shared<Document>(std::move(str)); }; const auto good = compose(compose(print, deref), makeDocument); const auto bad = compose(print, compose(deref, makeDocument)); int main() { good("good"); bad("good"); }
原因分析
1. 正常执行的good调用流程
good本质是compose(compose(print, deref), makeDocument),调用时的逻辑展开为:
compose(print, deref)(makeDocument("good"));
- 首先执行
makeDocument("good"),生成一个临时std::shared_ptr<Document>。 - 这个临时对象作为参数传递给内层
compose(print, deref)的operator(),在该函数中执行print(deref(临时shared_ptr))。 - 根据C++临时对象生命周期规则:临时对象会在完整表达式结束前保持存活。这里的完整表达式是整个
good("good")调用,因此临时shared_ptr会一直存活到print执行完毕,其指向的Document对象不会被销毁,print能正确读取str的值。
2. 触发UB的bad调用流程
bad本质是compose(print, compose(deref, makeDocument)),调用时的逻辑展开为:
print(compose(deref, makeDocument)("good"));
而内层compose(deref, makeDocument)("good")的执行逻辑是:
deref(makeDocument("good"));
这里的关键问题在于deref的返回值:它返回的是*std::forward<decltype(x)>(x),也就是临时shared_ptr指向的Document对象的左值引用。
- 执行
makeDocument("good")生成临时shared_ptr,随后deref解引用它得到Document&。 - 内层
compose的operator()返回这个Document&,但此时临时shared_ptr的生命周期已经结束——因为makeDocument("good")是一个子表达式,临时对象在子表达式执行完毕后就会被销毁,对应的Document对象也会因shared_ptr引用计数降为0而被析构(析构函数会把str改为"bad")。 - 当
print接收到这个Document&时,它指向的对象已经是销毁状态,访问doc1.str属于访问悬空引用,触发未定义行为。
核心差异总结
good的调用中,临时shared_ptr的生命周期覆盖了print的整个执行过程,因为它作为参数传递给内层compose的operator(),而该函数的完整表达式要到print执行完毕才结束。bad的调用中,临时shared_ptr只是内层compose中g(x)的执行结果,仅在deref执行期间存活,一旦deref返回引用,临时shared_ptr就被销毁,导致后续print访问悬空引用。
内容的提问来源于stack exchange,提问作者Enlico
相关产品推荐
相关产品推荐

