タイトルのとおりである。std::bindはnon-copyableな引数を束縛できないので、rvalue referenceを引数に取るような関数の扱いには注意を要する。
std::bindは、何らかの関数に事前に引数を与えた関数オブジェクトを生成する機能である。事前に引数を与えておくことで、後ほど複数回呼び出す際にいちいち引数を与えなくても良くなる便利機能。ただしC++14以上の環境だったらラムダ式で事足りるのであんまり出番はない。敢えて言えば、ラムダ式と違いオーバーロードされた関数オブジェクトに対応しやすいこと、コードが比較的綺麗であることがメリットと言えるが、関数ポインタを与えた場合に実行時オーバーヘッドが増してしまうことはデメリットだ。
そして困ったことに、std::bindもやはりムーブセマンティクスにまつわる致命的な欠点がある。std::functionと並びマジうぜえ。
std::bindの特徴の一つは、束縛した関数を何度でも呼び出せるというところだ。何度でも呼び出せるということは、最初に束縛した引数が後に何度も利用されるということである。そう、std::bindは、関数を何度呼び出そうと束縛した引数が有効であり続けることを要求してくるのだ。……さて、ここからが本記事タイトルの話だ。ちょっと考えてみてほしい。std::bindに束縛した関数が引数にrvalue referenceを持っていたとしたら。そしてもし、std::bindに束縛した引数が、ただ一度の関数呼び出しで破壊されてしまうとしたら。
次のコードを見てほしい。単純な足し算を行う関数Funcをstd::bindによって束縛している。
double Func(std::unique_ptr<double>&& x, std::unique_ptr<double>&& y) { //意図的にxとyの中身をmoveし破壊している。 auto xtmp = std::move(x); auto ytmp = std::move(y); return *xtmp + *ytmp; } int main() { auto x = std::make_unique<double>(3); auto y = std::make_unique<double>(5); auto func = std::bind(&Func, std::move(x), std::move(y)); std::cout << func() << std::endl;//これが仮に動いたとしても、 std::cout << func() << std::endl;//この時点で束縛したxとyが空なのでエラー。 return 0; }
上記のコードは、実際にはfuncのoperator()の呼び出しができずコンパイルエラーとなるのだが、仮にコンパイルが通り意図したように動作したと考えてみよう。最初のfunc()の時点で、std::bindに束縛したstd::unique_ptrはFuncの中で所有権を移譲され、funcに束縛されている方のstd::unique_ptrは空となる。この状態でもう一度funcを呼び出すと、当然空のポインタにアクセスしているためエラーになるだろう。
このようなエラーを避けるため、std::bindはrvalue referenceの引数をそもそも束縛できないように設計されている。らしい(海外の掲示板でそのように解説しているレスを見たことがあるだけであるので、信頼性は不明。そのスレッドを改めて探してみるも、見つけられなかった)。何故そんな仕様にしたのか正直理解に苦しむが、そうなっているのだから仕方がない。
解決方法は複数考えられる。
1. 引数をlvalue referenceに変更する。
もし関数を変更もしくはラップできるのなら、引数をrvalue referenceからlvalue referenceへと変えてしまうのが手っ取り早い。
double Func(std::unique_ptr<double>& x, std::unique_ptr<double>& y) { //lvalue referenceをmoveしてはいけない、なんてルールは存在しない。 auto xtmp = std::move(x); auto ytmp = std::move(y); return *xtmp + *ytmp; }
ただこの方法は正直馬鹿げていると思う。引数がrvalue referenceであるからこそ二度目の呼び出しが危険であることが一見して推察されるためプログラマーが注意深く使用できるというのに、lvalue referenceでは関数の処理を熟読しなければ分からない。std::bindはプログラマーに対して危険な使い方をさせないために、より一層危険な回避策へ走らせている。本末転倒ではないか。
2. ラムダ式を使う。
C++14以上の環境であれば、最も適切な方法はこれだろう。正直、std::bindはラムダ式に比べてデメリットが大きすぎて使う気になれない。ラムダ式はコードが長たらしく汚らしくなるが圧倒的に汎用度が高いので、モダンな環境で開発しているのなら是が非でもマスターすべきである。
//C++14以上なら auto func = [xtmp = std::move(x), ytmp = std::move(y)]() mutable
C++11では残念ながら変数をmoveしつつラムダ式にキャプチャさせることができないので、もし変数x、yの寿命がfunc2の呼び出しまで尽きないことが確かであるのなら参照キャプチャを、そうでないならstd::shared_ptrに押し込むか、それも嫌なら次のようなヘルパーを使おう。
//x、yの寿命が尽きない場合はこうするのが手っ取り早い。 auto func = [&x, &y]() mutable { return Func(std::move(x), std::move(y)); }; //もしくはこのようなクラスを用意する。 template <class T> struct MoveCaptureHelper { MoveCaptureHelper(T&& t) : t(std::move(t)) {} //コピーの際、tをコピーではなく強制的にムーブさせる。 //ムーブキャプチャできないラムダ式に対して、コピーキャプチャに偽装してムーブするわけである。 MoveCaptureHelper(const MoveCaptureHelper<T>& m) : t(std::move(m.t)) {} MoveCaptureHelper<T> operator=(const MoveCaptureHelper<T>& m) { t = std::move(m.t); } mutable T t; }; template <class T> MoveCaptureHelper<std::decay_t<T>> MoveCapture(T&& t) { return MoveCaptureHelper<std::decay_t<T>>(std::forward<T>(t)); } //こんな感じに使う。 auto xtmp = MoveCapture(std::move(x)); auto ytmp = MoveCapture(std::move(y)); auto func = [xtmp, ytmp]() mutable { return Func(std::move(xtmp.t), std::move(ytmp.t)); };
3. rvalue reference対応のバインドクラスを自作する。
標準ライブラリに近い機能を設計する自信のある人は、この方法を取ってみてもいいだろう。ちょっと試しに作ってここに載せてやろうかとも思ったが、やめた。束縛した引数の取り扱いがとても面倒くさいのだ。placeholderまで再現するとかなりの手間になるだろう。本気で実装し始めたら丸一日くらい消費してしまう気がする。
前回のstd::functionの話と今記事はムーブセマンティクス対応のThreadPoolを公開するための下準備である。このあたりの話が、何故GitHubなどで転がっているThreadPoolの大半が残念な出来であるのかを説明するために重要なのだ。……こんな話をいちいちしなくて済むように、標準ライブラリくらいきちんと制定してほしいものである。ねえ、C++標準化委員会さん?