LoginSignup
21
19

More than 5 years have passed since last update.

std::functionでのメンバ関数キャプチャの罠

Last updated at Posted at 2017-03-04

「std::functionを使うとメンバ関数を普通の関数みたいに呼べる!」という情報を見つけた。
自分もメンバ関数をそういう感じで扱いたかったのでやり方を調べてみた。

まず、よく紹介されているのが以下の方法。

#include <iostream>
#include <functional>

class Hoge {
    int val;
public:
    Hoge(int v) : val{v} {}
    int GetVal(void) const { return val; }
};

int main(void) {
    Hoge hoge(1);
    std::function<int()> GetValOfHoge = std::bind(&Hoge::GetVal, &hoge);
    std::cout << GetValOfHoge() << std::endl;
}

/* result:
1
*/

std::bindで第一引数にオブジェクトのポインタをバインドする。
ちなみに引数付きの関数をキャプチャするには、

/* こんな関数の場合 */
   int Func(int, int, int) {}

/* こんな感じ */
    std::function<int(int, int, int)> FuncOfHoge = std::bind(
        &Hoge::Func,
        &hoge,
        std::placeholders::_1,
        std::placeholders::_2,
        std::placeholders::_3);

このようにそれぞれの引数をstd::placeholdersを使ってバインドする。ちょっと冗長だが、仕方ない。

std::functionにキャプチャすると何がいいかというと、関数の引数に渡せたりだとか、コンテナにしまっておけたりだとかする。
例えばstd::vectorに格納しておいてまとめて呼び出す、いわゆるC#のマルチキャストデリゲートみたいな感じなことができる。(今回自分がしたかったのはこれ)


さて、これでメンバ関数を含めいろんなところに取りまわせるようになってめでたしめでたし…となるところだが、std::functionに関数キャプチャする方法ってもう一つあるよね。
みんな大好きラムダ式を使った方法。

#include <iostream>
#include <functional>

class Hoge {
    int val;
public:
    Hoge(int v) : val{v} {}
    int GetVal(void) const { return val; }
};

int main(void) {
    Hoge hoge(1);
    std::function<int()> GetValOfHoge = [&]() { return hoge.GetVal(); };
    std::cout << GetValOfHoge() << std::endl;
}
/* result:
1
*/

ラムダ式で外部のオブジェクトをキャプチャして、それを呼び出す。
直接的ではないけど、機能はさっきのstd::bindを使ったものとほとんど変わらない。

さて、2つ方法があるなら速いほうを使いたいよね。ということで測ってみる。

#include <iostream>
#include <chrono>
#include <functional>

class Hoge {
    int val;
public:
    Hoge(int v) : val{v} {}
    int GetVal(void) const { return val; }
};

constexpr int LOOP_COUNT = 1000000;

int main(void) {
    using std::chrono::nanoseconds;
    using std::chrono::duration_cast;
    using std::chrono::high_resolution_clock;

    Hoge hoge(1);
    std::function<int()> GetVal_Bind = std::bind(&Hoge::GetVal, &hoge);
    std::function<int()> GetVal_Lambda = [&]() { return hoge.GetVal(); };

    //Measure std::bind
    auto start = high_resolution_clock::now();
    for(int i = 0; i < LOOP_COUNT; i++)
        GetVal_Bind();
    auto end = high_resolution_clock::now();

    int count = duration_cast<nanoseconds>(end - start).count();

    std::cout << "std::bind" << std::endl;
    std::cout << "total:" << count << "ns" << std::endl;
    std::cout << "mean:" << (float)count / (float)LOOP_COUNT << "ns" << std::endl;

    //Measure lambda
    start = high_resolution_clock::now();
    for(int i = 0; i < LOOP_COUNT; i++)
        GetVal_Lambda();
    end = high_resolution_clock::now();

    count = duration_cast<nanoseconds>(end - start).count();

    std::cout << "lambda" << std::endl;
    std::cout << "total:" << count << "ns" << std::endl;
    std::cout << "mean:" << (float)count / (float)LOOP_COUNT << "ns" << std::endl;


    return 0;
}

結果は以下。

std::bind
total:71158144ns
mean:71.1581ns
lambda
total:36223729ns
mean:36.2237ns

意外に、ラムダ式のほうが2倍近く速い。(なお最適化は切ってある)
ということでstd::functionにメンバ関数をキャプチャするときはラムダ式を使おうと思う。(メモリの消費が如何程か気になるところではあるが…)

まとめ

巷ではメンバ関数をキャプチャするのにstd::bindを使った方法がよく紹介されるが、実はラムダ式で間接的にキャプチャしたほうが速い。

最後に、一応検証環境を載せておく。

  • CPU:Intel Xeon CPU E5640
  • メモリ:DDR3 12GB
  • OS: Windows10
  • コンパイラ:MSVC 19.00.24215.1 (VS2015付属のcl.exe)
21
19
4

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
21
19