You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 19:23:22