Przenieś przechwytywanie w lambdzie


157

Jak przechwycić przez ruch (znany również jako odwołanie do wartości r) w lambdzie C ++ 11?

Próbuję napisać coś takiego:

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

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

Odpowiedzi:


163

Uogólnione przechwytywanie lambda w C ++ 14

W C ++ 14 będziemy mieli tzw. Uogólnione przechwytywanie lambda . Umożliwia to przechwytywanie ruchu. Poniższy kod będzie prawnym kodem w 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 ); } ); 

Ale jest o wiele bardziej ogólny w tym sensie, że przechwycone zmienne można zainicjować za pomocą czegoś takiego:

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

W C ++ 11 nie jest to jeszcze możliwe, ale z pewnymi sztuczkami, które obejmują typy pomocnicze. Na szczęście kompilator Clang 3.4 już implementuje tę niesamowitą funkcję. Kompilator zostanie wydany w grudniu 2013 lub styczniu 2014, jeśli utrzymane zostanie ostatnie tempo wydawania .

UPDATE: Clang 3.4 kompilator został wydany w dniu 6 stycznia 2014 roku ze wspomnianą funkcją.

Obejście problemu przechwytywania ruchu

Oto implementacja funkcji pomocniczej, make_rrefktóra pomaga w przechwytywaniu sztucznego ruchu

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

A oto przypadek testowy dla tej funkcji, która została pomyślnie uruchomiona na moim 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() );
}

Wadą jest to, że lambdamożna je skopiować, a po skopiowaniu twierdzenie w konstruktorze kopiującym rref_implkończy się niepowodzeniem prowadzącym do błędu w czasie wykonywania. Poniższe może być lepszym i jeszcze bardziej ogólnym rozwiązaniem, ponieważ kompilator wykryje błąd.

Emulowanie uogólnionego przechwytywania lambda w C ++ 11

Oto jeszcze jeden pomysł, jak zaimplementować uogólnione przechwytywanie lambda. Użycie funkcji capture()(której implementację opisano poniżej) jest następujące:

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

Oto lambdaobiekt funktora (prawie prawdziwa lambda), który został przechwycony std::move(p)podczas przekazywania capture(). Drugim argumentem capturejest lambda, która przyjmuje przechwyconą zmienną jako argument. Gdy lambdajest używany jako obiekt funkcji, wszystkie argumenty, które są do niego przekazywane, będą przekazywane do wewnętrznej lambdy jako argumenty po przechwyconej zmiennej. (W naszym przypadku nie ma dalszych argumentów do przekazania). Zasadniczo dzieje się to samo, co w poprzednim rozwiązaniu. Oto jak capturejest wdrażane:

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

To drugie rozwiązanie jest również czystsze, ponieważ wyłącza kopiowanie lambdy, jeśli przechwycony typ nie jest kopiowalny. W pierwszym rozwiązaniu, które można sprawdzić tylko w czasie wykonywania za pomocą pliku assert().


Używałem tego długo z G ++ - 4.8 -std = c ++ 11 i pomyślałem, że jest to funkcja C ++ 11. Teraz jestem przyzwyczajony do używania tego i nagle zdałem sobie sprawę, że jest to funkcja C ++ 14 ... Co powinienem zrobić !!
RnMss

@RnMss Którą funkcję masz na myśli? Uogólnione przechwytywanie lambda?
Ralph Tandetzky

@RalphTandetzky Myślę, że tak, właśnie sprawdziłem i wersja clang w pakiecie z XCode wydaje się również go obsługiwać! Daje ostrzeżenie, że jest to rozszerzenie C ++ 1y, ale działa.
Christopher Tarquini

