std::functionは関数ポインタも関数オブジェクトもメンバ関数ポインタもまとめて管理することのできるとても便利な機能である。std::functionは"引数"と"戻り値"の型のみ指定されていればよく、それ以上は何も要求しないので、引数と戻り値の等しい関数オブジェクトや関数ポインタなどを纏めて配列にしてしまうなど、柔軟に使うことができる。与えた関数のインライン展開が難しく、関数実行時のオーバーヘッドが生じるデメリットはあるが、億単位の関数呼び出しが発生しない限りは無視して良いデメリットだ。
ただし、std::functionは万能ではない。特に致命的な欠点はnon-copyableな関数を与えることが出来ない点である。関数がnon-copyableという状況自体が稀ではあるが、関数オブジェクトならあり得る。例えば次のように、non-copyableな変数をキャプチャしたラムダ式などだ。
#include <functional> struct NonCopyable { NonCopyable() {} NonCopyable(const NonCopyable&) = delete; NonCopyable(NonCopyable&&) = default; }; int main() { auto lambda = [t = NonCopyable()]() mutable {}; std::function<void(void)> f(std::move(lambda));//compile error return 0; }
ラムダ式はキャプチャした変数をメンバ変数として保有する。今回のlambdaで言えば、non-copyableなNonCopyableクラスのインスタンスを保有していることになる。それに伴い、lambda自体もnon-copyableとなる。すると、std::functionにlambdaを与えようとするもstd::functionは削除済みのNonCopyableのコピーコンストラクタを呼び出そうとしてコンパイルエラーになってしまうのだ。std::functionにnon-copyableな関数オブジェクトを与えると、このようにコンパイルそのものが通らなくなってしまう。
これを解決する方法はいくつかある。
1. lambdaの定義を工夫してcopy-constructibleな形にする。
例えばstd::shared_ptrに押し込んでしまう方法がある。std::shared_ptr\<NonCopyable>をコピーしても、その中のNonCopyableインスタンス自体は複製されず、それぞれの間で値が共有される。したがってNonCopyable自体のコピーコンストラクタが呼ばれることはないため、これは意図したように動作する。手間のかからない解決方法ではあるが、余計なメモリ確保のコストが発生し気持ちが悪いので私はやらない。
auto lambda = [t = std::make_shared<NonCopyable>()]() {};
2. copy-constructibleなオブジェクトに押し込む。
lambdaそれ自体は変更せず、その関数オブジェクトを束縛しておくcopy-constructibleな関数オブジェクトを用意する方法。CopyableFunctionはコピーコンストラクタが呼ばれると例外を発生させるため、std::functionをコピーすることは不可能になるが、そもそもnon-copyableな関数オブジェクトを束縛しておきながらそれをコピーしなければならない状況などあるはずがない。あるとしたら設計者の脳味噌が腐っている場合だけである。 コピペすればよいCopyableFunctionの定義部分を除いてもコーディングのコストは1.と大差ないが、関数定義を変更できない場合などは有用かもしれない。
template <class Func> struct CopyableFunction { CopyableFunction(Func f) : func(std::move(f)) {} CopyableFunction(const CopyableFunction&) : func(ThrowException()) {} CopyableFunction(CopyableFunction&&) = default; Func ThrowException() { throw std::exception(); } template <class ...Args> decltype(auto) operator()(Args&& ...args) const { return func(std::forward<Args>(args)...); } template <class ...Args> decltype(auto) operator()(Args&& ...args) { return func(std::forward<Args>(args)...); } Func func; }; template <class Func> CopyableFunction<Func> MakeCopyableFunction(Func func) { return CopyableFunction<Func>(std::move(func)); } int main() { auto lambda = [t = NonCopyable()]() mutable {}; std::function<void(void)> f(MakeCopyableFunction(std::move(lambda))); f(); return 0; }
私はこのstd::functionの仕様に苦しめられ、ムーブセマンティクス対応のThreadPoolの設計にとても手間取った。標準ライブラリにも間抜けな機能はそれなりに多いらしい。