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

带执行策略的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,
      |                           

疑问

  1. 请解读上述错误信息,并提供将unique_ptr传入Lambda捕获的合适方法(注意:dfun对象还会在代码其他地方使用,不能直接将其定义放在Lambda体内)。
  2. 以下版本的循环可以编译通过,但在此处传递引用是否属于良好实践?
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的指针,可行但有几点注意事项:

  1. 生命周期安全:必须保证dfun在std::for_each执行期间(包括并行线程全部完成前)始终有效,不能提前销毁或重置。串行策略下只要dfun在for_each结束后才被销毁就没问题;并行策略下需额外注意线程同步与对象生命周期。
  2. 代码可读性:[dfun = &dfun]写法较绕,不如直接用[&dfun]捕获引用直观,效果完全一致(Lambda内部访问的都是原dfun对象)。
  3. 参数适配:原do_stuff接受const unique_ptr<A>&,你需要调整函数参数为const A&或const A*才能匹配*dfun的类型,否则会出现类型不匹配错误。

总的来说,捕获引用([&dfun])是更简洁且安全的良好实践,只要保证生命周期安全即可;捕获指针的写法本质没问题,但可读性稍差。


内容的提问来源于stack exchange,提问作者Øystein S

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 20:24:51