ことの発端
関数型プログラミングをやっていると関数を引数として渡すケースが出てきます。
で、型変換をうまい具合に吸収してくれる std::function
をよく使うのですが、その際に「std::functionの関数型の仮引数の型」と「実際の関数の仮引数の型」が異なる場合に、仮引数に渡る値が「参照」のまま転送されるのか、コピーなのかムーブなのか確認したいと思います。
検証
(1) A
というクラスはコンストラクタやデストラクタでログを記録します。
(2) 関数を受ける関数の型を fn_t
と fn_cref_t
で定義します。
using fn_t = std::function<void(A)>;
using fn_cref_t = std::function<void(const A&)>;
(3) 関数を引数として受け、その関数を呼び出す call
というテンプレート関数を定義します。F
には fn_t
か fn_cref_t
が入ります。
template <typename F> void call(F f, const A& a){
f(a);
}
(4) 実際の関数は下記のようになります。
auto fn1 = [](A){};
auto fn2 = [](const A&){};
auto fn3 = [](A&&){};
auto fn1a = [](auto){};
auto fn2a = [](const auto&){};
auto fn3a = [](auto&& x){ /* x.f(); */ };
サンプルコード
# include <iostream>
# include <functional>
using namespace std;
struct A {
A() { cout << "A::ctor default" << endl; }
A(const A&) { cout << "A::ctor copy" << endl; }
A(A&&) { cout << "A::ctor move" << endl; }
~A() { cout << "A::dtor" << endl; }
void f() { cout << "A::f" << endl; }
};
using fn_t = std::function<void(A)>;
using fn_cref_t = std::function<void(const A&)>;
auto fn1 = [](A){};
auto fn2 = [](const A&){};
auto fn3 = [](A&&){};
auto fn1a = [](auto){};
auto fn2a = [](const auto&){};
auto fn3a = [](auto&& x){ /* x.f(); */ };
template <typename F> void call(F f, const A& a){
f(a);
}
int main(void){
A a;
cout << "----" << endl;
cout << "fn_t #1" << endl;
call<fn_t>(fn1, a);
cout << "fn_t #2" << endl;
call<fn_t>(fn2, a);
cout << "fn_t #3" << endl;
call<fn_t>(fn3, a);
cout << "fn_t #1a" << endl;
call<fn_t>(fn1a, a);
cout << "fn_t #2a" << endl;
call<fn_t>(fn2a, a);
cout << "fn_t #3a" << endl;
call<fn_t>(fn3a, a);
cout << "fn_cref_t #1" << endl;
call<fn_cref_t>(fn1, a);
cout << "fn_cref_t #2" << endl;
call<fn_cref_t>(fn2, a);
cout << "fn_cref_t #3" << endl;
cout << "(compile error)" << endl;
// call<fn_cref_t>(fn3, a);
cout << "fn_cref_t #1a" << endl;
call<fn_cref_t>(fn1a, a);
cout << "fn_cref_t #2a" << endl;
call<fn_cref_t>(fn2a, a);
cout << "fn_cref_t #3a" << endl;
call<fn_cref_t>(fn3a, a);
cout << "----" << endl;
}
結果
A::ctor default
----
fn_t #1
A::ctor copy
A::ctor move
A::dtor
A::dtor
fn_t #2
A::ctor copy
A::dtor
fn_t #3
A::ctor copy
A::dtor
fn_t #1a
A::ctor copy
A::ctor move
A::dtor
A::dtor
fn_t #2a
A::ctor copy
A::dtor
fn_t #3a
A::ctor copy
A::dtor
fn_cref_t #1
A::ctor copy
A::dtor
fn_cref_t #2
fn_cref_t #3
(compile error)
fn_cref_t #1a
A::ctor copy
A::dtor
fn_cref_t #2a
fn_cref_t #3a
----
A::dtor
まとめ
引数の関数の型 | 実際の関数の型 |
A の転送状況 |
---|---|---|
std::function<void(A)> | void(A) | copy → move |
std::function<void(A)> | void(const A&) | copy |
std::function<void(A)> | void(A&&) | copy |
std::function<void(A)> | void(auto) | copy → move |
std::function<void(A)> | void(const auto&) | copy |
std::function<void(A)> | void(auto&&) | copy |
std::function<void(const A&)> | void(A) | copy |
std::function<void(const A&)> | void(const A&) | |
std::function<void(const A&)> | void(A&&) | (コンパイルエラー) |
std::function<void(const A&)> | void(auto) | copy |
std::function<void(const A&)> | void(const auto&) | |
std::function<void(const A&)> | void(auto&&) |
実際の関数の仮引数がどうあれ、一旦は call()
にて実際の関数をもとに std::function
が生成され、A
の値は std::function
を経由してから実際に関数に転送されますので、実体 A
を受ければコピーが発生します。
その後、実際の関数への呼び出しが発生するので、至極当然な結果でした。
ちなみに
上記の検証コードの fn_t
と fn_cref_t
で std::function
を使用しなかった場合には下記のような結果となります。
using fn_t = void(A);
using fn_cref_t = void(const A&);
|引数の関数の型|実際の関数の型|A
の転送状況|
|--|--|--|--|
|void(A)|void(A)|copy| |
|void(A)|void(const A&)| (コンパイルエラー)|
|void(A)|void(A&&)|(コンパイルエラー)|
|void(A)|void(auto)|copy|
|void(A)|void(const auto&)| (コンパイルエラー)|
|void(A)|void(auto&&)| (コンパイルエラー)|
|void(const A&)|void(A)| (コンパイルエラー)|
|void(const A&)|void(const A&)| | |
|void(const A&)|void(A&&)| (コンパイルエラー)|
|void(const A&)|void(auto)|| |
|void(const A&)|void(const auto&)| | |
|void(const A&)|void(auto&&)| (コンパイルエラー)|
std::function
という緩衝材が無いので無慈悲な結果ですが、void(A&&)
が一見オリジナルのA
の右辺値を受けているようなミスリード(実際はコピーされたA
の右辺値)が無くなるし、速度面でも有利になります。しかし、関数がキャプチャ指定子を有するラムダ式の場合(例えばauto fn1 = [=](A){};
のような場合)、関数オブジェクトとして取り扱う必要があるので全てコンパイルエラーとなります。