Di chuyển chụp trong lambda


157

Làm cách nào để chụp bằng cách di chuyển (còn được gọi là tham chiếu rvalue) trong lambda C ++ 11?

Tôi đang cố gắng viết một cái gì đó như thế này:

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

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

Câu trả lời:


163

Chụp lambda tổng quát trong C ++ 14

Trong C ++ 14, chúng ta sẽ có cái gọi là lambda tổng quát . Điều này cho phép chụp di chuyển. Sau đây sẽ là mã hợp pháp trong 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 ); } ); 

Nhưng nói chung chung hơn nhiều theo nghĩa là các biến bị bắt có thể được khởi tạo với bất cứ thứ gì tương tự như vậy:

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

Trong C ++ 11, điều này là không thể, nhưng với một số thủ thuật liên quan đến các loại trình trợ giúp. May mắn thay, trình biên dịch Clang 3.4 đã thực hiện tính năng tuyệt vời này. Trình biên dịch sẽ được phát hành vào tháng 12 năm 2013 hoặc tháng 1 năm 2014, nếu tốc độ phát hành gần đây sẽ được giữ nguyên.

UPDATE: Các trình biên dịch Clang 3.4 được phát hành vào ngày 06 tháng một năm 2014 với tính năng cho biết.

Một cách giải quyết để chụp di chuyển

Đây là một triển khai của một chức năng trợ make_rrefgiúp giúp chụp di chuyển nhân tạo

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

Và đây là trường hợp thử nghiệm cho chức năng đó đã chạy thành công trên gcc 4.7.3 của tôi.

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

Hạn chế ở đây là có thể lambdasao chép và khi sao chép xác nhận trong hàm tạo sao chép rref_implkhông thành công dẫn đến lỗi thời gian chạy. Sau đây có thể là một giải pháp tốt hơn và thậm chí chung hơn vì trình biên dịch sẽ bắt lỗi.

Thi đua chụp lambda tổng quát trong C ++ 11

Đây là một ý tưởng nữa, về cách thực hiện chụp lambda tổng quát. Việc sử dụng hàm capture()(có triển khai được tìm thấy thêm) như sau:

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

Đây lambdalà một đối tượng functor (gần như một lambda thật) đã bị bắt std::move(p)khi nó được truyền tới capture(). Đối số thứ hai capturelà lambda, lấy biến bị bắt làm đối số. Khi lambdađược sử dụng làm đối tượng hàm, thì tất cả các đối số được truyền cho nó sẽ được chuyển tiếp đến lambda bên trong dưới dạng đối số sau biến đã bắt. (Trong trường hợp của chúng tôi, không có thêm đối số nào được chuyển tiếp). Về cơ bản, giống như trong các giải pháp trước xảy ra. Đây là cách capturethực hiện:

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

Giải pháp thứ hai này cũng sạch hơn, vì nó vô hiệu hóa sao chép lambda, nếu loại bị bắt không thể sao chép. Trong giải pháp đầu tiên chỉ có thể được kiểm tra trong thời gian chạy với một assert().


Tôi đã sử dụng lâu như vậy với G ++ - 4.8 -std = c ++ 11 và tôi nghĩ đó là một tính năng của C ++ 11. Bây giờ tôi đã sử dụng việc này và đột nhiên nhận ra đó là một tính năng của C ++ 14 ... Tôi nên làm gì đây !!
RnMss

@RnMss Bạn muốn nói đến tính năng nào? Tổng quát chụp lambda?
Ralph Tandetzky

@RalphTandetzky Tôi nghĩ vậy, tôi mới kiểm tra và phiên bản clang kèm theo XCode dường như cũng hỗ trợ nó! Nó đưa ra một cảnh báo rằng đó là một phần mở rộng C ++ 1y nhưng nó hoạt động.
Christopher Tarquini

