Переміщення захоплення в лямбда


157

Як я можу зафіксувати переміщення (також відоме як посилання на значення rvalue) у лямбі C ++ 11?

Я намагаюся написати щось подібне:

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

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

Відповіді:


163

Узагальнене захоплення лямбда в C ++ 14

У C ++ 14 ми матимемо так званий узагальнений захоплення лямбда . Це дозволяє захопити рух. Наступним буде юридичний код на 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 ); } ); 

Але це набагато загальніше в тому сенсі, що захоплені змінні можуть бути ініціалізовані з будь-яким подібним:

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

У C ++ 11 це ще неможливо, але з деякими хитрощами, які передбачають допоміжні типи. На щастя, компілятор Clang 3.4 вже реалізує цю дивовижну функцію. Компілятор вийде в грудні 2013 або січні 2014 року, якщо буде збережено останній темп випуску .

UPDATE: Clang 3.4 компілятор був випущений 6 січня 2014 року зі вказаною функцією.

Обхідне рішення для захоплення ходу

Ось реалізація допоміжної функції, make_rrefяка допомагає у захопленні штучних рухів

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

І ось тест для цієї функції, який успішно працював на моєму 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() );
}

Недолік тут полягає в тому, що він lambdaможе бути скопійований і коли скопіюється твердження в конструкторі копій rref_implне вдається, що призводить до помилки виконання. Наступне може бути кращим і ще більш загальним рішенням, оскільки компілятор виявить помилку.

Емуляція узагальненого захоплення лямбда в C ++ 11

Ось ще одна ідея про те, як реалізувати узагальнене захоплення лямбда. Використання функції capture()(реалізація якої знаходиться далі) полягає в наступному:

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

Ось lambdaоб’єкт функтора (майже справжня лямбда), який захопив у std::move(p)міру передачі capture(). Другий аргумент capture- лямбда, який приймає захоплену змінну як аргумент. Якщо lambdaвикористовується як об'єкт функції, то всі передані йому аргументи будуть передані до внутрішньої лямбда як аргументи після захопленої змінної. (У нашому випадку подальших аргументів не потрібно пересилати). По суті, відбувається те саме, що і в попередньому рішенні. Ось як captureреалізовано:

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

Це друге рішення також є більш чистим, оскільки воно вимикає копіювання лямбда, якщо захоплений тип не піддається копіюванню. У першому рішенні, яке можна перевірити лише під час виконання за допомогою assert().


Я використовую це довго з G ++ - 4,8 -std = c ++ 11, і я подумав, що це функція C ++ 11. Зараз я звик використовувати це і раптом зрозумів, що це функція C ++ 14 ... Що мені робити !!
RnMss

@RnMss Яку функцію ви маєте на увазі? Узагальнене захоплення лямбда?
Ральф Тандецький

@RalphTandetzky Я думаю, що так, я щойно перевірив, і версія clang в комплекті з XCode, здається, підтримує і його! Це дає попередження, що це розширення C ++ 1y, але воно працює.
Крістофер Таркіні

@RnMss Або використовуйте moveCaptureобгортку, щоб передати їх як аргументи (цей метод використовується вище та в Capn'Proto, бібліотеці, створеною автором протобуфів), або просто прийміть, що вам потрібні компілятори, які його підтримують: P
Крістофер Таркіні,

9
Ні, насправді це не те саме. Приклад: Ви хочете покласти нитку лямбда, яка перемістить - захопить унікальний покажчик. Функція нересту може, можливо, повернутися, і jedinstveний_птр вийде із сфери застосування, перш ніж функтор буде виконаний. Тому у вас є звисаюча посилання на unique_ptr. Ласкаво просимо до невизначеної поведінки.
Ральф Тандецький

76

Ви також можете використовувати std::bindдля зйомки unique_ptr:

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

2
Дякуємо, що опублікували це!
mmocny

4
Ви перевірили, чи код складається? Мені це не так здається, оскільки по-перше, ім'я змінної відсутнє, а по-друге, unique_ptrпосилання rvalue не може пов'язуватися з int *.
Ральф Тандецький

7
Зауважте, що у Visual Studio 2013 перетворення std :: bind у функцію std :: все ще призводить до того, що вона копіює всі пов'язані змінні ( myPointerу цьому випадку). Тому наведений вище код не компілюється у VS2013. Однак він прокидається в GCC 4.8.
Алан

22

Ви можете досягти більшості того, що ви хочете використовувати 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));

