Mover captura en lambda


157

¿Cómo capturo por movimiento (también conocido como referencia de valor) en una lambda C ++ 11?

Estoy tratando de escribir algo como esto:

std::unique_ptr<int> myPointer(new int);

std::function<void(void)> example = [std::move(myPointer)]{
   *myPointer = 4;
};

Respuestas:


163

Captura generalizada de lambda en C ++ 14

En C ++ 14 tendremos la llamada captura lambda generalizada . Esto permite la captura de movimiento. El siguiente será un código legal en C ++ 14:

using namespace std;

// a unique_ptr is move-only
auto u = make_unique<some_type>( some, parameters );  

// move the unique_ptr into the lambda
go.run( [ u{move(u)} ] { do_something_with( u ); } ); 

Pero es mucho más general en el sentido de que las variables capturadas se pueden inicializar con algo así:

auto lambda = [value = 0] mutable { return ++value; };

En C ++ 11 esto aún no es posible, pero con algunos trucos que involucran tipos de ayuda. Afortunadamente, el compilador Clang 3.4 ya implementa esta increíble característica. El compilador se lanzará en diciembre de 2013 o enero de 2014, si se mantiene el ritmo de lanzamiento reciente .

ACTUALIZACIÓN: El compilador Clang 3.4 se lanzó el 6 de enero de 2014 con dicha función.

Una solución alternativa para la captura de movimientos

Aquí hay una implementación de una función auxiliar make_rrefque ayuda con la captura de movimiento artificial

#include <cassert>
#include <memory>
#include <utility>

template <typename T>
struct rref_impl
{
    rref_impl() = delete;
    rref_impl( T && x ) : x{std::move(x)} {}
    rref_impl( rref_impl & other )
        : x{std::move(other.x)}, isCopied{true}
    {
        assert( other.isCopied == false );
    }
    rref_impl( rref_impl && other )
        : x{std::move(other.x)}, isCopied{std::move(other.isCopied)}
    {
    }
    rref_impl & operator=( rref_impl other ) = delete;
    T && move()
    {
        return std::move(x);
    }

private:
    T x;
    bool isCopied = false;
};

template<typename T> rref_impl<T> make_rref( T && x )
{
    return rref_impl<T>{ std::move(x) };
}

Y aquí hay un caso de prueba para esa función que se ejecutó con éxito en mi gcc 4.7.3.

int main()
{
    std::unique_ptr<int> p{new int(0)};
    auto rref = make_rref( std::move(p) );
    auto lambda =
        [rref]() mutable -> std::unique_ptr<int> { return rref.move(); };
    assert(  lambda() );
    assert( !lambda() );
}

El inconveniente aquí es que lambdaes copiable y cuando se copia la afirmación en el constructor de copias de rref_implfallas que conducen a un error de tiempo de ejecución. Lo siguiente podría ser una solución mejor y aún más genérica porque el compilador detectará el error.

Emulación de captura lambda generalizada en C ++ 11

Aquí hay una idea más, sobre cómo implementar la captura lambda generalizada. El uso de la función capture()(cuya implementación se encuentra más abajo) es la siguiente:

#include <cassert>
#include <memory>

int main()
{
    std::unique_ptr<int> p{new int(0)};
    auto lambda = capture( std::move(p),
        []( std::unique_ptr<int> & p ) { return std::move(p); } );
    assert(  lambda() );
    assert( !lambda() );
}

Aquí lambdahay un objeto functor (casi una lambda real) que se ha capturado a std::move(p)medida que se pasa capture(). El segundo argumento de capturees una lambda que toma la variable capturada como argumento. Cuando lambdase usa como un objeto de función, todos los argumentos que se le pasan se enviarán a la lambda interna como argumentos después de la variable capturada. (En nuestro caso no hay más argumentos que enviar). Esencialmente, sucede lo mismo que en la solución anterior. Aquí se explica cómo capturese implementa:

#include <utility>

template <typename T, typename F>
class capture_impl
{
    T x;
    F f;
public:
    capture_impl( T && x, F && f )
        : x{std::forward<T>(x)}, f{std::forward<F>(f)}
    {}