@RnMss Albo użyj moveCaptureopakowania, aby przekazać je jako argumenty (ta metoda jest używana powyżej iw Capn'Proto, bibliotece twórcy protobuffów) lub po prostu zaakceptuj, że potrzebujesz kompilatorów, które ją obsługują: P
Christopher Tarquini

9
Nie, to właściwie nie to samo. Przykład: Chcesz odrodzić wątek z lambdą, która przesuwa-przechwytuje unikalny wskaźnik. Funkcja spawning może prawdopodobnie powrócić, a unique_ptr wyjść poza zakres, zanim funktor zostanie wykonany. Dlatego masz wiszące odwołanie do unique_ptr. Witamy w niezdefiniowanej krainie zachowań.
Ralph Tandetzky

76

Możesz również użyć std::binddo przechwycenia unique_ptr:

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

2
Dziękuję za wysłanie tego!
mmocny

4
Czy sprawdziłeś, czy kod się kompiluje? Dla mnie nie wygląda to tak, ponieważ po pierwsze brakuje nazwy zmiennej, a po drugie unique_ptrodwołanie do wartości r nie może być powiązane z plikiem int *.
Ralph Tandetzky

7
Należy zauważyć, że w programie Visual Studio 2013 konwertowanie std :: bind na std :: function nadal powoduje skopiowanie wszystkich powiązanych zmiennych ( myPointerw tym przypadku). Dlatego powyższy kod nie kompiluje się w VS2013. Jednak w GCC 4.8 działa dobrze.
Alan

22

Możesz osiągnąć większość tego, co chcesz, używając std::bind, na przykład:

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

Sztuczka polega na tym, że zamiast przechwytywać obiekt tylko do przenoszenia na liście przechwytywania, ustawiamy go jako argument, a następnie używamy częściowej aplikacji via, std::bindaby zniknął. Zauważ, że lambda przyjmuje ją przez odniesienie , ponieważ jest faktycznie przechowywana w obiekcie bind. Dodałem również kod, który pisze do rzeczywistego ruchomego obiektu, ponieważ jest to coś, co możesz chcieć zrobić.

W C ++ 14 możesz użyć uogólnionego przechwytywania lambda, aby osiągnąć te same cele, za pomocą tego kodu:

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

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

Ale ten kod nie kupi ci niczego, czego nie miałeś w C ++ 11 za pośrednictwem std::bind. (Są sytuacje, w których uogólnione przechwytywanie lambda jest bardziej wydajne, ale nie w tym przypadku).

Teraz jest tylko jeden problem; chciałeś umieścić tę funkcję w a std::function, ale ta klasa wymaga, aby funkcja była CopyConstructible , ale tak nie jest, to tylko MoveConstructible, ponieważ przechowuje element, std::unique_ptrktóry nie jest CopyConstructible .

Możesz obejść ten problem z klasą opakowującą i innym poziomem pośrednictwa, ale być może wcale nie potrzebujesz std::function. W zależności od potrzeb możesz skorzystać z std::packaged_task; wykonałby to samo zadanie std::function, ale nie wymaga, aby funkcja była kopiowalna, tylko przenośna (podobnie, std::packaged_taskjest tylko przenośna). Wadą jest to, że ponieważ jest przeznaczony do użycia w połączeniu ze std :: future, możesz go wywołać tylko raz.

Oto krótki program, który pokazuje wszystkie te koncepcje.

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

Umieściłem powyższy program na Coliru , więc możesz uruchomić i bawić się kodem.

Oto kilka typowych wyników ...

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

Możesz zobaczyć ponownie używane lokalizacje sterty, pokazując, że std::unique_ptr działa poprawnie. Widzisz również, że sama funkcja porusza się, gdy przechowujemy ją w opakowaniu, do którego dostarczamy std::function.

Jeśli przejdziemy na używanie std::packaged_task , stanie się ostatnia część

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

więc widzimy, że funkcja została przeniesiona, ale zamiast zostać przeniesiona na stertę, znajduje się wewnątrz std::packaged_taskstosu.

Mam nadzieję że to pomoże!


4

Późno, ale ponieważ niektórzy ludzie (w tym ja) wciąż tkwią w c ++ 11:

Szczerze mówiąc, nie podoba mi się żadne z opublikowanych rozwiązań. Jestem pewien, że zadziałają, ale wymagają mnóstwa dodatkowych rzeczy i / lub kryptograficznej std::bindskładni ... i nie sądzę, że warto się wysilić na takie tymczasowe rozwiązanie, które i tak zostanie refaktoryzowane przy aktualizacji do c ++> = 14. Myślę więc, że najlepszym rozwiązaniem jest całkowite uniknięcie przechwytywania przenoszenia dla c ++ 11.

Zwykle najprostszym i najlepiej czytelnym rozwiązaniem jest użycie std::shared_ptr, które można skopiować, dzięki czemu można całkowicie uniknąć przeniesienia. Wadą jest to, że jest trochę mniej wydajna, ale w wielu przypadkach wydajność nie jest tak ważna.

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

.

Jeśli zdarzy się bardzo rzadki przypadek, jest to naprawdę obowiązkowe move wskaźnika (np. Chcesz jawnie usunąć wskaźnik w osobnym wątku ze względu na długi czas usuwania lub wydajność jest absolutnie kluczowa), to jest prawie jedyny przypadek, w którym nadal używam surowe wskaźniki w C ++ 11. Można je oczywiście skopiować.

Zwykle oznaczam te rzadkie przypadki znakiem, //FIXME:aby upewnić się, że jest on refaktoryzowany po uaktualnieniu do 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;

Tak, surowe wskazówki są obecnie dość źle widziane (i nie bez powodu), ale naprawdę myślę, że w tych rzadkich (i tymczasowych!) Przypadkach są one najlepszym rozwiązaniem.


Dzięki, używanie C ++ 14 i innych rozwiązań nie było dobre. Uratowałem mój dzień!
Yoav Sternberg

1

Patrzyłem na te odpowiedzi, ale okazało się, że bind jest trudny do odczytania i zrozumienia. Więc zrobiłem zajęcia, które zamiast tego przeniosły się na kopię. W ten sposób jasno określa to, co robi.

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

move_with_copy_ctorKlasy i jest to funkcja pomocnika make_movable()będzie współpracować z każdym, ale nie copyable ruchomego obiektu. Aby uzyskać dostęp do opakowanego obiektu, użyj rozszerzenia operator()().

Oczekiwany wynik:

wartość: 1
obiekt nadal nie został usunięty
wartość: 1
Niszczenie 000000DFDD172280
obiekt został usunięty

Cóż, adres wskaźnika może się różnić. ;)

Demo


1

Wygląda na to, że działa na 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;
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.