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

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.


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.