Lambda'da yakalama


157

Bir C ++ 11 lambda'da hareketle (rvalue referansı olarak da bilinir) nasıl yakalayabilirim?

Ben böyle bir şey yazmaya çalışıyorum:

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

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

Yanıtlar:


163

C ++ 14'te genelleştirilmiş lambda yakalama

C ++ 14'te genelleştirilmiş lambda yakalama olarak adlandırılacak . Bu hareket yakalamayı mümkün kılar. Aşağıdaki C ++ 14 yasal kod olacaktır:

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

Ancak yakalanan değişkenlerin böyle bir şeyle başlatılabilmesi anlamında çok daha geneldir:

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

C ++ 11'de bu henüz mümkün değildir, ancak yardımcı türleri içeren bazı hilelerle. Neyse ki, Clang 3.4 derleyicisi bu harika özelliği zaten uygulamaktadır. Derleyici, son sürüm hızını koruyacaksa Aralık 2013 veya Ocak 2014'te piyasaya sürülecektir .

GÜNCELLEME: Clang 3.4 derleyici söz konusu özelliği ile 6 Ocak 2014 tarihinde serbest bırakıldı.

Hareket yakalama için geçici bir çözüm

make_rrefYapay hareket yakalamaya yardımcı olan bir yardımcı fonksiyonun uygulaması

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

Ve işte bu fonksiyon için benim gcc 4.7.3 başarıyla çalıştırılan bir test durumda.

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

Buradaki dezavantaj lambda, kopyalanabilir ve kopya yapıcısındaki iddianın rref_implbir çalışma zamanı hatasına yol açması başarısız olmasıdır. Aşağıdakiler daha iyi ve daha genel bir çözüm olabilir, çünkü derleyici hatayı yakalayacaktır.

C ++ 11'de genelleştirilmiş lambda yakalamayı taklit etme

Genelleştirilmiş lambda yakalamanın nasıl uygulanacağı hakkında bir fikir daha. capture()(Uygulaması daha aşağıda bulunan) fonksiyonunun kullanımı aşağıdaki gibidir:

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

İşte lambdayakalamış bir funktor nesne (neredeyse gerçek lamda) 'dir std::move(p)o geçirilir olarak capture(). İkinci argümanı capture, yakalanan değişkeni argüman olarak alan bir lambdadır. Bir lambdaişlev nesnesi olarak kullanıldığında, kendisine iletilen tüm bağımsız değişkenler, yakalanan değişkenin ardından bağımsız değişken olarak dahili lambda'ya iletilir. (Bizim durumumuzda iletilecek başka argüman yoktur). Esasen, önceki çözümdeki ile aynıdır. Nasıl captureuygulandığı aşağıda açıklanmıştır :

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

Bu ikinci çözüm de daha temizdir, çünkü yakalanan tip kopyalanamazsa lambda kopyalamayı devre dışı bırakır. Sadece bir ile çalışma zamanında kontrol edilebilir ilk çözümde assert().


G ++ - 4.8 -std = c ++ 11 ile bu kadar uzun zamandır kullanıyorum ve bir C ++ 11 özelliği olduğunu düşündüm. Şimdi bunu kullanmaya alışkınım ve aniden bir C ++ 14 özelliği olduğunu fark ettim ... Ne yapmalıyım !!
RnMss

@RnMss Ne demek istiyorsun? Genelleştirilmiş lambda yakalama?
Ralph Tandetzky

@RalphTandetzky Bence, sadece kontrol ettim ve XCode ile birlikte gelen clang sürümü de bunu destekliyor gibi görünüyor! Bir C ++ 1y uzantısı olduğuna dair bir uyarı verir, ancak çalışır.
Christopher Tarquini

@RnMss Ya moveCapturebunları argüman olarak iletmek için bir sarıcı kullanın (bu yöntem yukarıda ve protobuffların yaratıcısı tarafından bir kitaplık olan Capn'Proto'da kullanılır) veya sadece onu destekleyen derleyicilere ihtiyacınız olduğunu kabul edin: P
Christopher Tarquini

9
Hayır, aslında aynı şey değil. Örnek: Benzersiz işaretçiyi hareket ettiren lambda ile bir iplik oluşturmak istiyorsunuz. Yumurtlama işlevi büyük olasılıkla geri dönebilir ve işlev_çalıştırılmadan önce unique_ptr kapsam dışına çıkar. Bu nedenle, unique_ptr öğesine sarkan bir referansınız var. Undefined-behavior-land'a hoş geldiniz.
Ralph Tandetzky

76

Şunları std::bindyakalamak için de kullanabilirsiniz unique_ptr:

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

2
Bunu gönderdiğiniz için teşekkür ederiz!
mmocny

4
Kod derlenirse kontrol ettiniz mi? Bana öyle görünmüyor, çünkü öncelikle değişken adı eksik ve ikincisi bir unique_ptrrvalue referansı bir int *.
Ralph Tandetzky

7
Visual Studio 2013'te, bir std :: bind dosyasını bir std :: işlevine dönüştürmenin yine de tüm ilişkili değişkenleri (myPointer dosyasını bu durumda) . Bu nedenle, yukarıdaki kod VS2013'te derlenmez. Gerçi GCC 4.8'de iyi çalışıyor.
Alan

