Verschieben Sie die Erfassung in Lambda


157

Wie kann ich in einem C ++ 11-Lambda durch Verschieben (auch als r-Wert-Referenz bezeichnet) erfassen?

Ich versuche so etwas zu schreiben:

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

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

Antworten:


163

Verallgemeinerte Lambda-Erfassung in C ++ 14

In C ++ 14 haben wir die sogenannte generalisierte Lambda-Erfassung . Dies ermöglicht die Bewegungserfassung. Folgendes ist in C ++ 14 legal:

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 folgendermaßen initialisiert werden können:

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

In C ++ 11 ist dies noch nicht möglich, aber mit einigen Tricks, die Hilfstypen beinhalten. Glücklicherweise implementiert der Clang 3.4-Compiler diese großartige Funktion bereits. Der Compiler wird im Dezember 2013 oder Januar 2014 veröffentlicht, wenn das aktuelle Release-Tempo beibehalten wird.

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

Eine Problemumgehung für die Bewegungserfassung

Hier ist eine Implementierung einer Hilfsfunktion, make_rrefdie 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 lambdaes kopierbar ist und beim Kopieren die Behauptung im Kopierkonstruktor von rref_implfehlschlägt, was zu einem Laufzeitfehler führt. Das Folgende ist möglicherweise eine bessere und noch allgemeinere Lösung, da der Compiler den Fehler abfängt.

Emulieren der verallgemeinerten Lambda-Erfassung in C ++ 11

Hier ist eine weitere Idee, wie die generalisierte Lambda-Erfassung implementiert werden kann. Die Verwendung der Funktion capture()(deren Implementierung weiter unten zu finden ist) ist wie folgt:

#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 lambdaist ein Funktor Objekt (fast eine echte Lambda) , die eingefangen hat , std::move(p)wie es übergeben wird capture(). Das zweite Argument von captureist ein Lambda, das die erfasste Variable als Argument verwendet. Wenn lambdaes als Funktionsobjekt verwendet wird, werden alle an es übergebenen Argumente als Argumente nach der erfassten Variablen an das interne Lambda weitergeleitet. (In unserem Fall sind keine weiteren Argumente zu übermitteln). Im Wesentlichen geschieht das Gleiche wie in der vorherigen Lösung. So capturewird 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 ebenfalls sauberer, da sie das Kopieren des Lambda deaktiviert, wenn der erfasste Typ nicht kopierbar ist. In der ersten Lösung kann das nur zur Laufzeit mit einem überprüft werden assert().


Ich habe dies lange mit G ++ - 4.8 -std = c ++ 11 verwendet, und ich dachte, es ist eine C ++ 11-Funktion. Jetzt bin ich daran gewöhnt und merkte plötzlich, dass es sich um eine C ++ 14-Funktion handelt ... Was soll ich tun !!
RnMss

@RnMss Welche Funktion meinst du? Verallgemeinerte Lambda-Erfassung?
Ralph Tandetzky

@RalphTandetzky Ich denke schon, ich habe es gerade überprüft und die mit XCode gebündelte Version von clang scheint es auch zu unterstützen! Es wird eine Warnung ausgegeben, dass es sich um eine C ++ 1y-Erweiterung handelt, die jedoch funktioniert.
Christopher Tarquini

@RnMss Verwenden Sie entweder a moveCapture Wrapper, um sie als Argumente zu übergeben (diese Methode wird oben und in Capn'Proto, einer Bibliothek des Erstellers von Protobuffs, verwendet), oder akzeptieren Sie einfach, dass Sie Compiler benötigen, die dies unterstützen: P
Christopher Tarquini

9
Nein, es ist eigentlich nicht dasselbe. Beispiel: Sie möchten einen Thread mit einem Lambda erzeugen, der den eindeutigen Zeiger bewegt. Die Spawning-Funktion kann möglicherweise zurückkehren und der unique_ptr verlässt den Gültigkeitsbereich, bevor der Funktor ausgeführt wird. Daher haben Sie einen baumelnden Verweis auf ein unique_ptr. Willkommen im Land des undefinierten Verhaltens.
Ralph Tandetzky

76

Sie können auch std::bindFolgendes verwenden unique_ptr:

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

2
Vielen Dank für die Veröffentlichung!
mmocny

4
Haben Sie überprüft, ob der Code kompiliert wird? Das sieht für mich nicht so aus, da erstens der Variablenname fehlt und zweitens eine unique_ptrrvalue-Referenz nicht an eine binden kann int *.
Ralph Tandetzky

7
Beachten Sie, dass in Visual Studio 2013 beim Konvertieren einer std :: -Bindung in eine std :: -Funktion weiterhin alle gebundenen Variablen kopiert werden ( myPointerin diesem Fall). Daher wird der obige Code in VS2013 nicht kompiliert. In GCC 4.8 funktioniert es jedoch einwandfrei.
Alan

22

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

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 dabei ist, dass wir Ihr Nur-Verschieben-Objekt nicht in der Erfassungsliste erfassen, sondern es zu einem Argument machen und dann eine Teilanwendung über verwenden std::bind, um es verschwinden zu lassen. Beachten Sie, dass das Lambda es als Referenz verwendet , da es tatsächlich im Bindungsobjekt gespeichert ist. Ich habe auch Code hinzugefügt, der in das tatsächlich bewegliche Objekt schreibt , da Sie dies möglicherweise tun möchten.

In C ++ 14 können Sie mit diesem Code die verallgemeinerte Lambda-Erfassung verwenden, um dieselben 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});
};

