Move bug in task creation?

Jan 6, 2015 at 11:12 PM
Consider this code:
#include <pplx/pplxtasks.h>
#include <iostream>
#include <thread>

class A
    A() = default;
    A(A&) = delete;
    A(A&& that) { that.moved = true; }
    void operator = (A&) = delete;
    void operator = (A&&) { }
    void HelloWorld() const { std::cout << "Hello World!\n";  }

    bool moved = false;

int main()
    A a;
    //pplx::task<void>([b = std::move(a)]{ std::cout << b.moved << "\n"; b.HelloWorld(); }).wait();
    std::thread([b = std::move(a)]{ std::cout << b.moved << "\n"; b.HelloWorld(); }).join();
    std::cout << a.moved << "\n";
This runs and prints Hello World as expected. We can also see that "a" was moved from and that "b" was not (so it should be the new "a"). So, in other words, the code works as expected.

But uncomment the pplx::task line and comment the task line and you get a compile error. Obviously, pplx::create_task also fails.

This is compiled with visual studio 2015.

I'm uncertain why this fails. This may also be a compiler bug since I've noticed compiler support for generalized lambda captures are a little buggy. But I've not gotten this up and working on other compilers.

I've also tried workaround, such as "faking" move semantics to capture the variable as per the usual C++11 ways, but that also gave the same error.
Jan 7, 2015 at 6:44 PM
Hi Essentia,

So the compilation error is:
1> Source.cpp
1>C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\ppltasks.h(3915): error C2280: 'main::<lambda_4cfd68783a509ae5b42279aea526fb57>::<lambda_4cfd68783a509ae5b42279aea526fb57>(const main::<lambda_4cfd68783a509ae5b42279aea526fb57> &)': attempting to reference a deleted function
1> Source.cpp(21): note: see declaration of 'main::<lambda_4cfd68783a509ae5b42279aea526fb57>::<lambda_4cfd68783a509ae5b42279aea526fb57>'
1> Source.cpp(21): note: see reference to function template instantiation 'Concurrency::task<void>::task<main::<lambda_4cfd68783a509ae5b42279aea526fb57>>(_Ty,const Concurrency::task_options &)' being compiled
1> with
1> [
1> _Ty=main::<lambda_4cfd68783a509ae5b42279aea526fb57>
1> ]
the key point to note is that the compiler is failing because the copy constructor of the lambda is deleted; if it existed, it would have the signature main::lambda::lambda(const main::lambda&). Inside the constructor for tasks, we perform at least one copy which requires a copy constructor for the argument type.

The reason the copy constructor is deleted for the lambda is the same as why you have to perform a move-capture here: because As can't be copied, the lambdas capturing them can't be copied. This could be potentially fixed by using a move inside the constructor instead of a copy, but in the current code you can't build tasks out of non-copyable function objects.

An immediate (and heavyhanded) solution is to shove your A inside a shared_ptr. Alternatively, if you have control over the definition of A, you could try to rewrite it to be copyable.
Jan 12, 2015 at 2:57 AM
Sorry for late response. I didn't get a notification about any reply, so I just kind of forgot about this.

So casablanca does not (yet?) support capturing lambdas into tasks that can't be copied. That makes sense. Any plans on trying to work around or fix this some time in the future?

I actually fixed this in my real code by making A copyable (it's built on casablanca types anyway, which uses weird reference counted semantics when copying them).
Jan 12, 2015 at 7:07 PM
Unfortunately this is tied to the behavior inside the underlying "Parallel Patterns Library" implementation. Calling .get() on a task will return a copy of the object, making it safe to chain multiple continuations off of the same task as they cannot interact with each other.

This feature, however, means that non-copiable objects can't be used in tasks.

Most types inside cpprest use shared_ptr semantics to make their use easier: they're movable, copiable, and you don't have to worry about them being destroyed before completing.