Déplacer la capture en lambda


157

Comment capturer par déplacement (également appelé référence rvalue) dans un lambda C ++ 11?

J'essaye d'écrire quelque chose comme ceci:

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

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

Réponses:


163

Capture lambda généralisée en C ++ 14

En C ++ 14, nous aurons la soi-disant capture lambda généralisée . Cela permet la capture de mouvement. Ce qui suit sera le code juridique 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 ); } ); 

Mais c'est beaucoup plus général dans le sens où les variables capturées peuvent être initialisées avec quelque chose comme ceci:

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

En C ++ 11, ce n'est pas encore possible, mais avec quelques astuces qui impliquent des types d'assistance. Heureusement, le compilateur Clang 3.4 implémente déjà cette fonctionnalité géniale. Le compilateur sortira en décembre 2013 ou janvier 2014, si le rythme de publication récent est maintenu.

MISE À JOUR: Le compilateur Clang 3.4 a été publié le 6 janvier 2014 avec ladite fonctionnalité.

Une solution de contournement pour la capture de mouvement

Voici une implémentation d'une fonction d'assistance make_rrefqui aide à la capture de mouvement artificiel

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

Et voici un cas de test pour cette fonction qui a fonctionné avec succès sur mon 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() );
}

L'inconvénient ici est qu'il lambdaest copiable et lorsqu'il est copié, l'assertion dans le constructeur de copie derref_impl échoue conduisant à un bogue d'exécution. Ce qui suit pourrait être une solution meilleure et encore plus générique car le compilateur détectera l'erreur.

Émulation de la capture lambda généralisée en C ++ 11