Aber dieser Code kauft Ihnen nichts, was Sie in C ++ 11 nicht über hatten std::bind. (In einigen Situationen ist die allgemeine Lambda-Erfassung leistungsfähiger, in diesem Fall jedoch nicht.)

Jetzt gibt es nur noch ein Problem. Sie wollten diese Funktion in eine setzen std::function, aber diese Klasse erfordert, dass die Funktion CopyConstructible ist , aber nicht, es ist nur MoveConstructible, weil sie eine speichert, std::unique_ptrdie nicht CopyConstructible ist .

Sie müssen das Problem mit der Wrapper-Klasse und einer anderen Indirektionsebene umgehen, aber vielleicht brauchen Sie das überhaupt nicht std::function. Abhängig von Ihren Anforderungen können Sie möglicherweise verwenden std::packaged_task; Es würde den gleichen Job machen wie std::function, aber es erfordert nicht, dass die Funktion kopierbar ist, sondern nur beweglich (in ähnlicher Weise std::packaged_tasknur beweglich). Der Nachteil ist, dass Sie es nur einmal aufrufen können, da es in Verbindung mit std :: future verwendet werden soll.

Hier ist ein kurzes Programm, das all 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 das oben genannte Programm auf Coliru gestellt , damit Sie mit dem Code spielen können.

Hier sind einige typische Ausgaben ...

- &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-Speicherorte wiederverwendet werden, was zeigt, dass der std::unique_ptrordnungsgemäß funktioniert. Sie sehen auch, wie sich die Funktion selbst bewegt, wenn wir sie in einem Wrapper aufbewahren, dem wir den Feed zuführen std::function.

Wenn wir zu using wechseln, std::packaged_taskwird es 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, befindet sie sich innerhalb std::packaged_taskdes Stapels.

Hoffe das hilft!


4

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

Um ehrlich zu sein, mag ich keine der veröffentlichten Lösungen wirklich. Ich bin mir sicher, dass sie funktionieren werden, aber sie erfordern eine Menge zusätzlicher Dinge und / oder kryptischer std::bindSyntax ... und ich denke nicht, dass sich die Mühe für eine solche temporäre Lösung lohnt, die beim Upgrade auf c ++ ohnehin überarbeitet wird = 14. Ich denke, die beste Lösung besteht darin, die Verschiebungserfassung für c ++ 11 vollständig zu vermeiden.

Normalerweise ist die einfachste und am besten lesbare Lösung die Verwendung std::shared_ptr, die kopierbar sind und so das Verschieben vollständig vermeidbar ist. 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 auftritt, ist dies wirklich obligatorisch move den Zeiger (z. B. wenn Sie einen Zeiger aufgrund der langen Löschdauer explizit in einem separaten Thread löschen möchten oder die Leistung absolut entscheidend ist), ist dies so ziemlich der einzige Fall, in dem ich ihn noch verwende Rohzeiger in c ++ 11. Diese sind natürlich auch kopierbar.

Normalerweise markiere ich diese seltenen Fälle mit einem, //FIXME:um sicherzustellen, dass sie nach dem Upgrade auf c ++ 14 überarbeitet werden.

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

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


Vielen Dank, dass die Verwendung von C ++ 14 und keiner der anderen Lösungen gut war. Rettete meinen Tag!
Yoav Sternberg

1

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 explizit erklärt, 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 move_with_copy_ctorKlasse und ihre Hilfsfunktion funktionieren make_movable()mit jedem beweglichen, aber nicht kopierbaren Objekt. Verwenden Sie die Taste, um Zugriff auf das umschlossene Objekt zu erhalten operator()().

Erwartete Ausgabe:

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

Nun, die Zeigeradresse kann variieren. ;)

Demo


1

Dies scheint auf 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;
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.