ラムダでキャプチャを移動


157

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年12月または2014年1月にリリースされます。

UPDATE:クラン3.4コンパイラ前記特徴と2014年1月6日にリリースされました。

ムーブキャプチャの回避策

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での一般化されたラムダキャプチャのエミュレート

一般化されたラムダキャプチャを実装する方法について、もう1つ考えます。関数の使用法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()。の2番目の引数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) );
}

キャプチャされたタイプがコピー可能でない場合、ラムダのコピーが無効になるため、この2番目のソリューションもよりクリーンです。最初のソリューションでは、実行時にのみでチェックできますassert()


私はこれをG ++-4.8 -std = c ++ 11で長い間使用しており、C ++ 11の機能だと思いました。今、私はこれを使用していて、突然C ++ 14の機能であることに気付きました...どうすればいいですか!!
RnMss 2014年

@RnMssどの機能を意味しますか?一般化されたラムダキャプチャ?
Ralph Tandetzky、2014年

@RalphTandetzky私はそう思う、私はチェックしたところ、XCodeにバンドルされているclangのバージョンもそれをサポートしているようです!これはC ++ 1y拡張であることを警告しますが、機能します。
クリストファー

@RnMss moveCaptureラッパーを使用してそれらを引数として渡すか(このメソッドは、上記およびプロトバフの作成者によるライブラリであるCapn'Protoで使用されます)、またはそれをサポートするコンパイラーが必要であることを受け入れます:P
Christopher Tarquini

9
いいえ、実際には同じことではありません。例:一意のポインターを移動キャプチャするラムダを使用してスレッドを生成したいとします。関数が実行される前に、スポーン関数が戻る可能性があり、unique_ptrがスコープ外になります。したがって、unique_ptrへのぶら下がり参照があります。undefined-behavior-landへようこそ。
Ralph Tandetzky 2014年

76

std::bindをキャプチャするためにも使用できますunique_ptr

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

2
これを投稿していただきありがとうございます!
mmocny 2013年

4
コードがコンパイルされるかどうかを確認しましたか?最初は変数名が欠落しており、次にunique_ptr右辺値参照はにバインドできないため、私には見えませんint *
Ralph Tandetzky

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してそれを消滅させることです。ラムダは実際にはバインドオブジェクトに格納されているため、参照によって取得することに注意してください。実際の移動可能なオブジェクトに書き込むコードも追加しました。これ、あなたがやりたいかもしれないことだからです。

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。(一般化されたラムダキャプチャの方が強力な場合もありますが、この場合はそうではありません。)

ここで問題は1つだけです。あなたはこの機能を置きたいのだstd::functionが、そのクラスは、関数があることを必要とするコピーコンストラクトが、それはないが、それが唯一だ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
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;
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.