C++/標準ライブラリ/functional
はじめに
[編集]関数オブジェクトとは何か
[編集]関数オブジェクトとは、関数呼び出し演算子(operator()
)がオーバーロードされたクラスのオブジェクトのことです。つまり、オブジェクトに対して()
を使って関数のように呼び出すことができます。このように、関数オブジェクトは関数と同じように振る舞うことができます。
#include <iostream> #include <functional> class Funk { public: auto operator()(int x, int y) -> int { return x + y; } }; auto main() -> int { Funk f; std::cout << f(3, 4) << std::endl; // 出力: 7 return 0; }
関数オブジェクトの利点
[編集]関数オブジェクトには次のような利点があります。
- 状態を保持できる
- オブジェクトにメンバー変数を持たせることで、状態を保持できます。関数には状態を持たせられません。
- 引数の値による振る舞いの変化
- オブジェクト内でロジックを記述できるので、引数の値に応じて振る舞いを変更できます。
- テンプレート化が可能
- クラスをテンプレート化できるので、関数オブジェクトもテンプレート化が可能です。
関数オブジェクトの基本的な使い方
[編集]関数オブジェクトは、関数ポインタの代わりに使えます。たとえば、ソート関数の比較関数としてよく利用されます。
#include <algorithm> #include <vector> structDescendingOrder { auto operator()(int a, int b) -> bool { return a > b; } }; auto main() -> int { std::vector<int> v = {3, 1, 4, 1, 5, 9}; std::sort(v.begin(), v.end(), DescendingOrder()); // 降順ソート for (auto x : v) { std::cout << x << " "; // 出力: 9 5 4 3 1 1 } return 0; }
関数ポインタ
[編集]関数ポインタの基礎
[編集]関数ポインタとは、関数の開始アドレスを指すポインタのことです。関数ポインタを使えば、関数を値として渡したり、動的に関数を呼び出したりできます。
#include <iostream> auto add(int x, int y) -> int { return x + y; } auto main() -> int { int (*fptr)(int, int) = &add; // Cと同じ 関数ポインタの宣言 int result = fptr(3, 4); // 関数の呼び出し std::cout << "fptr(3, 4) = " << result << std::endl; auto (*fptr2)(int, int ) -> int = &add; // C+11 以降の形式 result = fptr2(5, 1); // 関数の呼び出し std::cout << "fptr2(5, 1) = " << result << std::endl; auto fptr3 = &add; // 型推論による関数宣言 result = fptr3(4, 7); // 関数の呼び出し std::cout << "fptr3(4, 7) = " << result << std::endl; return 0; }
関数ポインタの宣言と使用方法
[編集]関数ポインタの宣言には、いくつかの異なる構文があります。
- C言語と同じ構文
戻値型 (*ポインタ変数名)(引数の型1, 引数の型2, ...) ;
- C++11で導入された後置戻値型関数宣言構文
auto (*ポインタ変数名)(引数の型1, 引数の型2, ...) -> 戻値型 ;
- 型推論により自動的に型を確定する方法
auto ポインタ変数名 = & 関数名 ;
& 関数名
は関数名
とも書くことが出来ます。decltype
による方法decltype ( 関数名 ) ポインタ変数名 ;
- この方法は、関数ポインタの配列や関数ポインタを返す関数などで有益です。
関数ポインタを使って関数を呼び出す際は、通常の関数呼び出しと同じ構文で行えます。
int result = fptr(3, 4); // fptrは関数ポインタ
関数ポインタと関数オブジェクトの違い
[編集]関数ポインタと関数オブジェクトは、どちらも関数を値として扱えますが、次の点が異なります。
- 関数オブジェクトは状態を保持できるが、関数ポインタはできない
- 関数オブジェクトはテンプレート化が可能だが、関数ポインタはできない
- 関数オブジェクトは、オブジェクトに対する演算子のオーバーロードが可能
- 関数オブジェクトはメモリー・フットプリントが大きく実行にもオーバーヘッドを伴うが、関数ポインタは軽量。
関数オブジェクトクラス
[編集]関数オブジェクトクラスの作成
[編集]関数オブジェクトクラスを作成するには、operator()
をオーバーロードします。引数の型と数は、使用用途に合わせて設計します。
class Funk { public: auto operator()(int x, int y) -> int { return x + y; } };
関数呼び出し演算子のオーバーロード
[編集]operator()
をオーバーロードすることで、関数呼び出しの構文でオブジェクトを呼び出せるようになります。
Funk f; int result = f(3, 4); // f.operator()(3, 4)と同じ
メンバー変数の活用
[編集]関数オブジェクトクラスにメンバー変数を持たせることで、状態を保持できます。実行ごとに振る舞いを変えたい場合に役立ちます。
class Funky { int x; public: Funky(int x_) : x(x_) {} auto operator()(int y) -> int { return x + y; } }; auto main() -> int { Funky f1(3), f2(5); std::cout << f1(4) << std::endl; // 出力: 7 std::cout << f2(4) << std::endl; // 出力: 9 }
std::function
[編集]std::functionクラスの概要
[編集]std::function
は、関数ポインタ、ラムダ式、関数オブジェクトをまとめて扱えるクラステンプレートです。C++11で導入されました。
#include <functional> auto main() -> int { std::function<int(int, int)> f1 = [](int a, int b) { return a + b; }; // ラムダ式 std::function<int(int, int)> f2 = &add; // 関数ポインタ std::function<int(int, int)> f3 = Funk(); // 関数オブジェクト return 0; }
関数ポインタ、ラムダ式、関数オブジェクトの代入
[編集]std::function
オブジェクトに、関数ポインタ、ラムダ式、関数オブジェクトを代入できます。引数の型と個数が一致していれば可能です。
std::function<int(int, int)> f1 = [](int a, int b) { return a + b; }; std::function<int(int, int)> f2 = &add; std::function<int(int, int)> f3 = Funk();
関数の渡し方
[編集]std::function
を関数の引数や返り値として使えば、様々な種類の関数を受け渡しできます。
void call(const std::function<int(int, int)>& f, int a, int b) { std::cout << f(a, b) << std::endl; } auto main() -> int { call([](int a, int b) { return a - b; }, 5, 3); // 出力: 2 call(&add, 5, 3); // 出力: 8 Funk f; call(f, 5, 3); // 出力: 8 return 0; }
バインダ
[編集]std::bindの概要
[編集]std::bind
は、関数オブジェクトに対して、あらかじめ引数の一部を束縛(バインド)できる関数です。結果として、引数の一部が固定された関数オブジェクトが生成されます。
#include <functional> auto add(int a, int b, int c) -> int { return a + b + c; } auto main() -> int { auto f = std::bind(add, 1, 2, std::placeholders::_1); std::cout << f(3) << std::endl; // 出力: 6 (1 + 2 + 3) return 0; }
関数オブジェクトへの引数の部分束縛
[編集]std::bind
の第1引数に関数オブジェクトを渡し、続く引数で部分束縛ができます。
auto f = std::bind(Funk(), 1, std::placeholders::_1); std::cout << f(2) << std::endl; // 出力: 3
プレースホルダーの使用
[編集]束縛しない引数の位置には、std::placeholders::_1
、_2
...を使います。これらはあとから渡される引数に置き換えられます。
auto f = std::bind(add, std::placeholders::_2, std::placeholders::_1, 3); std::cout << f(1, 2) << std::endl; // 出力: 6 (2 + 1 + 3)
関数の合成
[編集]std::composeの概要
[編集]std::compose
は、2つの関数オブジェクトを合成する関数です。合成した関数オブジェクトは、最初の関数の結果を2番目の関数の引数として適用します。
#include <functional> auto square(int x) -> int { return x * x; } auto increment(int x) -> int { return x + 1; } auto main() -> int { auto f = std::compose(square, increment); std::cout << f(5) << std::endl; // 出力: 36 (increment(5)の結果を square に渡す) return 0; }
関数の合成
[編集]std::compose
の第1引数に適用する関数、第2引数に合成される関数を渡します。
auto f = std::compose(関数1, 関数2);
実行時には、関数2
が最初に実行され、その結果を関数1
の引数として適用します。
合成した関数オブジェクトの利用
[編集]合成した関数オブジェクトは、通常の関数呼び出しと同じ構文で使用できます。
std::cout << f(x) << std::endl; // f は合成した関数オブジェクト
その他のユーティリティ
[編集]std::mem_fn
[編集]std::mem_fn
は、クラスメンバー関数をラップし、関数オブジェクトとして振る舞えるようにします。メンバー関数ポインタから関数オブジェクトを生成できます。
#include <functional> #include <vector> struct Data { int x; int triple() const { return x * 3; } }; auto main() -> int { std::vector<Data> v = { {1}, {2}, {3} }; std::transform(v.begin(), v.end(), v.begin(), std::mem_fn(&Data::triple)); // vの各要素に triple() を適用 return 0; }
std::not_fn
[編集]std::not_fn
は、関数オブジェクトの結果を否定(論理否定)します。
#include <functional> auto main() -> int { std::not_fn<std::multiplies<int>> f = std::not_fn(std::multiplies<int>()); std::cout << f(3, 4) << std::endl; // 出力: 0 (3 * 4 の結果を否定) return 0; }
std::ref、std::cref
[編集]std::ref
とstd::cref
は、引数の参照を取得するためのヘルパー関数です。std::bind
と一緒に使うと便利です。
#include <functional> auto add(int a, int& b) -> int { return a + b; } auto main() -> int { int x = 1, y = 2; auto f = std::bind(add, x, std::ref(y)); y = 3; std::cout << f() << std::endl; // 出力: 4 (1 + 3) return 0; }
ラムダ式
[編集]ラムダ式の基本形
[編集]ラムダ式は、無名の関数オブジェクトを簡潔に記述できる仕組みです。C++11で導入されました。
[キャプチャ](パラメーターリスト) -> 戻値型 { 関数ボディ }
以下は、2つの引数の積を返すラムダ式の例です。
auto f = [](int a, int b) -> int { return a * b; }; std::cout << f(3, 4) << std::endl; // 出力: 12
ラムダキャプチャ
[編集]ラムダ式は、外側のスコープの変数をキャプチャ(参照、値渡し)できます。[=]
で値渡し、[&]
で参照渡しが可能です。
int x{1}; auto f1 = [=] { return x + 1; }; // xをコピー auto f2 = [&] { ++x; return x; }; // xを参照
ジェネリックラムダ
[編集]C++14から導入されたジェネリックラムダでは、型パラメータを使えます。
auto f = [](auto a, auto b) { return a + b; }; std::cout << f(1, 2) << std::endl; // 出力: 3 std::cout << f(1.2, 3.4) << std::endl; // 出力: 4.6
ラムダ式と関数オブジェクトの違い
[編集]ラムダ式は、簡潔に関数オブジェクトを記述できる点で利点があります。一方、関数オブジェクトクラスにはメンバー変数を持たせられるなど、より柔軟性があります。用途に合わせて使い分けましょう。
関数オブジェクトとカリー化
[編集]関数オブジェクトは、カリー化と密接な関係があります。カリー化とは、複数の引数を取る関数を、一つの引数を取る関数の連続として表現する技法です。
カリー化の例
[編集]カリー化を理解するには、まず通常の関数と比較する必要があります。以下の例では、足し算の関数を通常の方法とカリー化された方法で定義しています。
// 通常の方法 auto add(int x, int y) -> int { return x + y; } // カリー化された方法 auto curried_add = [](int x) { return [=](int y) { return x + y; }; };
curried_add
は、整数xを受け取り、別の関数を返します。この返された関数は、整数yを受け取り、xとyの和を返します。
カリー化された関数を呼び出すには、次のように2回に分けて引数を渡します。
auto add_5 = curried_add(5); // add_5は、yを受け取り、5+yを返す関数 int result = add_5(3); // 結果は8
カリー化と関数オブジェクト
[編集]カリー化は、関数オブジェクトと密接に関係しています。実際、上記のcurried_add
は、ラムダ式によって定義された関数オブジェクトです。
関数オブジェクトは、状態を持つことができるため、カリー化された関数の最初の引数の値を内部で保持できます。これにより、後続の呼び出しで、その値を使用できます。
std::bindによるカリー化
[編集]std::bind
を使えば、簡単にカリー化された関数オブジェクトを作ることができます。
#include <functional> auto add(int x, int y) -> int { return x + y; } auto main() -> int { auto add_5 = std::bind(add, 5, std::placeholders::_1); std::cout << add_5(3) << std::endl; // 出力: 8 return 0; }
この例では、std::bind
を使って引数xを5
に束縛し、残りの引数yをstd::placeholders::_1
で表しています。add_5
は、5
と引数の和を計算する関数オブジェクトになります。
カリー化の利点
[編集]カリー化には、以下のような利点があります。
- 部分適用により、より一般的な関数から特殊なケースを簡単に作れる
- 引数の順序を入れ替えたり、デフォルト値を設定したりできる
- 関数合成が簡単になる
カリー化は、関数型プログラミングの重要な概念です。関数オブジェクトを使うことで、C++でもカリー化を活用できます。
関数オブジェクトにおけるキャプチャ
[編集]ラムダ式ではキャプチャリストを使ってキャプチャできますが、関数オブジェクトの場合は、コンストラクタやメンバー変数を使って同様の機能を実現します。
#include <iostream> #include <functional> class Closure { int x; // キャプチャする変数 public: Closure(int x_) : x(x_) {} auto operator()(int y) const -> int { return x + y; // xをキャプチャして使用 } }; auto main() -> int { int a{1}; Closure c(a); // aをキャプチャ std::cout << c(3) << std::endl; // 出力: 4 a = 2; // aを変更 std::cout << c(3) << std::endl; // 出力: 4 (キャプチャした値は変わらない) return 0; }
この例では、Closure
クラスのコンストラクタでxをキャプチャし、operator()
の中で使用しています。キャプチャした値は、オブジェクトのメンバー変数に保存されるので、外側のスコープの変数が変更されても影響を受けません。
またメンバー変数への参照や、std::bind
を使ってキャプチャすることもできます。
auto main() -> int { int a{1}; Closure c = std::bind(Closure(), std::ref(a)); // aへの参照をキャプチャ std::cout << c(3) << std::endl; // 出力: 4 a = 2; // aを変更 std::cout << c(3) << std::endl; // 出力: 5 (参照されているので値が変わる) return 0; }
このようにして、関数オブジェクトでもクロージャ(状態を保持した関数)を作成できます。関数オブジェクトの柔軟性を生かして、様々な方法でキャプチャが可能です。
まとめ
[編集]関数オブジェクトの利点のまとめ
[編集]- 状態を保持できる
- 引数の値によって振る舞いを変化させられる
- テンプレート化が可能
std::function
を使えば、様々な関数を受け渡しできるstd::bind
で引数の部分束縛ができるstd::compose
で関数の合成ができるstd::mem_fn
でメンバー関数をラップできるstd::not_fn
で関数の結果を否定できる
このように、関数オブジェクトとそれをサポートする関数群は、C++プログラミングにおいて非常に高い汎用性と表現力を与えてくれます。
実際のユースケースの例
[編集]関数オブジェクトは、STLアルゴリズムの汎用性を高める重要な役割を果たしています。たとえば、std::sort
の比較関数オブジェクトとして使えます。
struct Student { std::string name; int score; }; // scoreで昇順ソート std::sort(students.begin(), students.end(), [](const Student& a, const Student& b) { return a.score < b.score; });
GUIプログラミングでは、ボタンクリックなどのイベントハンドラとして関数オブジェクトが活躍します。
button->setClickHandler([=](){ // ボタンクリック時の処理 ... });
最新の機能的プログラミングのライブラリrange-v3
では、多様な関数オブジェクトが使われています。
#include <range/v3/all.hpp> auto main() -> int { const std::vector<int> v = {1,2,3,4,5}; const auto rng = v | ranges::view::transform([](int x) { return x * x; }); // rngは { 1, 4, 9, 16, 25 } return 0; }
関数オブジェクトは、C++の様々な場面で柔軟性とパワーを発揮してくれる重要な概念です。上手く活用していきましょう。