Edited at

std::bindの中にstd::bindを書くと死ぬ(挙動が変わる)

More than 3 years have passed since last update.

タイトルの通りである.

以下のコードを見ていただきたい.

#include <iostream>

#include <functional>

class Hoge {
public:
Hoge() {
piyo = std::bind(&Hoge::f, this, std::placeholders::_1, std::bind(&Hoge::g, this));
}
std::function<void(int)> piyo;

private:
void f(int value, std::function<int()> callback) {
if(callback) {
std::cout << "Called f. Value = " << value + callback() << std::endl;
}
}
int g() {
return 1;
}

};

int main() {
Hoge hoge;
hoge.piyo(10);
return 0;
}

コード自体に深い意味はない.

ようは,std::bindに渡す値にstd::bindの返り値を使っているのである.

このコードはエラーを吐く.これは,外側のstd::bindが期待する型std::function<int()>が内側のstd::bindの返り値と適合しないという旨のエラーを吐く.

無論,std::bindの返り値はstd::function<int()>で間違いはない.

妙な挙動をしているのは外側のstd::bind.

std::bindは,渡された値が以下のいずれかである場合,挙動が変わる.

1.std::reference_wrapper<T>である場合

2.std::is_bind_expression<T>::valueがtrueである場合

3.std::is_placeholder<T>::valueが非ゼロである場合

今回は2に該当する.

std::bindは引数が2に該当するとき,バインドされた関数を先に評価し,返り値を受け取る.

つまり, 今回の場合はstd::bindは引数としてintを期待していることになる.

これはこれで有難い機能なのかもしれないが,テンプレートの冗長なエラーコードを読むか,あるいは事前に知っておかない限りはデバッグが困難な機能を仕込まれるのはつらい.とてもつらい.

これの解決法として,以下のサイトを参考にした.

Boost.Bind の protect 相当の関数を C++0x の に用意する

しかし,個人的にはう〜んという感じ.

Variadic Templateを使い,全く同じoperator()インターフェースを用意したクラスを作り,オブジェクトを内含することでラッピングしている.

単なるオペレータのデリゲータでしかないことはある程度テンプレートに慣れていればわかるが,少し遠回りな気もした.

個人的には以下のような実装の方がわかりやすいと感じる.

コアな話はわからないので,コンパイラの最適化等の観点から問題があるのなら教えて欲しい.

 template<typename T>

struct protect_wrapper : T {
protect_wrapper(const T& t) : T(t) {}
protect_wrapper(T&& t) : T(std::move(t)) {}
};

template<typename T, typename U = typename std::decay<T>::type >
typename std::enable_if< !std::is_bind_expression< U >::value, T&& >::type protect(T&& t) {
return std::forward<T>(t);
}

template<typename T, typename U = typename std::decay<T>::type >
typename std::enable_if< std::is_bind_expression< U >::value, protect_wrapper< U > >::type protect(T&& t) {
return protect_wrapper<U>(std::forward<T>(t));
}

ようはパブリック継承である.

オペレータが等価のものを用意するというアプローチではなく,(wrapper = T)の関係が成り立つ構造体を作る.結局のところ,何かでラッピングしてbindに特殊化させなければいいのだ.

呼び出しは以下のように行う.


Hoge() {
piyo = std::bind(&Hoge::f, this, std::placeholders::_1, protect(std::bind(&Hoge::g, this)));
}

まあ,別にこんなことをしなくとも,fの引数をstd::function<int()>からintにするだけで済むのだが...