线程池enqueue去内存分配后触发packaged_task拷贝构造报错求助
原始可运行代码
template <typename F, typename... Args> auto enqueue(F&& f, Args&&... args) -> std::future<decltype(f(args...))> { using return_type = decltype(f(args...)); auto task = std::make_shared<std::packaged_task<return_type()>>( std::bind(std::forward<F>(f), std::forward<Args>(args)...) ); auto result = task->get_future(); { std::unique_lock<std::mutex> lock(m_queue_mutex); if (m_stop) throw std::runtime_error("enqueue on stopped ThreadPool"); m_tasks.emplace([task]() { (*task)(); }); } m_condition.notify_one(); return result; }
优化后的错误代码
template <typename F, typename... Args> auto enqueue(F&& f, Args&&... args) -> std::future<decltype(f(args...))> { using return_type = decltype(f(args...)); auto task = std::packaged_task<return_type()>( std::bind(std::forward<F>(f), std::forward<Args>(args)...) ); auto result = task.get_future(); { std::unique_lock<std::mutex> lock(m_queue_mutex); if (m_stop) throw std::runtime_error("enqueue on stopped ThreadPool"); m_tasks.emplace([task = std::move(task)]() mutable { task(); }); } m_condition.notify_one(); return result; }
触发的错误
use of deleted function ‘std::packaged_task<_Res(_ArgTypes...)>::packaged_task(const std::packaged_task<_Res(_ArgTypes ...)&) [with _Res = void; _ArgTypes = {}]’
38 | m_tasks.emplace(task mutable { task(); });
问题原因
核心问题在于:
std::packaged_task是移动-only类型,拷贝构造函数被显式删除,只能通过移动语义转移所有权。- 线程池任务队列
m_tasks通常是std::queue<std::function<void()>>类型,而C11到C20标准中,std::function要求存储的可调用对象必须可拷贝构造。 - 尽管你用
std::move将packaged_task转移到lambda捕获列表,但lambda会因持有不可拷贝的packaged_task变成移动-only类型。当尝试将该lambda放入std::function时,std::function会尝试拷贝lambda,进而触发packaged_task的拷贝构造函数,导致编译错误。
原始代码用std::make_shared包装packaged_task,本质是让lambda捕获可拷贝的std::shared_ptr,使lambda本身成为可拷贝类型,符合std::function的要求,因此可以正常运行。
解决方案
根据需求,有三种可行方案:
方案1:保留std::make_shared实现
这是最稳妥、兼容性最好的方案,虽存在一次堆分配,但代码简洁可靠,适合大多数场景。
方案2:升级到C++23标准
C++23对std::function进行了扩展,允许其持有移动-only的可调用对象。此时你的优化代码可直接编译通过,无需修改——std::function会通过移动语义接收lambda,不再尝试拷贝。
方案3:自定义移动-only任务队列
若需在C++23之前的标准下避免显式使用std::make_shared,可自定义基于抽象基类的任务包装器替代std::function:
// 抽象任务基类 class Task { public: virtual ~Task() = default; virtual void operator()() = 0; }; // 具体任务模板类 template <typename F> class ConcreteTask : public Task { public: explicit ConcreteTask(F&& f) : m_func(std::move(f)) {} void operator()() override { m_func(); } private: F m_func; }; // 任务队列改为存储unique_ptr std::queue<std::unique_ptr<Task>> m_tasks; // enqueue函数修改如下 template <typename F, typename... Args> auto enqueue(F&& f, Args&&... args) -> std::future<decltype(f(args...))> { using return_type = decltype(f(args...)); auto task = std::packaged_task<return_type()>( std::bind(std::forward<F>(f), std::forward<Args>(args)...) ); auto result = task.get_future(); { std::unique_lock<std::mutex> lock(m_queue_mutex); if (m_stop) throw std::runtime_error("enqueue on stopped ThreadPool"); m_tasks.emplace(std::make_unique<ConcreteTask<decltype(std::move(task))>>( std::move(task) )); } m_condition.notify_one(); return result; }
该方案通过std::unique_ptr存储任务对象,利用移动语义避免拷贝,同时保持任务队列的类型统一性。本质仍存在堆分配(std::make_unique),但和std::make_shared的开销差异不大。
内容的提问来源于stack exchange,提问作者lior