    template <typename ...Ts> auto operator()( Ts&&...args )
        -> decltype(f( x, std::forward<Ts>(args)... ))
    {
        return f( x, std::forward<Ts>(args)... );
    }

    template <typename ...Ts> auto operator()( Ts&&...args ) const
        -> decltype(f( x, std::forward<Ts>(args)... ))
    {
        return f( x, std::forward<Ts>(args)... );
    }
};

template <typename T, typename F>
capture_impl<T,F> capture( T && x, F && f )
{
    return capture_impl<T,F>(
        std::forward<T>(x), std::forward<F>(f) );
}

Esta segunda solución también es más limpia, ya que deshabilita la copia de lambda, si el tipo capturado no se puede copiar. En la primera solución que solo se puede verificar en tiempo de ejecución con un assert().


He estado usando este tiempo con G ++ - 4.8 -std = c ++ 11, y pensé que es una característica de C ++ 11. Ahora estoy acostumbrado a usar esto y de repente me di cuenta de que es una característica de C ++ 14 ... ¡¿Qué debo hacer!
RnMss

@RnMss ¿Qué característica quieres decir? ¿Captura generalizada de lambda?
Ralph Tandetzky

@RalphTandetzky ¡Creo que sí, acabo de comprobar y la versión de clang incluida con XCode también parece ser compatible! Da una advertencia de que es una extensión C ++ 1y pero funciona.
Christopher Tarquini

