wake-up-neo.com

Bewegen Sie das Capture in Lambda

Wie fange ich durch Verschieben (auch als R-Wert-Referenz bezeichnet) in einem C++ 11-Lambda auf?

Ich versuche so etwas zu schreiben:

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

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

Generalisierte Lambda-Capture in C++ 14

In C++ 14 haben wir die sogenannte generalisierte Lambda-Erfassung . Dies ermöglicht die Bewegungserfassung. Das Folgende wird in C++ 14 legaler Code sein:

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

Es ist jedoch viel allgemeiner in dem Sinne, dass erfasste Variablen mit so etwas initialisiert werden können:

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

In C++ 11 ist dies noch nicht möglich, aber bei einigen Tricks, die Hilfstypen beinhalten. Glücklicherweise implementiert der Clang 3.4-Compiler dieses fantastische Feature bereits. Der Compiler wird im Dezember 2013 oder Januar 2014 veröffentlicht, wenn das Recent Release Pace beibehalten wird. 

UPDATE: Der Clang 3.4 Compiler wurde am 6. Januar 2014 mit der besagten Funktion veröffentlicht. 

Eine Problemumgehung für die Bewegungserfassung

Hier ist eine Implementierung einer Hilfsfunktion make_rref, die bei der Erfassung künstlicher Bewegungen hilft

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

Und hier ist ein Testfall für diese Funktion, die auf meinem gcc 4.7.3 erfolgreich ausgeführt wurde. 

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

Der Nachteil hierbei ist, dass lambda kopierbar ist und die Assertion im Kopierkonstruktor von rref_impl beim Kopieren fehlschlägt, was zu einem Laufzeitfehler führt. Das Folgende könnte eine bessere und noch allgemeinere Lösung sein, da der Compiler den Fehler abfängt. 

Emulieren der generalisierten Lambda-Capture in C++ 11

Hier ist eine weitere Idee, wie Sie die allgemeine Lambda-Erfassung implementieren können. Die Funktion capture() (deren Implementierung weiter unten zu finden ist) sieht folgendermaßen aus:

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

Hier ist lambda ein Funktionsobjekt (fast ein echtes Lambda), das std::move(p) erfasst hat, als es an capture() übergeben wird. Das zweite Argument von capture ist ein Lambda, das die erfasste Variable als Argument verwendet. Wenn lambda als Funktionsobjekt verwendet wird, werden alle an ihn übergebenen Argumente als Argumente nach der erfassten Variablen an das interne Lambda weitergeleitet. (In unserem Fall gibt es keine weiteren Argumente, die weitergeleitet werden müssen). Im Wesentlichen passiert dasselbe wie in der vorherigen Lösung. So wird capture implementiert:

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

Diese zweite Lösung ist auch sauberer, da das Kopieren des Lambda deaktiviert wird, wenn der erfasste Typ nicht kopierbar ist. In der ersten Lösung kann dies nur zur Laufzeit mit einer assert() überprüft werden.

118
Ralph Tandetzky

Sie können auch std::bind verwenden, um den unique_ptr zu erfassen:

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

Das meiste, was Sie wollen, können Sie mit std::bind wie folgt erreichen:

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

Der Trick hier besteht darin, dass Sie Ihr Move-Only-Objekt nicht in der Captures-Liste erfassen, sondern als Argument definieren und dann die Teilanwendung über std::bind verwenden, um es verschwinden zu lassen. Beachten Sie, dass das Lambda durch Referenz benötigt, da es tatsächlich im Bind-Objekt gespeichert ist. Ich habe auch Code hinzugefügt, der schreibt zum eigentlichen beweglichen Objekt, weil Sie dies vielleicht tun möchten.

In C++ 14 können Sie die generalisierte Lambda-Capture verwenden, um mit diesem Code die gleichen Ziele zu erreichen:

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

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

Dieser Code kauft Sie jedoch nicht über C++ 11 über std::bind . (Es gibt einige Situationen, in denen die allgemeine Lambda-Erfassung leistungsfähiger ist, in diesem Fall jedoch nicht.)

Jetzt gibt es nur ein Problem. Sie wollten diese Funktion in einen std::function einfügen, aber diese Klasse erfordert, dass die Funktion CopyConstructible ist. Dies ist jedoch nicht der Fall - es ist nur MoveConstructible , weil sie einen std::unique_ptr speichert, der nicht vorhanden ist CopyConstructible .

Sie können das Problem mit der Wrapper-Klasse und einer anderen Ebene der Dereferenzierung umgehen, benötigen aber möglicherweise gar keine std::function . Je nach Ihren Bedürfnissen können Sie std::packaged_task verwenden. es würde die gleiche Arbeit wie std::function ausführen, aber die Funktion muss nicht kopierbar sein, sondern nur beweglich sein (ähnlich ist std::packaged_task nur beweglich). Der Nachteil ist, dass es nur einmal aufgerufen werden kann, da es für die Verwendung mit std :: future vorgesehen ist.

Hier ist ein kurzes Programm, das alle diese Konzepte zeigt.

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

Ich habe ein Programm auf Coliru gestellt, damit Sie mit dem Code laufen und spielen können.

Hier ist eine typische Ausgabe ...

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

Sie sehen, dass Heap-Standorte wiederverwendet werden, was zeigt, dass der std::unique_ptr ordnungsgemäß funktioniert. Sie sehen auch, dass sich die Funktion selbst bewegt, wenn wir sie in einem Wrapper verstauen, den wir an std::function übergeben.

Wenn wir zu std::packaged_task wechseln, wird der letzte Teil

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

wir sehen also, dass die Funktion verschoben wurde, aber anstatt auf den Heap verschoben zu werden, ist sie im std::packaged_task, der sich auf dem Stapel befindet.

Hoffe das hilft!

18
Charphacy

Ich habe mir diese Antworten angesehen, aber ich fand es schwierig, sie zu lesen und zu verstehen. Also habe ich eine Klasse erstellt, die stattdessen kopiert wurde. Auf diese Weise wird deutlich, was es tut.

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

Die Klasse move_with_copy_ctor und ihre Hilfsfunktion make_movable() funktionieren mit jedem beweglichen, aber nicht kopierbaren Objekt. Verwenden Sie die Funktion operator()(), um Zugriff auf das umschlossene Objekt zu erhalten.

Erwartete Ausgabe:

 Wert: 1 
 Objekt noch nicht gelöscht 
 Wert: 1 
 Zerstören von 000000DFDD172280 
 Objekt wurde gelöscht 

Nun, die Zeigeradresse kann variieren. ;)

Demo

1
Adrian

Dies scheint bei gcc4.8 zu funktionieren

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

Spät, aber da einige Leute (einschließlich mir) immer noch auf C++ 11 stecken:

Um ehrlich zu sein, mag ich keine der veröffentlichten Lösungen. Ich bin mir sicher, dass sie funktionieren werden, aber sie benötigen eine Menge zusätzliches Material und/oder eine kryptische std::bind-Syntax ... und ich denke nicht, dass sich die Mühe für eine solche temporäre Lösung lohnt, die beim Upgrade auf C++ ohnehin umgestaltet wird > = 14. Ich denke, die beste Lösung ist es, das Capture für C++ 11 vollständig zu verschieben.

Normalerweise ist die einfachste und am besten lesbare Lösung die Verwendung von std::shared_ptr, die kopierbar sind und daher völlig umgangen werden können. Nachteil ist, dass es etwas weniger effizient ist, aber in vielen Fällen ist Effizienz nicht so wichtig.

// 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.

.

Wenn der sehr seltene Fall eintritt, dass es wirklich zwingend erforderlich ist, den Zeiger move zu verwenden (z. B. möchten Sie einen Zeiger in einem separaten Thread wegen längerer Löschdauer explizit löschen, oder die Leistung ist absolut entscheidend), ist dies der einzige Fall, in dem ich immer noch Verwenden Sie rohe Zeiger in C++ 11. Diese sind natürlich auch kopierbar.

Normalerweise markiere ich diese seltenen Fälle mit einem //FIXME:, um sicherzustellen, dass er nach einem Upgrade auf c ++ 14 umgestaltet wird.

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

Ja, rohe Hinweise sind in diesen Tagen ziemlich verpönt (und nicht ohne Grund), aber ich denke wirklich, dass sie in diesen seltenen (und vorübergehenden!) Fällen die beste Lösung sind.

0
user2328447