001-在lambda中使用unique_ptr的注意事项

问题概述

在处理jsonrpc请求时,为了简化解析流程,为不同的method定义了不同的方法,接收一个请求类的unique_ptr,然后通过std::unordered_map来关联method和方法,最后通过asio::thread_pool来异步处理任务。

请求的定义如下:

struct JsonRpcRequest
{
    std::string              method;
    std::unique_ptr<json>    params; // 可选
    std::unique_ptr<int64_t> id;     // 整数 id,nullptr 表示通知
};

unordered_map的定义如下:

std::unordered_map<std::string, std::function<void(std::unique_ptr<JsonRpcRequest>)>> 
m_method2Funcion = {
    {"online", [](std::unique_ptr<JsonRpcRequest> request) { parseOnline(request); }},
    // 其余方法省略
};

最初解析函数的定义如下:

// req为std::unique_ptr<JsonRpcRequest>
auto  it  = m_method2Funcion.find(req->method);
if (it != m_method2Funcion.end())
{
    asio::post(m_threadPool,
               [this, it, r = std::move(req)]()
               {
                   std::string method;
                   try
                   {
                       method = r->method;
                       it->second(std::move(r));
                   }
                   catch (std::exception const& e)
                   {
                       spdlog::error("处理请求{}时发生异常 {}", method, e.what());
                   }
                   catch (...)
                   {
                       spdlog::error("处理请求{}时发生未知异常", method);
                   }
               });
    return;
}

但是在it->second(std::move(r));调用时,始终报错“类型不匹配”:

无法引用 函数 "std::unique_ptr<_Ty, _Dx>::unique_ptr(const std::unique_ptr<_Ty, _Dx> &) [其中 _Ty=jsonrpc::JsonRpcRequest, _Dx=std::default_delete<jsonrpc::JsonRpcRequest>]" 

原因分析 - deepseek

以下内容来自deepseek

问题的根源在于:lambda 表达式通过捕获 r = std::move(req) 持有了一个 std::unique_ptr,这使得 lambda 的闭包类型不可复制。而 asio::post(在某些实现或版本中)会将传入的可调用对象复制一份存储(例如内部使用了 std::function),导致编译器尝试复制该 lambda,从而触发 unique_ptr 已删除的复制构造函数。

解决方案

使用 std::shared_ptr<std::unique_ptr<JsonRpcRequest>>(即“共享的 unique_ptr”)来转移所有权。这样 lambda 捕获一个可复制的 shared_ptr,内部仍然可以取出唯一的 unique_ptr 传递给处理函数。

修改你贴出的代码段:

auto& req = msg.request;   // 假设是 std::unique_ptr<JsonRpcRequest>&
auto  it  = m_method2Funcion.find(req->method);
if (it != m_method2Funcion.end())
{
    // 将 unique_ptr 包装到 shared_ptr 中
    auto pReq = std::make_shared<reqptr>(std::move(req));

    asio::post(m_threadPool,
               [this, it, pReq]()   // 捕获可复制的 shared_ptr
               {
                   // 注意:先取出方法名,因为在移动后 pReq 中的 unique_ptr 会变为空
                   std::string method = (*pReq)->method;
                   try
                   {
                       it->second(std::move(*pReq));  // 取出 unique_ptr 并移交所有权
                   }
                   catch (std::exception const& e)
                   {
                       spdlog::error("处理请求{}时发生异常 {}", method, e.what());
                   }
                   catch (...)
                   {
                       spdlog::error("处理请求{}时发生未知异常", method);
                   }
               });
    return;
}

为什么这样是正确的?

  • pReqstd::shared_ptr<reqptr>,可以被安全地复制(多个 lambda 副本共享同一个 unique_ptr)。
  • 实际调用 it->second(std::move(*pReq)) 时,会从 shared_ptr 的底层 unique_ptr移动出对象,之后 *pReq 为空。
  • 由于 lambda 通常只会被执行一次,这种一次性的 move 是安全的。
  • (*pReq)->method 必须在 move 之前求值,否则会出现空指针访问。

补充建议

  1. 确认 m_method2Funcion 的线程安全性:如果请求可能并发处理,而 m_method2Funcion 会在运行时被修改,需要考虑加锁或改用无锁结构。但根据你给的初始化列表,它更像是静态注册表,问题不大。
  2. asio 版本差异:较新的 Asio 通常只要求 handler 是 MoveConstructible,但旧版本或某些包装(如 io_context::post 在内部使用 std::function)会要求复制。上述方案兼容两种情况,是更健壮的做法。

在按上述建议修改后,还是会报错:

“std::unique_ptr<jsonrpc::JsonRpcRequest,std::default_deletejsonrpc::JsonRpcRequest>::unique_ptr(const std::unique_ptr<jsonrpc::JsonRpcRequest,std::default_deletejsonrpc::JsonRpcRequest> &)”: 无法将参数 1 从“_Ty”转换为“const std::unique_ptr<jsonrpc::JsonRpcRequest,std::default_deletejsonrpc::JsonRpcRequest> &”

以下为deepseek的解释

这个错误信息其实是同一个问题的不同表现:你试图在 asio::post 的 lambda 中以“捕获值”的方式传递 std::unique_ptr,而闭包需要被复制,这就调用了 unique_ptr 的拷贝构造函数(已被删除)

错误解读

编译器提示:

unique_ptr(const std::unique_ptr<...> &)”: 无法将参数 1 从“_Ty”转换为“const std::unique_ptr<...> &”

意思是:在尝试构造一个 unique_ptr(例如闭包对象的成员)时,需要调用拷贝构造函数,但传入的参数类型不匹配(或根本不存在可行的转换)。实际上,_Ty 就是你代码中的某个 unique_ptr 实例,而编译器无法找到从该实例到 const unique_ptr& 的合法转换(因为拷贝构造被 = delete 了)。这条信息通常在 lambda 按值捕获 unique_ptr 时出现,例如:

asio::post(m_threadPool, [this, it, req]() { ... });  // 错误:req 被复制

即使你写成了 r = std::move(req),如果 asio::post 内部要把整个 lambda 复制一份(比如存入 std::function),闭包内的 runique_ptr 类型)仍然需要被复制,同样会报错。

为什么 r = std::move(req) 还会报错?

你的原始代码:

[r = std::move(req)]() { ... }

这其实是 C++14 的初始化捕获,相当于在闭包对象内创建了一个 unique_ptr<JsonRpcRequest> r,并用 std::move(req) 初始化它。闭包类型中有一个 unique_ptr 成员,导致整个闭包对象不可复制

asio::post 的某些实现(特别是当它内部需要将可调用对象包装为 std::function<void()> 时)会复制传入的 handler,因此触发错误。

最终解决方案(再次确认)

std::shared_ptr 包装 unique_ptr,使闭包可复制:

auto& req = msg.request;   // req 是 unique_ptr<JsonRpcRequest>&
auto  it  = m_method2Funcion.find(req->method);
if (it != m_method2Funcion.end())
{
    // 将 unique_ptr 的所有权移入 shared_ptr
    auto pReq = std::make_shared<std::unique_ptr<JsonRpcRequest>>(std::move(req));

    asio::post(m_threadPool,
        [this, it, pReq]()   // shared_ptr 可以安全复制
        {
            std::string method = (*pReq)->method;   // 在移动前取出方法名
            try
            {
                it->second(std::move(*pReq));       // 取出 unique_ptr 并移动给处理函数
            }
            catch (std::exception const& e)
            {
                spdlog::error("处理请求{}时发生异常 {}", method, e.what());
            }
            catch (...)
            {
                spdlog::error("处理请求{}时发生未知异常", method);
            }
        });
    return;
}

关键点

  • pReq 的类型std::shared_ptr<std::unique_ptr<JsonRpcRequest>>
  • Lambda 捕获 pReq 时只复制 shared_ptr(引用计数+1),不会触碰内部的 unique_ptr
  • 在 lambda 体内通过 std::move(*pReq)unique_ptr 实际移动出来,交给你的处理函数。因为 lambda 只会执行一次,这种移动是安全的。

另一种方式:使用 std::function 的移动构造(要求 C++14)

如果你的 asio::post 支持移动语义(只要求 handler 可移动构造),你也可以显式移动 lambda:

asio::post(m_threadPool,
    [this, it, r = std::move(req)]() mutable {   // 注意 mutable
        // 直接使用 r (unique_ptr)
        std::string method = r->method;
        try { it->second(std::move(r)); }
        catch (...) { ... }
    });

你不能直接依赖这种行为,因为很多库(尤其是旧版 Asio 或使用 std::function 的场景)会要求复制。用 shared_ptr 是最通用、最安全的方法。

你的错误表明你当前的环境确实需要可复制的 handler,因此请使用 shared_ptr 方案。

问题总结

  • asio::post要求lambda捕获能够复制,而直接捕获unique_ptr会导致其调用删除的拷贝函数,导致错误;
  • 使用shared_ptr可以避免这个问题,因为shared_ptr是可复制的,而unique_ptr在移动后会被置空,因此不会出现空指针访问的问题。