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

関数オブジェクト

関数オブジェクト(function object)は、関数のように振る舞うことのできるオブジェクトのことです。関数オブジェクトは多くの場合、クラスに対して関数呼び出し演算子を定義することで実現されます。C++ではoperator()メンバ関数のオーバロードによってそれを実現します。

// クラス
struct Func {
   // 関数呼び出し演算子
   void operator()() { printf("hello"); }
};

// ファンクタ
Func object = Func();
object(); // "hello"

オブジェクト指向言語よっては、特定のインターフェースへの準拠や、特定のメソッド名(call, run)に対するメソッド呼び出しを規定することで、関数オブジェクトの振る舞いを実現している物もあります。

C++では様々な方法でこの関数オブジェクトを扱う事ができます。関数オブジェクトを変数に格納したり、引数として渡したり、テンプレートやクラスのメンバとして渡したりすることも可能です。関数オブジェクトはC++の標準ライブラリを活用する際にも頻繁に利用されます。

C++では以下に挙げる様々な方法で関数オブジェクトを扱うことができます。

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

ラムダ式

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

ラムダ式による関数オブジェクトは[](引数) { 処理 }形式で定義し、autoで保持することができます。

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

無名関数の即時実行やクロージャーを実現することもできます。

int i = 0;
[&] { i = 9; }();
i; // 9

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

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

std::vector<int> v = {1, 2, 3, 4};
std::for_each(v.begin(), v.end(), [](int i) {
  printf("%d", 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を活用する必要があります。より詳しい説明は以下のページを参考にしてください。

参考: 【C++】関数を引数に渡す色々な方法【STL テンプレート 関数ポインタ】

ファンクタ

クラスの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を渡します。

bind関数は非推奨となったbind1st/bind2nd関数の代わりとして使うことができます。

ラップされたオブジェクトを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(isdigit, '9');
fn(); // 1 (true)

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

std::bind関数のより詳しい解説やと特性については、以下の記事が参考になります。

std::bind関数|引数の束縛と部分適用 – bind1st/bind2ndからの移行
広告