Voici une autre idée, sur la façon d'implémenter la capture lambda généralisée. L'utilisation de la fonction capture()(dont l'implémentation se trouve plus bas) est la suivante:

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

Voici lambdaun objet foncteur (presque un vrai lambda) qui a capturé std::move(p)comme il est passé à capture(). Le deuxième argument de captureest un lambda qui prend la variable capturée comme argument. Quand lambdaest utilisé comme objet fonction, tous les arguments qui lui sont passés seront transmis au lambda interne en tant qu'arguments après la variable capturée. (Dans notre cas, il n'y a plus d'arguments à transmettre). Essentiellement, la même chose que dans la solution précédente se produit. Voici comment captureest implémenté:

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

Cette deuxième solution est également plus propre, car elle désactive la copie du lambda, si le type capturé n'est pas copiable. Dans la première solution, qui ne peut être vérifiée qu'au moment de l'exécution avec un fichier assert().


J'utilise depuis longtemps avec G ++ - 4.8 -std = c ++ 11, et je pensais que c'était une fonctionnalité C ++ 11. Maintenant, j'ai l'habitude de l'utiliser et j'ai soudainement réalisé que c'était une fonctionnalité C ++ 14 ... Que dois-je faire !!
RnMss

@RnMss De quelle fonctionnalité parlez-vous? Capture lambda généralisée?
Ralph Tandetzky

@RalphTandetzky Je pense que oui, je viens de vérifier et la version de clang fournie avec XCode semble le supporter aussi! Cela donne un avertissement qu'il s'agit d'une extension C ++ 1y mais cela fonctionne.
Christopher Tarquini

@RnMss Soit utiliser un moveCapturewrapper pour les passer en arguments (cette méthode est utilisée ci-dessus et dans Capn'Proto, une bibliothèque du créateur de protobuffs) soit simplement accepter que vous ayez besoin de compilateurs qui le supportent: P
Christopher Tarquini

9
Non, ce n'est pas la même chose. Exemple: vous voulez générer un thread avec un lambda qui capture le pointeur unique. La fonction spawning peut éventuellement retourner et le unique_ptr sortir de la portée avant que le foncteur ne soit exécuté. Par conséquent, vous avez une référence pendante à un unique_ptr. Bienvenue à undefined-behavior-land.
Ralph Tandetzky

76

Vous pouvez également utiliser std::bindpour capturer unique_ptr:

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

2
Merci d'avoir posté ceci!
mmocny

4
Avez-vous vérifié si le code se compile? Cela ne me semble pas être le cas, car d'une part le nom de la variable est manquant et d'autre part une unique_ptrréférence rvalue ne peut pas se lier à un int *.
Ralph Tandetzky

7
Notez que dans Visual Studio 2013, la conversion d'un std :: bind en une fonction std :: entraîne toujours la copie de toutes les variables liées ( myPointerdans ce cas). Par conséquent, le code ci-dessus ne se compile pas dans VS2013. Cela fonctionne bien dans GCC 4.8.
Alan

22

Vous pouvez réaliser la plupart de ce que vous voulez en utilisant std::bind, comme ceci:

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

L'astuce ici est qu'au lieu de capturer votre objet de mouvement uniquement dans la liste des captures, nous en faisons un argument puis utilisons une application partielle via std::bindpour le faire disparaître. Notez que le lambda le prend par référence , car il est en fait stocké dans l'objet de liaison. J'ai également ajouté du code qui écrit sur l'objet mobile réel, car c'est quelque chose que vous voudrez peut-être faire.

En C ++ 14, vous pouvez utiliser la capture lambda généralisée pour atteindre les mêmes fins, avec ce code:

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

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

Mais ce code ne vous achète rien que vous n'aviez pas en C ++ 11 via std::bind . (Dans certaines situations, la capture lambda généralisée est plus puissante, mais pas dans ce cas.)

Maintenant, il n'y a qu'un seul problème; vous vouliez mettre cette fonction dans a std::function, mais cette classe nécessite que la fonction soit CopyConstructible , mais ce n'est pas le cas, c'est uniquement MoveConstructible car elle stocke un std::unique_ptrqui n'est pas CopyConstructible .

Vous devez contourner le problème avec la classe wrapper et un autre niveau d'indirection, mais peut-être n'en avez-vous pas besoin std::functiondu tout. Selon vos besoins, vous pourrez peut-être utiliserstd::packaged_task ; il ferait le même travail que std::function, mais il ne nécessite pas que la fonction soit copiable, seulement déplaçable (de même, std::packaged_taskn'est que déplaçable). L'inconvénient est que, comme il est destiné à être utilisé avec std :: future, vous ne pouvez l'appeler qu'une seule fois.

Voici un petit programme qui montre tous ces concepts.

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

J'ai mis un programme ci-dessus sur Coliru , afin que vous puissiez exécuter et jouer avec le code.

Voici une sortie typique ...

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

Vous voyez les emplacements de tas réutilisés, ce qui montre que le std::unique_ptr fonctionne correctement. Vous voyez également la fonction elle-même se déplacer lorsque nous la stockons dans un wrapper auquel nous alimentons std::function.

Si nous passons à l'utilisation std::packaged_task , la dernière partie devient

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

donc nous voyons que la fonction a été déplacée, mais plutôt que d'être déplacée sur le tas, c'est à l'intérieur du std::packaged_taskqui est sur la pile.

J'espère que cela t'aides!


4

En retard, mais comme certaines personnes (dont moi) sont toujours bloquées sur c ++ 11:

Pour être honnête, je n'aime vraiment aucune des solutions publiées. Je suis sûr qu'ils fonctionneront, mais ils nécessitent beaucoup de choses supplémentaires et / ou cryptiquesstd::bind syntaxe ... et je ne pense pas que cela en vaille la peine pour une telle solution temporaire qui sera de toute façon refactorisée lors de la mise à niveau vers c ++> = 14. Je pense donc que la meilleure solution est d'éviter complètement la capture de mouvement pour C ++ 11.

Habituellement, la solution la plus simple et la mieux lisible est d'utiliser std::shared_ptr, qui sont copiables et donc le déplacement est complètement évitable. L'inconvénient est que c'est un peu moins efficace, mais dans de nombreux cas, l'efficacité n'est pas si 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 le cas très rare se produit, il est vraiment obligatoire de move le pointeur (par exemple, vous voulez supprimer explicitement un pointeur dans un thread séparé en raison d'une longue durée de suppression, ou les performances sont absolument cruciales), c'est à peu près le seul cas où j'utilise encore pointeurs bruts en c ++ 11. Ceux-ci sont bien sûr également copiables.

Habituellement, je marque ces rares cas avec un //FIXME:pour m'assurer qu'il est remanié une fois la mise à niveau vers 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;

Oui, les pointeurs bruts sont assez mal vus ces jours-ci (et non sans raison), mais je pense vraiment que dans ces cas rares (et temporaires!), Ils sont la meilleure solution.


Merci, l'utilisation de C ++ 14 et aucune des autres solutions était bonne. J'ai sauvé ma journée!
Yoav Sternberg

1

Je regardais ces réponses, mais j'ai trouvé que bind était difficile à lire et à comprendre. Donc, ce que j'ai fait, c'est créer une classe qui se déplace sur copie à la place. De cette façon, il est explicite avec ce qu'il fait.

#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 et sa fonction d'assistance fonctionneront make_movable()avec n'importe quel objet déplaçable mais non copiable. Pour accéder à l'objet encapsulé, utilisez le operator()().

Production attendue:

valeur: 1
objet toujours pas supprimé
valeur: 1
Détruire 000000DFDD172280
l'objet a été supprimé

Eh bien, l'adresse du pointeur peut varier. ;)

Demo


1

Cela semble fonctionner sur 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;
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.