【C++】色々な関数オブジェクト【ラムダ ファンクタ 関数ポインタ】


C++では関数をオブジェクトとして扱う事が出来ます。関数を変数に格納したり、引数として渡したり、テンプレートに渡したりすることも可能になります。

C++では色々な方法でこの関数オブジェクト(function object)を扱うことが出来ます。

最近は# ラムダ式が頻繁に用いられています。C++で古くから用いられている関数オブジェクトとしては# ファンクタが有名です。

ラムダ式

ラムダ式(lambda expression)は関数オブジェクトを簡易的に定義するための機能です。プログラム中の様々なタイミングで定義が可能です。

ラムダ式はautoで保持することが出来ます。

auto fn = [](int v) { return v * 2; };
fn(3); // 6

無名関数やクロージャーを実現することも出来ます。

int i = 0;
[&] { i = 9; }();
std::cout << i; // 9

int j = 0;
auto fn = [&j] { return j++; };
std::cout << fn(); // 0
std::cout << fn(); // 1

ラムダ式はアルゴリズム系の標準ライブラリでも活用出来ます。

std::vector<int> v = {1, 2, 3, 4};
std::for_each(v.begin(), v.end(), [](int i) {
  std::cout << i;
}); // 1234

ラムダ式の評価結果であるクロージャーオブジェクト(closure object)は関数ポインタへの変換機能を持つため、C言語APIの比較関数やコールバック関数として活用することも出来ます。

char a[] = {'c', 'b', 'a'};
qsort(a, sizeof a, sizeof *a, [](const void *l, const void *r) {
  return *(char*)l - *(char*)r;
});
a; // {'a', 'b', 'c'}

関数オブジェクトを引数としてやり取りする際には、テンプレートや# std::functionを活用する必要があります。より詳しい説明は以下のページを参考にしてください。

関数を引数に渡す方法

ファンクタ

クラスのoperator()演算子を多重定義/オーバロードすることで、クラスのインスタンスに対して関数と同等の振る舞いをさせることが出来ます。このような関数オブジェクトはファンクタ(functor)と呼ばれています。

struct Functor {
  int operator()(int v) { return v * 2; }
};
Functor fn = Functor();
fn(3); // 6

メンバ変数を活用すれば、関数に状態を持たせることも可能になります。

struct Functor {
  int i;
  Functor(int i) : i(i) {}
  int operator()(int v) { return v * i++; }
};
Functor fn = Functor(1);
fn(3); // 3
fn(3); // 6
fn(3); // 9
実は、ラムダ式のキャプチャ機能は、このファンクタとメンバ変数の仕組みを応用した形で実現されていたりします。

ファンクタはラムダ式と同様に標準ライブラリとともに活用することが出来ます。

struct Print {
  void operator()(int i) { std::cout << i; }
};
std::vector<int> v = {1, 2, 3, 4};
std::for_each(v.begin(), v.end(), Print()); // 1234

一昔前まではC++の関数オブジェクトと言えばこのファンクタのことを指していました。

関数ポインタ

関数ポインタ(function pointer)は関数へのポインタです。関数ポインタは厳密には関数オブジェクトではないのですが、用途によっては関数オブジェクトと同等に扱うこともできるため、注意点とともに紹介しておきます。

int function(int v) { return v * 2; }
int(*fn)(int) = function;
fn(3); // 6

標準ライブラリは関数ポインタにも対応しています。

const char* a[] = {"hello", "world"};
std::for_each(std::begin(a), std::end(a), puts); // hello world

C言語との連携を意識する際には関数オブジェクトの代わりにこちらの関数ポインタを用いたAPI設計を採ると良いでしょう。

関数の定義方法にもよりますが、関数ポインタによる関数のやり取りは、コンパイラ側の最適化の恩恵を受けられなくなる場合がありますので、基本的には先程紹介したラムダ式やファンクタを用いるがベストです。

ブロック

ブロック(Blocks)はApple製コンパイラ(Apple LLVM)のC言語拡張です。利用用途はラムダ式とほぼ同じです。

int (^fn)(int) = ^(int v) { return v * 2; };
fn(3); // 6

クロージャーとしての利用も可能です。

__block const char* s = NULL;
^{ s = "hello"; }();
puts(s); // "hello"

やはり標準ライブラリとのやり取りが可能です。

std::vector<int> v = {1, 2, 3, 4};
std::for_each(v.begin(), v.end(), ^(int i) {
    std::cout << i;
}); // 1234

std::function

関数オブジェクトをstd::functionクラスで保持することも可能です。

std::function<int(int)> fn = [](int v) { return v * 2; };
fn(3);    // 6

std::function<int(int)> gn = isspace;
gn('\n'); // 1 (true)

引数型として利用すれば、関数オブジェクトを関数間でやり取りすることが可能になります。

std::function<int(int)> get() { return [](int v) { return v * 2; }; }
int call(std::function<int(int)> fn) { return fn(3); }
int v = call(get()); // 6

std::functionはラムダ式だけでなく、先程の# ファンクタ# 関数ポインタ# ブロックを保持することも可能です。

std::function<int(int)> functor = Functor();
functor(3); // 6

std::function<int(int)> funcPtr = function;
funcPtr(3); // 6

std::bind

std::bindで関数オブジェクトをラップする方法もあります。

auto fn = std::bind([](int v) { return v * 2; }, std::placeholders::_1);
fn(3); // 6
~~~
> std::placeholders::_1は第一引数の存在を示しています。第二引数が必要な場合は続けてstd::placeholders::_2を渡します。

ラップされたオブジェクトを`std::function`に変換することも可能です。
// (変換コンストラクタが働く)
~~~
std::function<int(int)> fn = std::bind([](int v) { return v * 2; }, std::placeholders::_1);
fn(3); // 6

ファンクタや関数ポインタを保持することも可能です。

auto fn_Functor = std::bind(Functor(), std::placeholders::_1);
fn_Functor(3); // 6

auto fn_FuncPtr = std::bind(function, std::placeholders::_1);
fn_FuncPtr(3); // 6

std::bind構築時、事前に引数を与えることも可能です。

auto fn = std::bind([](int v) { return v * 2; }, 3);
fn(); // 6

auto fn = std::bind([](auto& f) { return 1 + f(); }, []() { return 2; });
fn(); // 3

広告