Sposta la cattura in lambda


157

Come acquisisco per spostamento (noto anche come riferimento rvalue) in un lambda C ++ 11?

Sto cercando di scrivere qualcosa del genere:

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

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

Risposte:


163

Acquisizione lambda generalizzata in C ++ 14

In C ++ 14 avremo la cosiddetta cattura lambda generalizzata . Ciò consente l'acquisizione di mosse. Quanto segue sarà il codice legale in 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 ); } ); 

Ma è molto più generale nel senso che le variabili catturate possono essere inizializzate con qualcosa del genere:

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

In C ++ 11 questo non è ancora possibile, ma con alcuni trucchi che coinvolgono i tipi di aiuto. Fortunatamente, il compilatore Clang 3.4 implementa già questa fantastica funzionalità. Il compilatore verrà rilasciato a dicembre 2013 o gennaio 2014, se il ritmo di rilascio recente verrà mantenuto.

AGGIORNAMENTO: il compilatore Clang 3.4 è stato rilasciato il 6 gennaio 2014 con la suddetta funzionalità.

Una soluzione alternativa per l'acquisizione di mosse

Ecco un'implementazione di una funzione di aiuto make_rrefche aiuta con l'acquisizione artificiale delle mosse

#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) };
}

Ed ecco un caso di prova per quella funzione eseguita correttamente sul mio 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() );
}

Lo svantaggio qui è che lambdaè copiabile e quando viene copiata l'asserzione nel costruttore della copia di rref_implfail che porta a un bug di runtime. Di seguito potrebbe essere una soluzione migliore e ancora più generica perché il compilatore rileverà l'errore.

Emulazione della cattura lambda generalizzata in C ++ 11

Ecco un'altra idea, su come implementare la cattura lambda generalizzata. L'uso della funzione capture()(la cui implementazione si trova più in basso) è il seguente:

#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() );
}

Ecco lambdaun oggetto functor (quasi un vero lambda) che è stato catturato std::move(p)mentre viene passato capture(). Il secondo argomento di captureè un lambda che prende la variabile catturata come argomento. Quando lambdaviene utilizzato come oggetto funzione, tutti gli argomenti passati ad esso verranno inoltrati al lambda interno come argomenti dopo la variabile acquisita. (Nel nostro caso non ci sono ulteriori argomenti da inoltrare). In sostanza, accade lo stesso della soluzione precedente. Ecco come captureviene implementato:

#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) );
}

Questa seconda soluzione è anche più pulita, perché disabilita la copia della lambda, se il tipo acquisito non è copiabile. Nella prima soluzione che può essere verificata solo in fase di esecuzione con un assert().


Ho usato così a lungo con G ++ - 4.8 -std = c ++ 11 e ho pensato che fosse una funzionalità C ++ 11. Ora sono abituato a usare questo e improvvisamente ho capito che è una funzione C ++ 14 ... Cosa dovrei fare !!
RssMs

@RnMss Quale caratteristica intendi? Cattura lambda generalizzata?
Ralph Tandetzky,

@RalphTandetzky Penso di sì, ho appena controllato e anche la versione di clang in bundle con XCode sembra supportarla! Segnala che si tratta di un'estensione C ++ 1y ma funziona.
Christopher Tarquini,

@RnMss O usa un moveCapturewrapper per passarli come argomenti (questo metodo è usato sopra e in Capn'Proto, una libreria del creatore di protobuff) o accetta semplicemente di aver bisogno di compilatori che lo supportano: P
Christopher Tarquini,

9
No, in realtà non è la stessa cosa. Esempio: si desidera generare un thread con un lambda che sposta-cattura il puntatore univoco. È possibile che la funzione di spawn ritorni e unique_ptr esca dall'ambito prima che il functor venga eseguito. Pertanto, hai un riferimento penzolante a unique_ptr. Benvenuti nella terra del comportamento indefinito.
Ralph Tandetzky,

76

È inoltre possibile utilizzare std::bindper acquisire unique_ptr:

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

2
Grazie per aver postato questo!
mmocny,

4
Hai controllato, se il codice viene compilato? A me non sembra così, dal momento che in primo luogo manca il nome della variabile e in secondo luogo un unique_ptrriferimento al valore non può essere associato a un int *.
Ralph Tandetzky,

7
Si noti che in Visual Studio 2013, la conversione di una funzione std :: bind in una funzione std :: comporta comunque la copia di tutte le variabili associate ( myPointerin questo caso). Pertanto, il codice sopra riportato non viene compilato in VS2013. Si svegliava bene in GCC 4.8 però.
Alan,

22

È possibile ottenere la maggior parte di ciò che si desidera utilizzare std::bind, in questo modo:

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));

