Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is a lambda not-movable if it captures a not-copiable object using std::move()?

Here is a sample code where I generate the error:

#include <functional>
using namespace std;

struct S {
    S() = default;
    S(const S&) = delete;
    S(S&&) = default;
    S& operator=(const S&) = delete;
    S& operator=(S&&) = delete;
};

template <typename F>
void post(F&& func)
{
    function<void()> f{forward<F>(func)};
}

int main()
{
    S s;
    post([s2 = move(s)] { });
}

Inside the lambda in main(), I capture the local variable s using std::move(). Before calling post(), s2 must have been move constructed successfully.

However, inside post(), f cannot be constructed with an rvalue reference to the type of this lambda.

If I remove, s2 = move(s), f can be constructed with this rvalue reference.

Why does adding s2 = move(s) render the lambda not-movable?

Here, is a link to try on coliru.

like image 741
Benji Mizrahi Avatar asked Dec 28 '17 07:12

Benji Mizrahi


People also ask

Can std :: function store lambda?

Instances of std::function can store, copy, and invoke any CopyConstructible Callable target -- functions, lambda expressions, bind expressions, or other function objects, as well as pointers to member functions and pointers to data members.

Is a lambda a std :: function?

Lambda's type One important thing to note is that a lambda is not a std::function .

What does it mean to lambda capture this?

The lambda is capturing an outside variable. A lambda is a syntax for creating a class. Capturing a variable means that variable is passed to the constructor for that class. A lambda can specify whether it's passed by reference or by value.

What is mutable lambda?

The mutable keyword is used so that the body of the lambda expression can modify its copies of the external variables x and y , which the lambda expression captures by value. Because the lambda expression captures the original variables x and y by value, their values remain 1 after the lambda executes.


2 Answers

Your lambda does not become non-movable by having a move capture. But it does become non-copyable, which is a problem.

std::function does not support moving the supplied functor into itself, it always does a copy. Non-copyable lambdas (and other callables) therefore cannot be used with std::function. The reason for this limitation is that the standard requires std::function to be copyable, which could not be achieved if it was initialised with a non-copyable callable.

like image 109
Angew is no longer proud of SO Avatar answered Nov 15 '22 16:11

Angew is no longer proud of SO


The problem is not with your lambda, but with your object being non-copyable, since std::function demands its objects to be copyable the compiler complains. You should almost always follow the rule-of-zero.

In general:

  • A lambda can be both copyable and moveable.
  • If the lambda has non-copyable captures, it makes the the lambda itself not copyable. Those objects can be wrapped in a smart_pointer which can be moved (or copied - shared_ptr) in the lambda capture though.
  • If there is no capture by value, the closure type (the lambda) is typically trivially copyable and trivially moveable.
  • The closure type would be trivially copyable and trivially moveable if-and-only-if all captured by value objects are of trivially copyable and trivially moveable non-const types (eg C-like types).
    • Otherwise if there is capture by value, the move constructors of the closure type would copy the captured-by-value objects.
  • If there is capture by value of a const object, any moves in the capture list would result in a copy.
  • If the lambda itself is const it is never moved, only copied, even to other const lambdas.

example:

#include <iostream>
#include <type_traits>

struct S
{
    S() {
        std::cout << "ctor" << '\n';
    }
    ~S() noexcept {
        std::cout << "dtor" << '\n';
    }
    S(const S&) { 
        std::cout << "copy ctor\n";
    }
    S(S&&) noexcept noexcept {
        std::cout << "move ctor\n";
    }
    S& operator= (const S&) {
        std::cout << "copy aop\n";
    }
    S& operator= (S&&) noexcept {
        std::cout << "move aop\n";
    }
};

template <typename T>
void getTraits()
{
    std::cout << std::boolalpha
        << "trivially_copy_constructible? "
        << std::is_trivially_copy_constructible_v<T>
        << "\ntrivially_move_constructible? "
        << std::is_trivially_move_constructible_v<T> << '\n' ;
}

int main()
{
    S s ;
    const S cs;
    {
        std::cout << "capture by value\n" ;
        //auto closure = [s = std::move(s)] {} ; // S::move construct               // 1.
        //auto closure = [cs = std::move(cs)] {} ; // S::copy construct             // 2.
        //const auto closure = [s = std::move(s)] {} ; // S::move construct         // 3.
        const auto closure = [cs = std::move(cs)] {} ; // S::copy construct         // 4.
        getTraits<decltype(closure)>();

        const auto copy_constructed = std::move(closure);
        const auto move_constructed = std::move(closure);
    }

    {
        std::cout << "\ncapture by reference\n";
        const auto closure = [&s] {};
        getTraits<decltype(closure)>();
    }
}

Uncomment 1, 2, 3, 4 one at a time and check the outputs. Remember std::move simply turns an object into an rvalue reference.

like image 40
KeyC0de Avatar answered Nov 15 '22 17:11

KeyC0de