C++: 関数オブジェクトとラムダ
レッスン37では、イテレータについて学びました。
ここでは、STLアルゴリズムの「秘密兵器」——関数オブジェクトについて学びます。
アルゴリズムに、関数オブジェクト。これが、STLの力です。
1. 関数オブジェクトとは
(1) 1.1 関数オブジェクトとは?
関数オブジェクト(Functor)とは、関数呼び出し演算子を持つオブジェクトです。
関数オブジェクトの種類:
- 関数ポインタ
- 関数オブジェクトクラス(
operator()をオーバーロード) - ラムダ式(C++11)
(2) 1.2 なぜ関数オブジェクトが必要?
STLアルゴリズムは比較演算子を使用するものが多いですが、デフォルトの動作をカスタマイズしたい場合があります。関数オブジェクトで独自のルールを定義できます。
たとえ話:
- アルゴリズム = 仕事(比較などの操作)
- 関数オブジェクト = ルール(どのように比較するか:昇順/降順/その他)
2. 関数ポインタ
(1) 2.1 基本的な使い方
▶ サンプル 2:STLコンテナの使用(難易度 ⭐)
#include <iostream>
#include <vector>
#include <algorithm>
// 降順比較関数
bool compareDesc(int a, int b) {
return a > b; // 大きい方を前に
}
int main() {
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
// 関数ポインタでソート
std::sort(v.begin(), v.end(), compareDesc);
for (int x : v) {
std::cout << x << " ";
}
std::cout << std::endl;
return 0;
}
出力:
TEXT 📖 参照専用9 6 5 4 3 2 2 1 1
- 関数ポインタはC言語由来の方法で、C++では関数オブジェクトやラムダの使用が推奨されます
3. 関数オブジェクトクラス
(1) 3.1 関数オブジェクトクラスとは?
関数オブジェクトクラスとは、operator() をオーバーロードしたクラスで、インスタンスを関数のように呼び出せます。
▶ サンプル:カスタム比較(難易度 ⭐⭐)
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
// 文字列長で比較する関数オブジェクト
struct CompareByLength {
bool operator()(const std::string& a, const std::string& b) const {
return a.length() < b.length();
}
};
int main() {
std::vector<std::string> words = {"apple", "banana", "cat", "dog"};
// 文字列長でソート
std::sort(words.begin(), words.end(), CompareByLength());
for (const auto& w : words) {
std::cout << w << " ";
}
std::cout << std::endl;
return 0;
}
出力:
TEXT 📖 参照専用cat dog apple banana
(2) 3.2 関数オブジェクトの利点
| 特徴 | 関数ポインタ | 関数オブジェクトクラス |
|---|---|---|
| 状態保持 | 不可 | 可能(メンバ変数) |
| パフォーマンス | インライン化困難 | インライン化可能、高速 |
| 柔軟性 | 低い | 高い(テンプレート) |
(3) 3.3 状態を持つ関数オブジェクト
▶ サンプル:状態付き関数オブジェクト(難易度 ⭐⭐⭐)
#include <iostream>
#include <algorithm>
#include <vector>
// 閾値を保持する関数オブジェクト
struct Counter {
int threshold; // 閾値(状態)
Counter(int t) : threshold(t) {}
bool operator()(int x) const {
return x > threshold; // 閾値と比較
}
};
int main() {
std::vector<int> v = {1, 5, 10, 15, 20};
// 閾値10以上をカウント
Counter counter(10);
// 10より大きい要素を数える
int count = std::count_if(v.begin(), v.end(), counter);
std::cout << "10より大きい要素数:" << count << std::endl; // 出力:2
return 0;
}
4. ラムダ式
(1) 4.1 ラムダ式とは?
ラムダ式はC++11で導入されたインライン関数で、関数の中で直接定義できます。
基本構文:
[キャプチャ](パラメータ) -> 戻り値型 { 本体 }
| 部分 | 説明 |
|---|---|
キャプチャ |
外部変数のリスト(取り込む変数) |
パラメータ |
引数リスト |
戻り値型 |
戻り値の型(省略可能) |
本体 |
関数本体 |
(2) 4.2 基本的な例
▶ サンプル:ラムダでソート(難易度 ⭐)
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
// ラムダで降順ソート
std::sort(v.begin(), v.end(), [](int a, int b) {
return a > b;
});
for (int x : v) {
std::cout << x << " ";
}
std::cout << std::endl;
return 0;
}
(3) 4.3 キャプチャリスト
キャプチャリストはラムダ内で外部の変数を使用する方法です。
| 構文 | 説明 |
|---|---|
[] |
なにもキャプチャしない |
[x] |
xを値でキャプチャ |
[&x] |
xを参照でキャプチャ |
[=] |
すべての変数を値でキャプチャ |
[&] |
すべての変数を参照でキャプチャ |
[this] |
thisポインタをキャプチャ(クラス内で使用) |
▶ サンプル:キャプチャ付きラムダ(難易度 ⭐⭐)
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> v = {1, 5, 10, 15, 20};
int threshold = 10;
// 閾値をキャプチャしてカウント
int count = std::count_if(v.begin(), v.end(),
[threshold](int x) {
return x > threshold;
});
std::cout << threshold << "より大きい要素数:" << count << std::endl;
return 0;
}
5. STL定義の関数オブジェクト
(1) 5.1 算術関数オブジェクト
<functional> ヘッダで定義されている関数オブジェクト:
| 関数オブジェクト | 機能 | 例 |
|---|---|---|
std::plus<T> |
加算 | std::plus<int>() |
std::minus<T> |
減算 | std::minus<int>() |
std::multiplies<T> |
乗算 | std::multiplies<int>() |
std::divides<T> |
除算 | std::divides<int>() |
std::negate<T> |
符号反転 | std::negate<int>() |
▶ サンプル:multipliesの使用(難易度 ⭐)
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
// 各要素を2倍に変換
std::transform(v.begin(), v.end(), v.begin(),
std::bind(std::multiplies<int>(), std::placeholders::_1, 2));
for (int x : v) {
std::cout << x << " ";
}
std::cout << std::endl;
return 0;
}
(2) 5.2 比較関数オブジェクト
| 関数オブジェクト | 機能 |
|---|---|
std::equal_to<T> |
等価 |
std::not_equal_to<T> |
不等価 |
std::greater<T> |
大なり |
std::less<T> |
小なり |
std::greater_equal<T> |
以上 |
std::less_equal<T> |
以下 |
(3) 5.3 論理関数オブジェクト
| 関数オブジェクト | 機能 |
|---|---|
std::logical_and<T> |
論理積 |
std::logical_or<T> |
論理和 |
std::logical_not<T> |
論理否定 |
6. 総合例
▶ サンプル 1:成績処理システム(難易度 ⭐⭐⭐)
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
struct Student {
std::string name;
int score;
};
int main() {
std::vector<Student> students = {
{"田中", 85},
{"佐藤", 92},
{"鈴木", 78}
};
// 1. 点数でソート
std::sort(students.begin(), students.end(),
[](const Student& a, const Student& b) {
return a.score > b.score;
});
// 2. 最高点の生徒を見つける
auto max_it = std::max_element(students.begin(), students.end(),
[](const Student& a, const Student& b) {
return a.score < b.score;
});
std::cout << "最高点:" << max_it->name << " " << max_it->score << std::endl;
// 3. 及格者数をカウント
int passed = std::count_if(students.begin(), students.end(),
[](const Student& s) {
return s.score >= 60;
});
std::cout << "及格者数:" << passed << std::endl;
return 0;
}
▶ サンプル 3:状態を持つ関数オブジェクト(難易度 ⭐⭐)
#include <iostream>
#include <vector>
#include <algorithm>
// 状態を保持する関数オブジェクト
class ThresholdCounter {
private:
int threshold;
int count;
public:
ThresholdCounter(int t) : threshold(t), count(0) {}
void operator()(int value) {
if (value > threshold) {
count++;
}
}
int getCount() const { return count; }
};
int main() {
std::vector<int> data = {10, 25, 5, 30, 15, 40, 8};
ThresholdCounter counter(20);
// 閾値以上の値をカウント
std::for_each(data.begin(), data.end(), std::ref(counter));
std::cout << "20より大きい値の数: " << counter.getCount() << std::endl;
return 0;
}
出力:
TEXT 📖 参照専用20より大きい値の数: 3
❓ よくある質問
Q:ラムダと関数オブジェクトクラスの使い分けは? A:- 単純・一時的 → ラムダ(コードが簡潔)
- 再利用・状態保持 → 関数オブジェクトクラス(正式な定義)
Q:
autoでラムダ型を宣言できる? A:ラムダは固有の型を持つため、autoを使うのが一般的で、型を明示的に書く必要はありません。
auto func = [](int x) { return x * 2; };
// std::function<int(int)> func = ... // も可能
Q:いつ
std::functionを使う? A:関数オブジェクトを変数に保存する場合(メンバ変数、戻り値など)、std::functionを使います。
📖 まとめ
| トピック | 要点 |
|---|---|
| 関数ポインタ | 単純だが柔軟性に欠ける |
| 関数オブジェクトクラス | 状態を保持可能、パフォーマンスが良い |
| ラムダ式 | インラインで簡潔な関数定義 |
| 定義済み関数オブジェクト | std::plus など、<functional> で提供 |
| キャプチャリスト | ラムダ内で外部変数を使用 |
📝 練習問題
-
初級(難易度 ⭐): 関数オブジェクトクラス(
operator()をオーバーロードしたクラス)を作成し、「降順」比較を実装してください。std::sortでテストしてください。 -
中級(難易度 ⭐⭐):
std::function型を使って、様々な呼び出し可能オブジェクト(関数、ラムダ、関数オブジェクト)を保存し、呼び出してください。 -
上級(難易度 ⭐⭐⭐):
std::bindを使って引数をバインドし、新しい呼び出し可能オブジェクトを作成してください。「部分適用」の関数アダプタを実装してください。
- 関数オブジェクト(ファンクタ):
operator()をオーバーロードしたクラス - 関数オブジェクトは状態を持てる、関数ポインタより柔軟
std::function:型消去された呼び出し可能オブジェクトstd::bind:引数をバインドして新しい呼び出し可能オブジェクトを作成- ラムダ式は関数オブジェクトの簡潔な書き方
次のレッスン:STLアダプタ(#39)