Pindahkan tangkapan di lambda


157

Bagaimana cara saya menangkap dengan memindahkan (juga dikenal sebagai referensi nilai) dalam lambda C ++ 11?

Saya mencoba menulis sesuatu seperti ini:

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

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

Jawaban:


163

Tangkapan lambda yang digeneralisasi dalam C ++ 14

Dalam C ++ 14 kita akan memiliki apa yang disebut penangkapan lambda umum . Hal ini memungkinkan penangkapan langkah. Berikut ini adalah kode hukum dalam 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 ); } ); 

Tetapi jauh lebih umum dalam arti bahwa variabel yang ditangkap dapat diinisialisasi dengan hal-hal seperti:

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

Di C ++ 11 ini belum memungkinkan, tetapi dengan beberapa trik yang melibatkan tipe pembantu. Untungnya, kompiler Dentang 3.4 sudah mengimplementasikan fitur luar biasa ini. Compiler akan dirilis Desember 2013 atau Januari 2014, jika kecepatan rilis terbaru akan dipertahankan.

UPDATE: The dentang 3.4 compiler dirilis pada 6 2014 Jan dengan kata fitur.

Solusi untuk menangkap langkah

Berikut ini adalah implementasi dari fungsi helper make_rrefyang membantu menangkap gerakan buatan

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

Dan ini adalah test case untuk fungsi yang berjalan dengan sukses pada gcc 4.7.3 saya.

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

Kelemahan di sini adalah yang lambdadapat disalin dan ketika menyalin pernyataan dalam copy constructor darirref_impl gagal menyebabkan bug runtime. Berikut ini mungkin solusi yang lebih baik dan lebih umum karena kompiler akan menangkap kesalahan.

Meniru penangkapan lambda umum di C ++ 11

Berikut ini satu ide lagi, tentang bagaimana menerapkan penangkapan lambda umum. Penggunaan fungsi capture()(yang implementasinya ditemukan lebih jauh ke bawah) adalah sebagai berikut:

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

Berikut lambdaadalah objek functor (hampir lambda nyata) yang telah ditangkap std::move(p)saat diteruskan ke capture(). Argumen kedua captureadalah lambda yang mengambil variabel yang ditangkap sebagai argumen. Ketika lambdadigunakan sebagai objek fungsi, maka semua argumen yang diteruskan ke itu akan diteruskan ke lambda internal sebagai argumen setelah variabel yang ditangkap. (Dalam kasus kami tidak ada argumen lebih lanjut untuk diteruskan). Intinya, sama seperti pada solusi sebelumnya yang terjadi. Begini caranya capturediimplementasikan:

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

Solusi kedua ini juga lebih bersih, karena menonaktifkan menyalin lambda, jika jenis yang ditangkap tidak dapat disalin. Dalam solusi pertama yang hanya dapat diperiksa saat runtime dengan assert().


Saya telah menggunakan selama ini dengan G ++ - 4.8 -std = c ++ 11, dan saya pikir ini adalah fitur C ++ 11. Sekarang saya sudah terbiasa menggunakan ini dan tiba-tiba menyadari itu adalah fitur C ++ 14 ... Apa yang harus saya lakukan !!
RnMss

@RnMss Fitur apa yang Anda maksud? Penangkapan lambda umum?
Ralph Tandetzky

@RalphTandetzky Saya pikir begitu, saya baru saja memeriksa dan versi dentang yang dibundel dengan XCode tampaknya mendukungnya juga! Ini memberi peringatan bahwa itu adalah ekstensi C ++ 1thn tetapi berhasil.
Christopher Tarquini