@RnMss Utilice un moveCapturecontenedor para pasarlos como argumentos (este método se usa arriba y en Capn'Proto, una biblioteca del creador de protobuffs) o simplemente acepte que necesita compiladores que lo admitan: P
Christopher Tarquini

9
No, en realidad no es lo mismo. Ejemplo: Desea generar un hilo con una lambda que captura con movimiento el puntero único. La función de desove posiblemente puede regresar y el unique_ptr queda fuera de alcance antes de que se ejecute el functor. Por lo tanto, tiene una referencia pendiente a un unique_ptr. Bienvenido a undefined-behaviour-land.
Ralph Tandetzky

76

También puede usar std::bindpara capturar unique_ptr:

std::function<void()> f = std::bind(
                              [] (std::unique_ptr<int>& p) { *p=4; },
                              std::move(myPointer)
                          );

2
¡Gracias por publicar esto!
mmocny

44
¿Has comprobado si el código se compila? A mí no me parece así, ya que, en primer lugar, falta el nombre de la variable y, en segundo lugar, una unique_ptrreferencia de valor no puede unirse a un int *.
Ralph Tandetzky

77
Tenga en cuenta que en Visual Studio 2013, la conversión de una función std :: bind a std :: todavía da como resultado que copie todas las variables enlazadas ( myPointeren este caso). Por lo tanto, el código anterior no se compila en VS2013. Sin embargo, funciona bien en GCC 4.8.
Alan

22

Puede lograr la mayor parte de lo que quiere usar std::bind, así:

std::unique_ptr<int> myPointer(new int{42});

auto lambda = std::bind([](std::unique_ptr<int>& myPointerArg){
    *myPointerArg = 4;
     myPointerArg.reset(new int{237});
}, std::move(myPointer));

El truco aquí es que, en lugar de capturar su objeto de solo movimiento en la lista de capturas, lo convertimos en un argumento y luego usamos una aplicación parcial std::bindpara que desaparezca. Tenga en cuenta que el lambda lo toma por referencia , porque en realidad está almacenado en el objeto de enlace. También agregué código que escribe en el objeto móvil real, porque eso es algo que es posible que desee hacer.

En C ++ 14, puede usar la captura lambda generalizada para lograr los mismos fines, con este código:

std::unique_ptr<int> myPointer(new int{42});

auto lambda = [myPointerCapture = std::move(myPointer)]() mutable {
    *myPointerCapture = 56;
    myPointerCapture.reset(new int{237});
};

Pero este código no le compra nada que no tenía en C ++ 11 a través de std::bind. (Hay algunas situaciones en las que la captura lambda generalizada es más poderosa, pero no en este caso).

Ahora solo hay un problema; quería poner esta función en a std::function, pero esa clase requiere que la función sea CopyConstructible , pero no lo es, solo es MoveConstructible porque está almacenando una std::unique_ptrque no es CopyConstructible .

Debe solucionar el problema con la clase wrapper y otro nivel de indirección, pero tal vez no lo necesite std::functionen absoluto. Dependiendo de sus necesidades, puede usar std::packaged_task; haría el mismo trabajo que std::function, pero no requiere que la función sea copiable, solo móvil (de manera similar, std::packaged_tasksolo es móvil). La desventaja es que debido a que está destinado a usarse junto con std :: future, solo puede llamarlo una vez.

Aquí hay un breve programa que muestra todos estos conceptos.

#include <functional>   // for std::bind
#include <memory>       // for std::unique_ptr
#include <utility>      // for std::move
#include <future>       // for std::packaged_task
#include <iostream>     // printing
#include <type_traits>  // for std::result_of
#include <cstddef>

void showPtr(const char* name, const std::unique_ptr<size_t>& ptr)
{
    std::cout << "- &" << name << " = " << &ptr << ", " << name << ".get() = "
              << ptr.get();
    if (ptr)
        std::cout << ", *" << name << " = " << *ptr;
    std::cout << std::endl;
}

// If you must use std::function, but your function is MoveConstructable
// but not CopyConstructable, you can wrap it in a shared pointer.
template <typename F>
class shared_function : public std::shared_ptr<F> {
public:
    using std::shared_ptr<F>::shared_ptr;

    template <typename ...Args>
    auto operator()(Args&&...args) const
        -> typename std::result_of<F(Args...)>::type
    {
        return (*(this->get()))(std::forward<Args>(args)...);
    }
};

template <typename F>
shared_function<F> make_shared_fn(F&& f)
{
    return shared_function<F>{
        new typename std::remove_reference<F>::type{std::forward<F>(f)}};
}


int main()
{
    std::unique_ptr<size_t> myPointer(new size_t{42});
    showPtr("myPointer", myPointer);
    std::cout << "Creating lambda\n";

#if __cplusplus == 201103L // C++ 11

    // Use std::bind
    auto lambda = std::bind([](std::unique_ptr<size_t>& myPointerArg){
        showPtr("myPointerArg", myPointerArg);  
        *myPointerArg *= 56;                    // Reads our movable thing
        showPtr("myPointerArg", myPointerArg);
        myPointerArg.reset(new size_t{*myPointerArg * 237}); // Writes it
        showPtr("myPointerArg", myPointerArg);
    }, std::move(myPointer));

#elif __cplusplus > 201103L // C++14

    // Use generalized capture
    auto lambda = [myPointerCapture = std::move(myPointer)]() mutable {
        showPtr("myPointerCapture", myPointerCapture);
        *myPointerCapture *= 56;
        showPtr("myPointerCapture", myPointerCapture);
        myPointerCapture.reset(new size_t{*myPointerCapture * 237});
        showPtr("myPointerCapture", myPointerCapture);
    };

#else
    #error We need C++11
#endif

    showPtr("myPointer", myPointer);
    std::cout << "#1: lambda()\n";
    lambda();
    std::cout << "#2: lambda()\n";
    lambda();
    std::cout << "#3: lambda()\n";
    lambda();

#if ONLY_NEED_TO_CALL_ONCE
    // In some situations, std::packaged_task is an alternative to
    // std::function, e.g., if you only plan to call it once.  Otherwise
    // you need to write your own wrapper to handle move-only function.
    std::cout << "Moving to std::packaged_task\n";
    std::packaged_task<void()> f{std::move(lambda)};
    std::cout << "#4: f()\n";
    f();
#else
    // Otherwise, we need to turn our move-only function into one that can
    // be copied freely.  There is no guarantee that it'll only be copied
    // once, so we resort to using a shared pointer.
    std::cout << "Moving to std::function\n";
    std::function<void()> f{make_shared_fn(std::move(lambda))};
    std::cout << "#4: f()\n";
    f();
    std::cout << "#5: f()\n";
    f();
    std::cout << "#6: f()\n";
    f();
#endif
}

Puse el programa anterior en Coliru , para que pueda ejecutar y jugar con el código.

Aquí hay una salida típica ...

- &myPointer = 0xbfffe5c0, myPointer.get() = 0x7ae3cfd0, *myPointer = 42
Creating lambda
- &myPointer = 0xbfffe5c0, myPointer.get() = 0x0
#1: lambda()
- &myPointerArg = 0xbfffe5b4, myPointerArg.get() = 0x7ae3cfd0, *myPointerArg = 42
- &myPointerArg = 0xbfffe5b4, myPointerArg.get() = 0x7ae3cfd0, *myPointerArg = 2352
- &myPointerArg = 0xbfffe5b4, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 557424
#2: lambda()
- &myPointerArg = 0xbfffe5b4, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 557424
- &myPointerArg = 0xbfffe5b4, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 31215744
- &myPointerArg = 0xbfffe5b4, myPointerArg.get() = 0x7ae3cfd0, *myPointerArg = 3103164032
#3: lambda()
- &myPointerArg = 0xbfffe5b4, myPointerArg.get() = 0x7ae3cfd0, *myPointerArg = 3103164032
- &myPointerArg = 0xbfffe5b4, myPointerArg.get() = 0x7ae3cfd0, *myPointerArg = 1978493952
- &myPointerArg = 0xbfffe5b4, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 751631360
Moving to std::function
#4: f()
- &myPointerArg = 0x7ae3cfd4, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 751631360
- &myPointerArg = 0x7ae3cfd4, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 3436650496
- &myPointerArg = 0x7ae3cfd4, myPointerArg.get() = 0x7ae3d000, *myPointerArg = 2737348608
#5: f()
- &myPointerArg = 0x7ae3cfd4, myPointerArg.get() = 0x7ae3d000, *myPointerArg = 2737348608
- &myPointerArg = 0x7ae3cfd4, myPointerArg.get() = 0x7ae3d000, *myPointerArg = 2967666688
- &myPointerArg = 0x7ae3cfd4, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 3257335808
#6: f()
- &myPointerArg = 0x7ae3cfd4, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 3257335808
- &myPointerArg = 0x7ae3cfd4, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 2022178816
- &myPointerArg = 0x7ae3cfd4, myPointerArg.get() = 0x7ae3d000, *myPointerArg = 2515009536

Puede ver las ubicaciones del montón que se reutilizan, lo que muestra que std::unique_ptrfunciona correctamente. También verá que la función en sí se mueve cuando la guardamos en un contenedor al que alimentamos std::function.

Si cambiamos a usar std::packaged_task, la última parte se convierte en

Moving to std::packaged_task
#4: f()
- &myPointerArg = 0xbfffe590, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 751631360
- &myPointerArg = 0xbfffe590, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 3436650496
- &myPointerArg = 0xbfffe590, myPointerArg.get() = 0x7ae3d000, *myPointerArg = 2737348608

así que vemos que la función se ha movido, pero en lugar de moverse al montón, está dentro std::packaged_taskde la pila.

¡Espero que esto ayude!


4

Tarde, pero como algunas personas (incluyéndome a mí) todavía están atrapados en c ++ 11:

Para ser honesto, no me gusta ninguna de las soluciones publicadas. Estoy seguro de que funcionarán, pero requieren muchas cosas adicionales y / o std::bindsintaxis críptica ... y no creo que valga la pena el esfuerzo para una solución tan temporal que se refactorizará de todos modos al actualizar a c ++> = 14. Así que creo que la mejor solución es evitar la captura de movimientos para c ++ 11 por completo.

Por lo general, la solución más simple y mejor legible es usar std::shared_ptr, que son copiables y, por lo tanto, el movimiento es completamente evitable. Lo malo es que es un poco menos eficiente, pero en muchos casos la eficiencia no es tan importante.

// myPointer could be a parameter or something
std::unique_ptr<int> myPointer(new int);

// convert/move the unique ptr into a shared ptr
std::shared_ptr<int> mySharedPointer( std::move(myPointer) );

std::function<void(void)> = [mySharedPointer](){
   *mySharedPointer = 4;
};

// at end of scope the original mySharedPointer is destroyed,
// but the copy still lives in the lambda capture.

.

Si ocurre un caso muy raro, que es realmente obligatorio para moveel puntero (por ejemplo, si desea eliminar explícitamente un puntero en un hilo separado debido a la larga duración de eliminación, o el rendimiento es absolutamente crucial), ese es prácticamente el único caso en el que todavía uso punteros en bruto en c ++ 11. Estos, por supuesto, también se pueden copiar.

Por lo general, marco estos casos raros con un //FIXME:para garantizar que se refactorice una vez que se actualice a c ++ 14.

// myPointer could be a parameter or something
std::unique_ptr<int> myPointer(new int);

//FIXME:c++11 upgrade to new move capture on c++>=14

// "move" the pointer into a raw pointer
int* myRawPointer = myPointer.release();

// capture the raw pointer as a copy.
std::function<void(void)> = [myRawPointer](){
   std::unique_ptr<int> capturedPointer(myRawPointer);
   *capturedPointer = 4;
};

// ensure that the pointer's value is not accessible anymore after capturing
myRawPointer = nullptr;

Sí, los punteros en bruto están bastante mal vistos en estos días (y no sin razón), pero realmente creo que en estos casos raros (¡y temporales!) Son la mejor solución.


Gracias, usar C ++ 14 y ninguna de las otras soluciones fueron buenas. Me salvó el día!
Yoav Sternberg el

1

Estaba mirando estas respuestas, pero me pareció difícil leer y entender. Entonces, lo que hice fue crear una clase que se moviera en copia. De esta manera, es explícito con lo que está haciendo.

#include <iostream>
#include <memory>
#include <utility>
#include <type_traits>
#include <functional>

namespace detail
{
    enum selection_enabler { enabled };
}

#define ENABLE_IF(...) std::enable_if_t<(__VA_ARGS__), ::detail::selection_enabler> \
                          = ::detail::enabled

// This allows forwarding an object using the copy constructor
template <typename T>
struct move_with_copy_ctor
{
    // forwarding constructor
    template <typename T2
        // Disable constructor for it's own type, since it would
        // conflict with the copy constructor.
        , ENABLE_IF(
            !std::is_same<std::remove_reference_t<T2>, move_with_copy_ctor>::value
        )
    >
    move_with_copy_ctor(T2&& object)
        : wrapped_object(std::forward<T2>(object))
    {
    }

    // move object to wrapped_object
    move_with_copy_ctor(T&& object)
        : wrapped_object(std::move(object))
    {
    }

    // Copy constructor being used as move constructor.
    move_with_copy_ctor(move_with_copy_ctor const& object)
    {
        std::swap(wrapped_object, const_cast<move_with_copy_ctor&>(object).wrapped_object);
    }

    // access to wrapped object
    T& operator()() { return wrapped_object; }

private:
    T wrapped_object;
};


template <typename T>
move_with_copy_ctor<T> make_movable(T&& object)
{
    return{ std::forward<T>(object) };
}

auto fn1()
{
    std::unique_ptr<int, std::function<void(int*)>> x(new int(1)
                           , [](int * x)
                           {
                               std::cout << "Destroying " << x << std::endl;
                               delete x;
                           });
    return [y = make_movable(std::move(x))]() mutable {
        std::cout << "value: " << *y() << std::endl;
        return;
    };
}

int main()
{
    {
        auto x = fn1();
        x();
        std::cout << "object still not deleted\n";
        x();
    }
    std::cout << "object was deleted\n";
}

La move_with_copy_ctorclase y su función auxiliar funcionarán make_movable()con cualquier objeto móvil pero no copiable. Para obtener acceso al objeto envuelto, use el operator()().

Rendimiento esperado:

valor: 1
objeto aún no eliminado
valor: 1
Destruyendo 000000DFDD172280
objeto fue eliminado

Bueno, la dirección del puntero puede variar. ;)

Demo


1

Esto parece funcionar en gcc4.8

#include <memory>
#include <iostream>

struct Foo {};

void bar(std::unique_ptr<Foo> p) {
    std::cout << "bar\n";
}

int main() {
    std::unique_ptr<Foo> p(new Foo);
    auto f = [ptr = std::move(p)]() mutable {
        bar(std::move(ptr));
    };
    f();
    return 0;
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.