람다에서 캡처 이동


157

C ++ 11 람다에서 이동 (rvalue 참조라고도 함)으로 어떻게 캡처합니까?

다음과 같이 쓰려고합니다.

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 년 12 월 또는 2014 년 1 월에 릴리스 됩니다.

최신 정보: 연타 3.4 컴파일러는 상기 기능 6 년 1 월 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() );
}

여기 lambdastd::move(p)전달 될 때 캡처 한 functor 객체 (거의 실제 람다)가 있습니다 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 어떤 기능을 의미합니까? 일반화 된 람다 캡처?
Ralph Tandetzky

@RalphTandetzky 그렇게 생각합니다. 방금 확인했으며 XCode와 번들로 제공되는 clang 버전도 그것을 지원하는 것 같습니다! 그것은 C ++ 1y 확장자라는 경고를 주지만 작동합니다.
Christopher Tarquini

@RnMss moveCapture래퍼를 사용하여 인수로 인수로 전달하거나 (이 방법은 위에서 설명한 바와 같이 프로토 버프 작성자가 만든 Capn'Proto에서 사용됩니다) 또는이를 지원하는 컴파일러가 필요하다는 사실 만 인정하십시오. : P
Christopher Tarquini

9
아니요, 실제로는 다릅니다. 예 : 고유 포인터를 이동 캡처하는 람다가있는 스레드를 생성하려고합니다. 스폰 함수는 반환 될 수 있고 functor가 실행되기 전에 unique_ptr이 범위를 벗어납니다. 따라서 unique_ptr에 매달려있는 참조가 있습니다. undefined-behavior-land에 오신 것을 환영합니다.
Ralph Tandetzky

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_ptrrvalue 참조가에 바인딩 할 수 없기 때문에 나에게 보이지 않습니다 int *.
랄프 탄 데츠 키

7
Visual Studio 2013에서 std :: bind를 std :: function으로 변환하면 모든 바인딩 된 변수 ( 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));

여기서의 요점은 캡처 목록에서 이동 전용 개체를 캡처하는 대신 인수로 만든 다음 부분 응용 프로그램을 통해 std::bind사라지게하는 것입니다. 람다는 실제로 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에없는 것을 사지 않습니다 std::bind. (일반화 된 람다 캡처가 더 강력하지만이 경우에는 그렇지 않은 상황이 있습니다.)

이제 한 가지 문제가 있습니다. 이 함수를에 넣고 싶었지만 std::function해당 클래스에서는 함수가 CopyConstructible이어야 하지만 MoveConstructible 만이 아닙니다. 저장하기 때문에std::unique_ptr 아닙니다 . .

래퍼 클래스 및 다른 수준의 간접 문제로 문제를 해결해야하지만 전혀 필요하지는 않습니다 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를 사용하고 다른 솔루션을 사용하지 않는 것이 좋습니다. 내 하루를 구했다!
Yoav Sternberg

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()어떤 이동이 아닌 복사 가능한 객체와 함께 작동합니다. 래핑 된 객체에 액세스하려면을 사용하십시오 operator()().

예상 출력 :

값 : 1
개체가 여전히 삭제되지 않았습니다
값 : 1
000000 파괴 파괴
개체가 삭제되었습니다

포인터 주소가 다를 수 있습니다. ;)

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;
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.