LoginSignup
0
0

More than 1 year has passed since last update.

std::functionを引数とする場合、その関数の型が実際に呼び出す関数の型と異なる場合の挙動

Last updated at Posted at 2022-02-08

ことの発端

関数型プログラミングをやっていると関数を引数として渡すケースが出てきます。
で、型変換をうまい具合に吸収してくれる std::function をよく使うのですが、その際に「std::functionの関数型の仮引数の型」と「実際の関数の仮引数の型」が異なる場合に、仮引数に渡る値が「参照」のまま転送されるのか、コピーなのかムーブなのか確認したいと思います。

検証

(1) A というクラスはコンストラクタやデストラクタでログを記録します。
(2) 関数を受ける関数の型を fn_tfn_cref_t で定義します。

using fn_t      = std::function<void(A)>;
using fn_cref_t = std::function<void(const A&)>;

(3) 関数を引数として受け、その関数を呼び出す call というテンプレート関数を定義します。F には fn_tfn_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_tfn_cref_tstd::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){}; のような場合)、関数オブジェクトとして取り扱う必要があるので全てコンパイルエラーとなります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0