将std::async从lambda改为函数指针编译失败,求原因?
问题背景
我正在编写一个封装std::async的工具函数,用于捕获所有未处理异常并记录日志。最初的代码可以正常运行,后来尝试将双重转发的可变lambda替换为函数指针,直接利用std::async的参数传递签名,结果编译失败,想知道问题出在哪。
可正常运行的代码
#include <functional> #include <future> #include <iostream> #include <string> #include <thread> namespace Async { namespace detail { template <typename F, typename... Args> std::invoke_result_t<F, Args...> RunWithLogging(std::string label, F&& f, Args&&... args) { try { return std::invoke(std::forward<F>(f), std::forward<Args>(args)...); } catch (std::exception& ex) { std::cout << label << "Exception escaped thread \"" << label << "\": " << ex.what(); throw; } catch (...) { std::cout << label << "Exception escaped thread \"" << label << "\": (non-standard exception type)"; throw; } } } // namespace detail /// 功能类似std::async(std::launch::async, f, args...): /// - 捕获并记录所有未处理的异常 /// 返回的future会在析构时join线程 template <typename F, typename... Args> [[nodiscard]] std::future<std::invoke_result_t<F, Args...>> Launch( std::string label, F&& f, Args&&... args) { return std::async( std::launch::async, [label = std::move(label), f = std::forward<F>(f), ... args = std::forward<Args>(args)]() mutable { return detail::RunWithLogging(std::move(label), std::forward<F>(f), std::forward<Args>(args)...); }); } } // namespace Async
修改后编译失败的代码
template <typename F, typename... Args> [[nodiscard]] std::future<std::invoke_result_t<F, Args...>> Launch( std::string label, F&& f, Args&&... args) { return std::async(std::launch::async, &detail::RunWithLogging, std::move(label), std::forward<F>(f), std::forward<Args>(args)...); }
编译错误信息
C:\...\lib\Async\Thread.h(33,15): error C2672: 'async': no matching overloaded function found [C:\...\build\lib\Async\AsyncTest.vcxproj] C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Tools\MSVC\14.34.31933\include\future(1535,81): message : could be 'std::future<_Select_invoke_traits<decay<_Ty>::type,decay<_ArgTypes>::type...>::type> std::async(_Fty &&,_ArgTypes &&...)' [C:\...\build\lib\Async\AsyncTest.vcxproj] C:\...\lib\Async\Thread.h(33,15): message : 'std::future<_Select_invoke_traits<decay<_Ty>::type,decay<_ArgTypes>::type...>::type> std::async(_Fty &&,_ArgTypes &&...)': could not deduce template argument for '_ArgTypes &&' from 'overloaded-function' [C:\...\build\lib\Async\AsyncTest.vcxproj] C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Tools\MSVC\14.34.31933\include\future(1522,81): message : or 'std::future<_Select_invoke_traits<decay<_Ty>::type,decay<_ArgTypes>::type...>::type> std::async(std::launch,_Fty &&,_ArgTypes &&...)' [C:\...\build\lib\Async\AsyncTest.vcxproj] C:\...\lib\Async\Thread.h(32,47): message : 'std::future<_Select_invoke_traits<decay<_Ty>::type,decay<_ArgTypes>::type...>::type> std::async(std::launch,_Fty &&,_ArgTypes &&...)': could not deduce template argument for '_Fty' [C:\...\build\lib\Async\AsyncTest.vcxproj] C:\...\lib\Async\ThreadTest.cpp(24,57): message : see reference to function template instantiation 'std::future<int> Async::Launch<int(__cdecl *)(int),int>(std::string,F &&,int &&)' being compiled [C:\...\build\lib\Async\AsyncTest.vcxproj] with [ F=int (__cdecl *)(int) ]
我已经尝试过这些方法,但都没用:
- 为
std::invoke_result_t的参数添加std::decay_t<> - 显式指定
RunWithLogging的模板参数 - 使用
&detail::RunWithLogging或detail::RunWithLogging - 使用
auto返回类型
问题原因与解决方案
核心问题
detail::RunWithLogging是一个模板函数,它本身不是具体的函数,只有当模板参数被推导或显式指定后,才会生成具体的函数实例。而std::async无法从传入的参数中自动推导RunWithLogging的模板参数——std::async的参数推导逻辑仅处理自身的模板参数,不会递归推导传入的函数模板的参数。
当你把&detail::RunWithLogging传给std::async时,编译器看到的是一个重载集合(模板函数可以生成无数个不同的重载),无法确定要选择哪一个具体的函数实例,因此推导失败。
解决方案
你需要显式指定RunWithLogging的模板参数,让编译器明确知道要使用哪个函数实例。修改后的Launch函数如下:
template <typename F, typename... Args> [[nodiscard]] std::future<std::invoke_result_t<F, Args...>> Launch( std::string label, F&& f, Args&&... args) { // 显式指定RunWithLogging的模板参数 return std::async( std::launch::async, &detail::RunWithLogging<std::decay_t<F>, std::decay_t<Args>...>, std::move(label), std::forward<F>(f), std::forward<Args>(args)...); }
这里使用std::decay_t是因为std::async会对传入的参数进行衰变(decay)处理(比如把左值引用转为值类型),和lambda版本中捕获参数的行为一致。
另外,也可以用一个轻量包装lambda来隐式推导模板参数,本质和原lambda逻辑一致,但代码更简洁:
template <typename F, typename... Args> [[nodiscard]] std::future<std::invoke_result_t<F, Args...>> Launch( std::string label, F&& f, Args&&... args) { return std::async( std::launch::async, [](std::string l, F&& func, Args&&... args) { return detail::RunWithLogging(std::move(l), std::forward<F>(func), std::forward<Args>(args)...); }, std::move(label), std::forward<F>(f), std::forward<Args>(args)...); }
为什么原lambda版本能正常工作?
原lambda是一个具体的闭包类型,它的operator()是一个实例化后的非模板函数,std::async可以轻松推导它的参数类型,进而正确调用。而直接传递模板函数指针时,缺少明确的实例化信息,导致推导失败。
内容的提问来源于stack exchange,提问作者MHebes

