web-dev-qa-db-fra.com

Déplacer la capture en lambda

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

J'essaie d'écrire quelque chose comme ça:

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

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

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

En C++ 14, nous aurons le soi-disant capture lambda généralisée . Cela permet la capture de mouvements. Ce qui suit sera un code légal 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 il 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, cela n'est pas encore possible, mais avec quelques astuces impliquant des types d'assistance. Heureusement, le compilateur Clang 3.4 implémente déjà cette fonctionnalité géniale. Le compilateur sera publié en décembre 2013 ou janvier 2014, si le rythme de publication récent est conservé. 

UPDATE: Le compilateur Clang 3.4 a été publié le 6 janvier 2014 avec cette fonctionnalité. 

Solution de contournement pour la capture de mouvements

Voici une implémentation d'une fonction d'assistance make_rref qui aide à la capture de mouvements artificiels

#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 scénario 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 que lambda est copiable et que, lors de la copie, l'assertion dans le constructeur de copie de rref_impl échoue, entraînant 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. 

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

Voici une autre idée sur la mise en œuvre de la capture lambda généralisée. L’utilisation de la fonction capture() (dont l’implémentation se trouve plus loin) 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() );
}

Ici lambda est un objet foncteur (presque un vrai lambda) qui a capturé std::move(p) lorsqu’il est passé à capture(). Le deuxième argument de capture est un lambda qui prend la variable capturée comme argument. Lorsque lambda est utilisé en tant qu'objet fonction, tous les arguments qui lui sont transmis seront transmis à la lambda interne en tant qu'arguments après la variable capturée. (Dans notre cas, il n'y a pas d'autres arguments à transmettre). Essentiellement, la même chose que dans la solution précédente se produit. Voici comment capture est 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, celle-ci ne peut être vérifiée au moment de l'exécution qu'avec un assert().

118
Ralph Tandetzky

Vous pouvez également utiliser std::bind pour capturer le unique_ptr:

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

Vous pouvez réaliser presque tout 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 réservé au déplacement dans la liste de captures, nous en faisons un argument, puis nous utilisons une application partielle via std::bind pour le faire disparaître. Notez que le lambda le prend par référence, car il est réellement stocké dans l'objet bind. J'ai aussi ajouté du code qui écrit sur l'objet mobile réel, parce que 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 objectifs, 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 de ce 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 un std::function , mais cette classe nécessite que la fonction soit CopyConstructible , mais ce n'est pas le cas, c'est seulement MoveConstructible car il stocke un std::unique_ptr qui n'est pas CopyConstructible .

Vous pouvez contourner le problème avec la classe wrapper et un autre niveau d'indirection, mais vous n'avez peut-être pas besoin de std::function . Selon vos besoins, vous pourrez peut-être utiliser std::packaged_task ; il ferait le même travail que std::function , mais il n'exigera pas que la fonction soit copiable, seulement mobile (de même, std::packaged_task n'est mobile que). 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 programme court 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 le 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 pouvez voir les emplacements de tas réutilisés, ce qui montre que le std::unique_ptr fonctionne correctement. Vous voyez également que la fonction elle-même se déplace lorsque nous la stockons dans un wrapper dans lequel nous alimentons std::function.

Si nous passons à 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

nous voyons donc que la fonction a été déplacée, mais plutôt que d'être déplacé sur le tas, c'est dans le std::packaged_task qui se trouve sur la pile.

J'espère que cela t'aides!

18
Charphacy

Je cherchais ces réponses, mais j’ai trouvé difficile de lire et de comprendre. Donc ce que j'ai fait était de faire un cours qui se déplaçait à 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 classe move_with_copy_ctor et sa fonction d'assistance make_movable() fonctionneront avec tout objet déplaçable mais non copiable. Pour accéder à l'objet encapsulé, utilisez la operator()().

Production attendue:

 valeur: 1 
 objet toujours non supprimé 
 valeur: 1 
 en train de détruire 000000DFDD172280 
 un objet a été supprimé 
.

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

Demo

1
Adrian

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;
}
1
andoryu-

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 une syntaxe cryptique std::bind ... et je ne pense pas que cela vaille la peine de déployer des efforts pour une solution aussi temporaire qui sera de toute façon refactorisée lors de la mise à niveau vers c ++ > = 14. Je pense donc que la meilleure solution consiste à éviter complètement la capture de mouvement pour c ++ 11.

Généralement, la solution la plus simple et la plus lisible consiste à utiliser std::shared_ptr, qui est copiable, ce qui permet d'éviter tout déplacement. Inconvénient, 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 survient et qu'il est vraiment obligatoire de move le pointeur (par exemple, vous voulez supprimer explicitement un pointeur dans un thread séparé en raison de la durée de suppression prolongée ou si la performance est absolument cruciale), c'est à peu près le seul cas où utilisez des pointeurs bruts dans c ++ 11. Ce sont bien sûr aussi copiables.

En général, je marque ces rares cas avec un //FIXME: pour m'assurer qu'il est refactoré 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](){
   *myRawPointer = 4;
   // ...
   delete myRawPointer;
};

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

Oui, les indications brutes sont assez mal vues de nos jours (et pas sans raison), mais je pense vraiment que dans ces cas rares (et temporaires!), Elles constituent la meilleure solution.

0
user2328447