Хитрість тут полягає в тому, що замість того, щоб зафіксувати ваш об'єкт, що рухається лише у списку захоплень, ми робимо це аргументом, а потім використовуємо часткове додаток via, std::bindщоб він зник. Зауважте, що лямбда приймає це за посиланням , оскільки він фактично зберігається в об'єкті зв'язування. Я також додав код, який пише до фактичного рухомого об’єкта, тому що це щось, що ви можете зробити.

У C ++ 14 ви можете використовувати узагальнене захоплення лямбда, щоб досягти тих же цілей, за допомогою цього коду:

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

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

Але цей код не купує у вас нічого, чого у вас не було в C ++ 11 via std::bind. (Є деякі ситуації, коли узагальнене захоплення лямбда є більш потужним, але не в цьому випадку.)

Зараз є лише одна проблема; Ви хотіли поставити цю функцію в std::function, але цей клас вимагає, щоб ця функція була CopyConstructible , але це не так, це лише MoveConstructible, тому що вона зберігає файл, std::unique_ptrякий не є CopyConstructible .

Ви повинні вирішити проблему з класом обгортки та іншим рівнем непрямості, але, можливо, вам це зовсім не потрібно std::function. Залежно від ваших потреб, ви можете користуватися std::packaged_task; вона буде виконувати ту саму роботу std::function, що і вона, але вона не вимагає функції копіювання, лише рухомого (аналогічно std::packaged_taskлише рухомому). Мінус полягає в тому, що, оскільки він призначений для використання в поєднанні зі std :: future, ви можете його зателефонувати лише один раз.

Ось коротка програма, яка показує всі ці поняття.

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

Я поставив вищеописану програму на Coliru , так що ви можете запускати і грати з кодом.

Ось кілька типових результатів ...

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

Ви можете побачити, що місця купівлі використовуються повторно, показуючи, що пристрій std::unique_ptrпрацює належним чином. Ви також бачите, як сама функція рухається навколо, коли ми зберігаємо її в обгортці, в яку подаємо std::function.

Якщо ми перейдемо до використання std::packaged_task, вона стає останньою частиною

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

тож ми бачимо, що функція була переміщена, але замість того, щоб переміщуватися на купу, це всередині того, std::packaged_taskщо є у стеку.

Сподіваюся, це допомагає!


4

Пізно, але як деякі люди (включаючи мене) все ще тримаються на c ++ 11:

Якщо чесно, я не дуже люблю жодне з розміщених рішень. Я впевнений, що вони спрацюють, але їм потрібно багато додаткових матеріалів та / або криптовалютного std::bindсинтаксису ... і я не думаю, що варто докладати зусиль для такого тимчасового рішення, яке в будь-якому разі буде відновлено після оновлення до c ++> = 14. Тож я вважаю, що найкращим рішенням є повністю уникнути захоплення ходу для c ++ 11 повністю.

Зазвичай найпростішим і зручним для читання рішенням є використання std::shared_ptr, які можна скопіювати, і тому переміщення повністю уникнути. Недоліком є ​​те, що це трохи менш ефективно, але у багатьох випадках ефективність не настільки важлива.

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

.

Якщо трапляється дуже рідкісний випадок, це дійсно обов'язково move покажчика (наприклад, ви хочете явно видалити вказівник в окремий потік через тривалу тривалість видалення, або продуктивність абсолютно важлива), це майже єдиний випадок, коли я все ще використовую сировинні покажчики в c ++ 11. Звичайно, вони також можна скопіювати.

Зазвичай я відзначаю ці рідкісні випадки a, //FIXME:щоб переконатися, що він відновиться після оновлення до 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;

Так, сирі покажчики досить нахмурилися в ці дні (і не без підстав), але я дійсно думаю, що в цих рідкісних (і тимчасових!) Випадках вони є найкращим рішенням.


Спасибі, використовуючи C ++ 14, а не інші рішення були хорошими. Врятував мій день!
Йоав Штернберг

1

Я дивився на ці відповіді, але мені було важко читати та розуміти. Тому я зробив клас, який перемістився на копію. Таким чином, це явно з тим, що він робить.

#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_ctorКлас , і це допоміжна функція make_movable()буде працювати з будь-яким рухомим , але не Copyable об'єкта. Щоб отримати доступ до загорнутого об'єкта, скористайтесь значком operator()().

Очікуваний вихід:

значення: 1
об’єкт досі не видалений
значення: 1
Знищує 000000DFDD172280
об’єкт видалено

Ну, адреса вказівника може відрізнятися. ;)

Demo


1

Це, здається, працює на 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;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.