Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

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.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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

In short: It is never safe to assume you can pass a move-only functor to anything in the <algorithm> library. 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 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.

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading