带执行策略的std::for_each中Lambda捕获std::unique_ptr的问题
这是某问题的后续提问。移除<execution>头文件并使用无执行策略的std::for_each()重载时,代码可正常运行,但加上执行策略(无论是std::execution::seq还是std::execution::par)就会触发编译错误。
最小复现代码
#include <algorithm> #include <iostream> #include <vector> #include <memory> #include <cmath> #include <execution> struct A { A() {}; virtual ~A() = default; virtual double d(double x) = 0; }; struct B : A { double d(double x) override { return std::sqrt(std::abs(x)); } }; struct C : A { double d(double x) override { return std::abs(x); } }; std::unique_ptr<A> choose_operation(std::string s) { if (s == "B") { return std::make_unique<B>(); } else { return std::make_unique<C>(); } } double do_stuff(double x, const std::unique_ptr<A>& dfun) { x -= 2; return dfun->d(x); } int main() { std::vector<double> v {3, -4, 2, -8, 15, 267}; auto dfun = choose_operation("B"); std::for_each(std::execution::seq, v.begin(), v.end(), [dfun = std::move(dfun)](double& x) { x = do_stuff(x, dfun); }); return 0; }
编译命令:g++-13 -std=c++23 main.cpp
错误信息
In file included from /opt/homebrew/Cellar/gcc/13.2.0/include/c++/13/execution:40, from main.cpp:6: /opt/homebrew/Cellar/gcc/13.2.0/include/c++/13/pstl/glue_algorithm_impl.h: In instantiation of '__pstl::__internal::__enable_if_execution_policy<_ExecutionPolicy, void> std::for_each(_ExecutionPolicy&&, _ForwardIterator, _ForwardIterator, _Function) [with _ExecutionPolicy = const __pstl::execution::v1::sequenced_policy&; _ForwardIterator = __gnu_cxx::__normal_iterator<double*, vector<double> >; _Function = main()::<lambda(double&)>; __pstl::__internal::__enable_if_execution_policy<_ExecutionPolicy, void> = void]': main.cpp:39:18: required from here /opt/homebrew/Cellar/gcc/13.2.0/include/c++/13/pstl/glue_algorithm_impl.h:59:40: error: use of deleted function 'main()::<lambda(double&)>::<lambda>(const main()::<lambda(double&)>&)' 59 | __pstl::__internal::__pattern_walk1( | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ 60 | std::forward<_ExecutionPolicy>(__exec), __first, __last, __f, | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 61 | __pstl::__internal::__is_vectorization_preferred<_ExecutionPolicy, _ForwardIterator>(__exec), | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 62 | __pstl::__internal::__is_parallelization_preferred<_ExecutionPolicy, _ForwardIterator>(__exec)); | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ main.cpp:39:83: note: 'main()::<lambda(double&)>::<lambda>(const main()::<lambda(double&)>&)' is implicitly deleted because the default definition would be ill-formed: 39 | std::for_each(std::execution::seq, v.begin(), v.end(), [dfun = std::move(dfun)](double& x) { | ^ main.cpp:39:83: error: use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = A; _Dp = std::default_delete<A>]' In file included from /opt/homebrew/Cellar/gcc/13.2.0/include/c++/13/memory:78, from main.cpp:4: /opt/homebrew/Cellar/gcc/13.2.0/include/c++/13/bits/unique_ptr.h:522:7: note: declared here 522 | unique_ptr(const unique_ptr&) = delete; | ^~~~~~~~~~ In file included from /opt/homebrew/Cellar/gcc/13.2.0/include/c++/13/pstl/glue_execution_defs.h:50, from /opt/homebrew/Cellar/gcc/13.2.0/include/c++/13/execution:34: /opt/homebrew/Cellar/gcc/13.2.0/include/c++/13/pstl/algorithm_impl.h:107:98: note: initializing argument 4 of 'void __pstl::__internal::__pattern_walk1(_ExecutionPolicy&&, _ForwardIterator, _ForwardIterator, _Function, _IsVector, std::false_type) [with _ExecutionPolicy = const __pstl::execution::v1::sequenced_policy&; _ForwardIterator = __gnu_cxx::__normal_iterator<double*, std::vector<double> >; _Function = main()::<lambda(double&)>; _IsVector = std::integral_constant<bool, false>; std::false_type = std::integral_constant<bool, false>]' 107 | __pattern_walk1(_ExecutionPolicy&&, _ForwardIterator __first, _ForwardIterator __last, _Function __f, |
疑问
- 请解读上述错误信息,并提供将
unique_ptr传入Lambda捕获的合适方法(注意:dfun对象还会在代码其他地方使用,不能直接将其定义放在Lambda体内)。 - 以下版本的循环可以编译通过,但在此处传递引用是否属于良好实践?
std::for_each(std::execution::seq, v.begin(), v.end(), [dfun = &dfun](double& x) { x = do_stuff(x, *dfun); });
错误信息解读
核心问题在于带执行策略的std::for_each要求传入的可调用对象(此处为Lambda)必须可复制,但你的Lambda捕获了std::unique_ptr<A>——而unique_ptr的拷贝构造函数是被显式删除的(unique_ptr(const unique_ptr&) = delete),因此Lambda的默认拷贝构造函数也被隐式删除。当PSTL(并行标准模板库)内部尝试复制该Lambda以执行循环逻辑时,就触发了编译错误。
而不带执行策略的std::for_each不需要复制可调用对象,直接原地调用,因此不会触发该问题。
正确传递unique_ptr的方法
由于dfun还需在其他地方使用,不能转移所有权,推荐以下两种方案:
方案1:捕获unique_ptr的引用
直接捕获dfun的引用,Lambda内部使用原对象的引用,无需拷贝unique_ptr:
std::for_each(std::execution::seq, v.begin(), v.end(), [&dfun](double& x) { x = do_stuff(x, dfun); });
注意:必须保证dfun的生命周期覆盖std::for_each的执行周期——若使用par并行策略,要确保所有线程完成前dfun不会被销毁或重置。
方案2:捕获原始指针并适配函数参数
从unique_ptr中取出原始指针捕获,同时修改do_stuff以接受原始指针或对象引用(更合理,因为函数不需要管理所有权):
// 修改do_stuff为接受const A&,更符合语义 double do_stuff(double x, const A& dfun) { x -= 2; return dfun.d(x); } // 调用时捕获原始指针 std::for_each(std::execution::seq, v.begin(), v.end(), [ptr = dfun.get()](double& x) { x = do_stuff(x, *ptr); });
这种方式既避免了拷贝unique_ptr,又解耦了函数与智能指针的绑定,代码更清晰。
方案3:使用std::shared_ptr(仅必要时使用)
若确实需要拷贝可调用对象(比如并行场景下每个线程需持有一份),可将unique_ptr替换为shared_ptr,它支持拷贝且通过引用计数管理所有权:
// 修改工厂函数返回shared_ptr std::shared_ptr<A> choose_operation(std::string s) { if (s == "B") { return std::make_shared<B>(); } else { return std::make_shared<C>(); } } // 捕获shared_ptr std::for_each(std::execution::seq, v.begin(), v.end(), [dfun](double& x) { x = do_stuff(x, dfun); });
此方案会引入引用计数开销,仅当需要多副本持有所有权时考虑。
关于后续问题:传递引用是否为良好实践
你写出的代码本质是捕获dfun的指针,可行但有几点注意事项:
- 生命周期安全:必须保证
dfun在std::for_each执行期间(包括并行线程全部完成前)始终有效,不能提前销毁或重置。串行策略下只要dfun在for_each结束后才被销毁就没问题;并行策略下需额外注意线程同步与对象生命周期。 - 代码可读性:
[dfun = &dfun]写法较绕,不如直接用[&dfun]捕获引用直观,效果完全一致(Lambda内部访问的都是原dfun对象)。 - 参数适配:原
do_stuff接受const unique_ptr<A>&,你需要调整函数参数为const A&或const A*才能匹配*dfun的类型,否则会出现类型不匹配错误。
总的来说,捕获引用([&dfun])是更简洁且安全的良好实践,只要保证生命周期安全即可;捕获指针的写法本质没问题,但可读性稍差。
内容的提问来源于stack exchange,提问作者Øystein S