@RnMss Baik menggunakan moveCapturepembungkus untuk meneruskannya sebagai argumen (metode ini digunakan di atas dan di Capn'Proto, perpustakaan oleh pencipta protobuffs) atau buat saja terima bahwa Anda memerlukan kompiler yang mendukungnya: P
Christopher Tarquini

9
Tidak, sebenarnya itu bukan hal yang sama. Contoh: Anda ingin menelurkan utas dengan lambda yang memindahkan-menangkap pointer unik. Fungsi spawning dapat kembali dan unique_ptr keluar dari ruang lingkup sebelum functor dieksekusi. Oleh karena itu, Anda memiliki referensi yang menggantung ke unique_ptr. Selamat datang di tanah perilaku yang tidak terdefinisi.
Ralph Tandetzky

76

Anda juga dapat menggunakan std::binduntuk menangkap unique_ptr:

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

2
Terima kasih telah memposting ini!
mmocny

4
Sudahkah Anda memeriksa, jika kode dikompilasi? Itu tidak terlihat bagi saya, karena pertama nama variabel tidak ada dan kedua unique_ptrreferensi nilai tidak dapat mengikat ke int *.
Ralph Tandetzky

7
Perhatikan bahwa di Visual Studio 2013, mengonversi fungsi std :: bind ke std :: masih menghasilkannya menyalin semua variabel terikat ( myPointerdalam hal ini). Karenanya kode di atas tidak dikompilasi dalam VS2013. Ini bekerja dengan baik di GCC 4.8.
Alan

22

Anda dapat mencapai sebagian besar dari apa yang ingin Anda gunakan std::bind, seperti ini:

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

Kuncinya di sini adalah bahwa alih-alih menangkap objek hanya bergerak Anda dalam daftar tangkapan, kami menjadikannya argumen dan kemudian menggunakan aplikasi parsial via std::binduntuk membuatnya menghilang. Perhatikan bahwa lambda mengambilnya dengan referensi , karena sebenarnya disimpan dalam objek bind. Saya juga menambahkan kode yang menulis ke objek bergerak yang sebenarnya, karena itu sesuatu yang mungkin ingin Anda lakukan.

Di C ++ 14, Anda dapat menggunakan tangkapan lambda umum untuk mencapai tujuan yang sama, dengan kode ini:

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

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

Tetapi kode ini tidak membelikan Anda apa pun yang tidak Anda miliki di C ++ 11 via std::bind. (Ada beberapa situasi di mana penangkapan lambda secara umum lebih kuat, tetapi tidak dalam hal ini.)

Sekarang hanya ada satu masalah; Anda ingin meletakkan fungsi ini dalam a std::function, tetapi kelas itu mengharuskan fungsi tersebut menjadi CopyConstructible , tetapi tidak, itu hanya MoveConstructible karena menyimpan std::unique_ptryang bukan CopyConstructible .

Anda dapat mengatasi masalah dengan kelas pembungkus dan tingkat tipuan lainnya, tetapi mungkin Anda tidak perlu std::functionsama sekali. Tergantung pada kebutuhan Anda, Anda mungkin dapat menggunakan std::packaged_task; itu akan melakukan pekerjaan yang sama dengan std::function, tetapi tidak memerlukan fungsi untuk dapat disalin, hanya bergerak (sama, std::packaged_taskhanya bergerak). Kelemahannya adalah karena ini dimaksudkan untuk digunakan bersama dengan std :: future, Anda hanya dapat menyebutnya sekali.

Berikut ini adalah program singkat yang menunjukkan semua konsep ini.

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

Saya telah meletakkan program di atas pada Coliru , sehingga Anda dapat menjalankan dan bermain dengan kode.

Inilah beberapa keluaran khas ...

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

Anda bisa melihat tumpukan lokasi yang digunakan kembali, menunjukkan bahwa std::unique_ptritu berfungsi dengan baik. Anda juga melihat fungsi itu sendiri bergerak ketika kita menyimpannya di pembungkus yang kita beri makan std::function.

Jika kita beralih menggunakan std::packaged_task, itu bagian terakhir menjadi

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

jadi kita melihat bahwa fungsi telah dipindahkan, tetapi alih-alih dipindahkan ke tumpukan, itu ada di dalam std::packaged_taskyang ada di tumpukan.

Semoga ini membantu!


4

Terlambat, tetapi karena beberapa orang (termasuk saya) masih terjebak pada c ++ 11:

Sejujurnya, saya tidak terlalu suka solusi yang diposting. Saya yakin mereka akan bekerja, tetapi mereka membutuhkan banyak hal tambahan dan / atau std::bindsintaksis samar ... dan saya tidak berpikir itu sepadan dengan usaha untuk solusi sementara yang akan dire-refoured lagi ketika memperbarui ke c ++> = 14. Jadi saya pikir solusi terbaik adalah menghindari penangkapan untuk c ++ 11 sepenuhnya.

Biasanya solusi yang paling sederhana dan paling mudah dibaca adalah menggunakan std::shared_ptr, yang dapat disalin dan langkah ini benar-benar dapat dihindari. Kelemahannya, itu sedikit kurang efisien, tetapi dalam banyak kasus efisiensi tidak begitu penting.

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

.

Jika kasus yang sangat jarang terjadi, itu benar-benar wajib untuk movepointer (misalnya Anda ingin secara eksplisit menghapus pointer di utas terpisah karena durasi hapus yang lama, atau kinerja sangat penting), itu adalah satu-satunya kasus di mana saya masih menggunakan pointer mentah dalam c ++ 11. Ini tentu saja juga dapat disalin.

Biasanya saya menandai kasus-kasus langka ini dengan //FIXME:untuk memastikan bahwa itu di-refactored setelah ditingkatkan ke 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;

Ya, pointer mentah sangat disukai pada hari-hari ini (dan bukan tanpa alasan), tapi saya benar-benar berpikir dalam kasus yang jarang (dan sementara!) Ini adalah solusi terbaik.


Terima kasih, menggunakan C ++ 14 dan tidak ada solusi lain yang baik. Selamatkan hari saya!
Yoav Sternberg

1

Saya melihat jawaban-jawaban ini, tetapi saya merasa sulit untuk membaca dan memahami. Jadi yang saya lakukan adalah membuat kelas yang pindah pada salinan sebagai gantinya. Dengan cara ini, itu eksplisit dengan apa yang dilakukannya.

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

The move_with_copy_ctorkelas dan itu fungsi pembantu make_movable()akan bekerja dengan bergerak tapi tidak objek menyatakan bahwa pihak. Untuk mendapatkan akses ke objek yang dibungkus, gunakan operator()().

Output yang diharapkan:

nilai: 1
objek masih belum dihapus
nilai: 1
Menghancurkan 000000DFDD172280
objek telah dihapus

Nah, alamat pointer mungkin berbeda. ;)

Demo


1

Ini sepertinya bekerja pada 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;
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.