Search code examples
c++lambdastlc++17

Passing unique pointer to function in a lambda capture for parallel execution


This question is a follow-up to this one. Of note, by removing the <execution> header and using the std::for_each() overload with no execution strategy, it works well.

I have a setup with a minimal reproducing example shown below.

Base class A is pure virtual and the two derived classes have different versions of a function d() that is supposed to do some operations.

I use the factory function choose_operation to choose between class B and C at runtime, and it returns a std::unique_ptr<A>. Then I use a std::for_each, and I need to pass the correct version of A::d() into std::for_each, and from there onto a function do_stuff() which does more work. The error is the same whether I use std::execution::seq or 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;
}

I compile it with g++-13 -std=c++23 main.cpp.

Here is the error message:

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,
      |                          

Can someone please help interpret the error message, and suggest appropriate ways of passing the unique pointer into the lambda capture?

I also should add that the object dfun is used other places in the code, so simply putting its definition inside the lambda body is not a good solution.

Follow-up question

I notice that the following version for the for-loop does compile, but is it good practice to pass a reference here?

std::for_each(std::execution::seq, v.begin(), v.end(), [dfun = &dfun](double& x) {
        x = do_stuff(x, *dfun);
    });

Solution

  • The parallel algorithm tries to copy the lambda, which doesn't work because the unique_ptr can only be moved.

    C++ standard section "Algorithms requirements" [algorithms.requirements] paragraph 10:

    Unless otherwise specified, algorithms that take function objects as arguments are permitted to copy those function objects freely. Programmers for whom object identity is important should consider using a wrapper class that points to a noncopied implementation object such as reference_wrapper<T> (20.14.5), or some equivalent solution

    Then section 25.5.4 "For each" [alg.foreach] states for the serial version:

    Preconditions: Function meets the Cpp17MoveConstructible requirements (Table 26). [Note: Function need not meet the requirements of Cpp17CopyConstructible (Table 27).

    while it states for the parallel version

    Preconditions: Function meets the Cpp17CopyConstructible requirements

    In short: Different algorithms define different requirements on moving or copying. Normally it works, because why should a basic algorithm copy its functor, right?

    However, with parallel algorithm, things are more complicated. Additionally, as a programmer you actually often want to have a copy so that you can put thread-local state such as allocations into the functor and reuse them between invocations. Sadly, the standard makes this pretty much impossible because it is not specified whether a copy will actually take place or not. See for example Thread-specific variables when using parallel algorithms

    Anyway, try std::ref, switch to shared_ptr, or hold the unique_ptr outside the lambda and then use a plain reference or raw pointer in the lambda.