@RnMss Hoặc sử dụng moveCapturetrình bao bọc để truyền chúng dưới dạng đối số (phương thức này được sử dụng ở trên và trong Capn'Proto, thư viện của người tạo protobuff) hoặc chấp nhận rằng bạn yêu cầu trình biên dịch hỗ trợ nó: P
Christopher Tarquini

9
Không, nó thực sự không phải là điều tương tự. Ví dụ: Bạn muốn sinh ra một chủ đề với lambda để di chuyển bắt con trỏ duy nhất. Hàm sinh sản có thể có thể quay trở lại và unique_ptr đi ra khỏi phạm vi trước khi functor được thực thi. Do đó, bạn có một tham chiếu lơ lửng đến unique_ptr. Chào mừng đến với đất không xác định hành vi.
Ralph Tandetzky

76

Bạn cũng có thể sử dụng std::bindđể chụp unique_ptr:

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

2
Cảm ơn vì đã đăng tải điều này!
mmocny

4
Bạn đã kiểm tra, nếu mã biên dịch? Nó không giống với tôi, vì trước hết tên biến bị thiếu và thứ hai là một unique_ptrtham chiếu giá trị không thể liên kết với một int *.
Ralph Tandetzky

7
Lưu ý rằng trong Visual Studio 2013, việc chuyển đổi một std :: bind thành hàm std :: vẫn dẫn đến việc nó sao chép tất cả các biến bị ràng buộc ( myPointertrong trường hợp này). Do đó, đoạn mã trên không biên dịch trong VS2013. Mặc dù vậy, nó vẫn ổn trong GCC 4.8.
Alan

22

Bạn có thể đạt được hầu hết những gì bạn muốn sử dụng std::bind, như thế này:

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

Mẹo ở đây là thay vì chụp đối tượng chỉ di chuyển của bạn trong danh sách chụp, chúng tôi biến nó thành đối số và sau đó sử dụng một phần ứng dụng thông qua std::bindđể làm cho nó biến mất. Lưu ý rằng lambda lấy nó bằng cách tham chiếu , vì nó thực sự được lưu trữ trong đối tượng liên kết. Tôi cũng đã thêm mã ghi vào đối tượng có thể di chuyển thực tế, vì đó là điều bạn có thể muốn làm.

Trong C ++ 14, bạn có thể sử dụng chụp lambda tổng quát để đạt được cùng một kết thúc, với mã này:

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

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

Nhưng mã này không mua cho bạn bất cứ thứ gì bạn không có trong C ++ 11 thông qua std::bind. (Có một số tình huống trong đó chụp lambda tổng quát mạnh hơn, nhưng không phải trong trường hợp này.)

Bây giờ chỉ có một vấn đề; bạn muốn đặt chức năng này trong một std::function, nhưng lớp đó yêu cầu chức năng đó là CopyConstructible , nhưng không phải, đó chỉ là MoveConstructible vì nó lưu trữ một std::unique_ptrthứ không phải là CopyConstructible .

Bạn phải giải quyết vấn đề với lớp bao bọc và một mức độ gián tiếp khác, nhưng có lẽ bạn không cần std::functiongì cả. Tùy thuộc vào nhu cầu của bạn, bạn có thể sử dụng std::packaged_task; nó sẽ làm công việc tương tự như std::function, nhưng nó không yêu cầu chức năng có thể sao chép được, chỉ có thể di chuyển (tương tự, std::packaged_taskchỉ có thể di chuyển). Nhược điểm là vì nó dự định sẽ được sử dụng cùng với std :: tương lai, nên bạn chỉ có thể gọi nó một lần.

Đây là một chương trình ngắn cho thấy tất cả các khái niệm này.

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

Tôi đã đặt một chương trình trên cho Coliru , để bạn có thể chạy và chơi với mã.

Đây là một số đầu ra điển hình ...

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

Bạn có thể thấy các vị trí heap đang được sử dụng lại, cho thấy rằng các vị trí std::unique_ptrhoạt động đúng. Bạn cũng thấy chức năng tự di chuyển xung quanh khi chúng ta bỏ nó trong một trình bao bọc mà chúng ta cung cấp std::function.

Nếu chúng ta chuyển sang sử dụng std::packaged_task, phần cuối cùng sẽ trở thành

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

vì vậy chúng ta thấy rằng hàm đã được di chuyển, nhưng thay vì được di chuyển lên heap, nó nằm trong std::packaged_taskngăn xếp đó.

Hi vọng điêu nay co ich!


4

Muộn, nhưng như một số người (bao gồm cả tôi) vẫn bị mắc kẹt trên c ++ 11:

Thành thật mà nói, tôi không thực sự thích bất kỳ giải pháp được đăng. Tôi chắc chắn rằng chúng sẽ hoạt động, nhưng chúng đòi hỏi rất nhiều công cụ bổ sung và / hoặc std::bindcú pháp mật mã ... và tôi không nghĩ rằng nó đáng để nỗ lực cho một giải pháp tạm thời như vậy dù sao cũng sẽ được tái cấu trúc khi nâng cấp lên c ++> = 14. Vì vậy, tôi nghĩ rằng giải pháp tốt nhất là tránh di chuyển bắt cho c ++ 11 hoàn toàn.

Thông thường, giải pháp đơn giản và dễ đọc nhất là sử dụng std::shared_ptr, có thể sao chép và do đó việc di chuyển là hoàn toàn có thể tránh được. Nhược điểm là, nó kém hiệu quả hơn một chút, nhưng trong nhiều trường hợp, hiệu quả không quá quan trọng.

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

.

Nếu trường hợp rất hiếm xảy ra, điều đó thực sự bắt buộc đối movevới con trỏ (ví dụ: bạn muốn xóa rõ ràng một con trỏ trong một luồng riêng biệt do thời gian xóa dài hoặc hiệu suất là rất quan trọng), đó là trường hợp duy nhất tôi vẫn sử dụng con trỏ thô trong c ++ 11. Đây là tất nhiên cũng có thể sao chép.

Thông thường tôi đánh dấu các trường hợp hiếm gặp này bằng một //FIXME:để đảm bảo rằng nó được tái cấu trúc một khi nâng cấp lên 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;

Vâng, con trỏ thô khá nhíu mày trong những ngày này (và không phải không có lý do), nhưng tôi thực sự nghĩ rằng trong những trường hợp hiếm hoi (và tạm thời!) Này, chúng là giải pháp tốt nhất.


Cảm ơn, sử dụng C ++ 14 và các giải pháp khác không tốt. Cứu ngày của tôi!
Yoav Sternberg

1

Tôi đã xem xét những câu trả lời này, nhưng tôi thấy ràng buộc khó đọc và hiểu. Vì vậy, những gì tôi đã làm là tạo ra một lớp di chuyển trên bản sao thay thế. Theo cách này, nó rõ ràng với những gì nó đang làm.

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

Các move_with_copy_ctorlớp và chức năng helper của nó make_movable()sẽ làm việc với bất kỳ đối tượng copyable di chuyển nhưng không phải. Để có quyền truy cập vào đối tượng được bọc, sử dụng operator()().

Sản lượng dự kiến:

giá trị: 1
đối tượng vẫn không bị xóa
giá trị: 1
Phá hủy 000000DFDD172280
đối tượng đã bị xóa

Vâng, địa chỉ con trỏ có thể thay đổi. ;)

Demo


1

Điều này dường như hoạt động trên 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;
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.