22

Kullanmak istediğiniz şeylerin çoğuna şu şekilde ulaşabilirsiniz std::bind:

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

Buradaki hile, yakalama listesinde salt hareket eden nesnenizi yakalamak yerine, onu bir argüman haline getirmek ve daha sonra std::bindortadan kaldırmak için kısmi uygulamayı kullanmaktır . Lambda'nın referans olarak aldığını unutmayın , çünkü aslında bağlama nesnesinde saklanır. Ayrıca kod eklediği yazıyor , gerçek hareketli nesneye , çünkü bu yapmak isteyebileceğiniz bir şey.

C ++ 14'te, aynı kodları elde etmek için şu kodla genelleştirilmiş lambda yakalamayı kullanabilirsiniz:

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

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

Ancak bu kod size C ++ 11 ile sahip olmadığınız hiçbir şeyi satın almaz std::bind. (Genelleştirilmiş lambda yakalamanın daha güçlü olduğu, ancak bu durumda olmadığı bazı durumlar vardır.)

Şimdi sadece bir sorun var; bu işlevi a'ya koymak istediniz std::function, ancak bu sınıf işlevin a'ya CopyConstructible , ancak değil, yalnızca MoveConstructible, çünkü CopyConstructiblestd::unique_ptr olmayan bir depolama .

Sarıcı sınıfı ve başka bir dolaylama düzeyi ile ilgili sorunu çözmek için çalışabilirsiniz, ancak belki de hiç ihtiyacınız yoktur std::function. İhtiyaçlarınıza bağlı olarak,std::packaged_task ; aynı işi yapar std::function, ancak işlevin kopyalanabilmesini gerektirmez, sadece taşınabilir (benzer şekilde std::packaged_tasksadece taşınabilir). Dezavantajı, std :: future ile birlikte kullanılması amaçlandığından, yalnızca bir kez çağırabilirsiniz.

İşte tüm bu kavramları gösteren kısa bir program.

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

Ben Coliru yukarıdaki bir program koydum , böylece çalıştırmak ve kod ile oynayabilirsiniz.

İşte bazı tipik çıktı ...

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

Öbek konumlarının yeniden kullanıldığını, std::unique_ptr düzgün çalıştığını görebilirsiniz. Ayrıca, beslediğimiz bir sarıcıda sakladığımızda işlevin kendisinin hareket ettiğini görüyorsunuz std::function.

Kullanmaya geçersek std::packaged_task geçersek, son kısım

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

bu nedenle işlevin taşındığını görüyoruz, ancak yığına taşınmak yerine, yığının std::packaged_tasküzerinde yer alıyor.

Bu yardımcı olur umarım!


4

Geç, ama bazı insanlar (ben dahil) hala c ++ 11'e takılı kalıyor:

Dürüst olmak gerekirse, yayınlanan çözümlerden hiçbirini gerçekten sevmiyorum. Eminim işe yarayacaklar, ancak çok fazla ek şey ve / veya şifreli std::bindsözdizimi gerektiriyorlar ... ve c ++ 'ya yükseltirken yine de yeniden düzenlenecek geçici bir çözüm için çabaya değeceğini düşünmüyorum> = 14. Bence en iyi çözüm c ++ 11 için hareket yakalamayı tamamen önlemek.

Genellikle en basit ve en iyi okunabilir çözüm, std::shared_ptrkopyalanabilir olan ve böylece hareketin tamamen önlenebilir olduğu kullanmaktır. Dezavantajı, biraz daha az verimli olması, ancak çoğu durumda verimlilik o kadar önemli değildir.

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

.

Çok nadir bir durum ortaya çıkarsa, move işaretçinin (örneğin, uzun silme süresi nedeniyle bir işaretçiyi ayrı bir iş parçacığında açıkça silmek istiyorsanız veya performans kesinlikle çok önemlidir), hala kullandığım tek durum bu c ++ 11'de ham işaretçiler. Bunlar elbette kopyalanabilir.

Genellikle bir //FIXME:kez c ++ 14'e yükseltildiğinde yeniden düzenlenmesi için bu nadir durumlarda bir işaretlerim .

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

Evet, ham işaretçiler bu günlerde (ve sebepsiz değil) oldukça kaşlarını çattı, ancak bu nadir (ve geçici!) Durumlarda gerçekten en iyi çözüm olduğunu düşünüyorum.


Teşekkürler, C ++ 14 kullanarak ve diğer çözümler iyi değildi. Günümü kurtardım!
Yoav Sternberg

1

Bu cevaplara bakıyordum, ama okumayı ve anlamayı zor buldum. Yaptığım şey bunun yerine kopyaya geçen bir sınıf yapmaktı. Bu şekilde, ne yaptığı açıktır.

#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_ctorSınıf ve 's yardımcı işlevi make_movable()herhangi hareketli değil copyable nesne ile çalışacaktır. Sarılmış nesneye erişmek için operator()().

Beklenen çıktı:

değer: 1
nesne hala silinmedi
değer: 1
000000DFDD172280 yok ediliyor
nesne silindi

İşaretçi adresi değişebilir. ;)

Demo


1

Bu gcc4.8 üzerinde çalışıyor gibi görünüyor

#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;
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.