Il trucco qui è che invece di catturare il tuo oggetto solo spostamento nell'elenco catture, lo trasformiamo in un argomento e quindi usiamo un'applicazione parziale std::bindper farlo svanire. Nota che lambda lo prende per riferimento , perché è effettivamente memorizzato nell'oggetto bind. Ho anche aggiunto del codice che scrive sull'oggetto mobile reale, perché è qualcosa che potresti voler fare.

In C ++ 14, puoi usare l'acquisizione lambda generalizzata per ottenere gli stessi fini, con questo codice:

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

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

Ma questo codice non ti compra nulla che non avessi in C ++ 11 via std::bind. (Ci sono alcune situazioni in cui la cattura lambda generalizzata è più potente, ma non in questo caso.)

Ora c'è solo un problema; volevi mettere questa funzione in a std::function, ma quella classe richiede che la funzione sia CopyConstructible , ma non lo è, è solo MoveConstructible perché memorizza un oggetto std::unique_ptrche non è CopyConstructible .

È necessario aggirare il problema con la classe wrapper e un altro livello di riferimento indiretto, ma forse non è necessario std::functionaffatto. A seconda delle esigenze, potresti essere in grado di utilizzare std::packaged_task; farebbe lo stesso lavoro di std::function, ma non richiede che la funzione sia copiabile, solo mobile (allo stesso modo, std::packaged_taskè solo mobile). Il rovescio della medaglia è che, poiché è destinato ad essere utilizzato in combinazione con std :: future, è possibile chiamarlo solo una volta.

Ecco un breve programma che mostra tutti questi concetti.

#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
}

Ho messo il programma sopra su Coliru , quindi puoi correre e giocare con il codice.

Ecco alcuni output tipici ...

- &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

Puoi vedere le posizioni di heap riutilizzate, dimostrando che std::unique_ptrfunziona correttamente. Vedi anche la funzione stessa spostarsi quando la mettiamo in un wrapper a cui ci nutriamo std::function.

Se passiamo all'utilizzo std::packaged_task, diventa l'ultima parte

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

quindi vediamo che la funzione è stata spostata, ma invece di spostarsi nell'heap, è all'interno di quello std::packaged_taskche è nello stack.

Spero che questo ti aiuti!


4

In ritardo, ma poiché alcune persone (incluso me) sono ancora bloccate su c ++ 11:

Ad essere sincero, non mi piace molto nessuna delle soluzioni pubblicate. Sono sicuro che funzioneranno, ma richiedono un sacco di roba aggiuntiva e / o std::bindsintassi criptica ... e non credo che valga la pena fare una soluzione così temporanea che verrà comunque rifattorizzata durante l'aggiornamento a c ++> = 14. Quindi penso che la soluzione migliore sia quella di evitare di spostare completamente l'acquisizione per c ++ 11.

Di solito è la soluzione più semplice e leggibile da usare std::shared_ptr, che è copiabile e quindi la mossa è completamente evitabile. L'unico inconveniente è che è un po 'meno efficiente, ma in molti casi l'efficienza non è così 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.

.

Se si verifica il caso molto raro, che è davvero obbligatorio per moveil puntatore (ad esempio, si desidera eliminare esplicitamente un puntatore in un thread separato a causa della lunga durata dell'eliminazione, o le prestazioni sono assolutamente cruciali), è praticamente l'unico caso in cui continuo a utilizzare puntatori non elaborati in c ++ 11. Questi sono ovviamente anche copiabili.

Di solito segnare questi rari casi con a //FIXME:per assicurarmi che venga refactored dopo l'aggiornamento 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ì, in questi giorni i puntatori grezzi sono piuttosto disapprovati (e non senza motivo), ma penso davvero che in questi rari (e temporanei!) Casi siano la soluzione migliore.


Grazie, l'uso di C ++ 14 e non delle altre soluzioni erano buone. Mi hai salvato la giornata!
Yoav Sternberg,

1

Stavo guardando queste risposte, ma ho trovato il legame difficile da leggere e capire. Quindi quello che ho fatto è stato creare una classe che si spostava invece su copia. In questo modo, è esplicito con ciò che sta facendo.

#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_ctorclasse e la sua funzione di supporto funzioneranno make_movable()con qualsiasi oggetto mobile ma non copiabile. Per accedere all'oggetto avvolto, utilizzare il operator()().

Uscita prevista:

valore: 1
l'oggetto non è stato ancora cancellato
valore: 1
Distruggere 000000DFDD172280
l'oggetto è stato cancellato

Bene, l'indirizzo del puntatore può variare. ;)

Demo


1

Questo sembra funzionare su 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;
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.