C++: コンテナアダプタ
レッスン38では、関数オブジェクトについて学びました。
ここでは、関数アダプタについて学びます——既存の関数を「改造」して必要な形に変換します。
レゴブロックのように、小さな部品を組み合わせて大きな作品を作ります。
1. アダプタの概要
(1) 1.1 アダプタとは?
アダプタ(Adapter)とは、関数オブジェクトの振る舞いを変更するテンプレートです。
主なアダプタ:
std::bind(引数のバインド)std::ref(参照渡し)std::not_fn(論理否定)std::mem_fn(メンバ関数ポインタ)
2. std::bind——引数のバインド
(1) 2.1 基本的な使い方
std::bind は関数の引数をバインドし、新しい関数オブジェクトを作成します。
▶ サンプル 2:コード例(難易度 ⭐)
#include <iostream>
#include <functional>
int add(int a, int b) {
return a + b;
}
int main() {
// addの第一引数を10にバインド
auto add10 = std::bind(add, 10, std::placeholders::_1);
std::cout << "add10(5) = " << add10(5) << std::endl; // 出力:15
std::cout << "add10(20) = " << add10(20) << std::endl; // 出力:30
return 0;
}
std::placeholders::_1は「後で埋める第一引数」を表す
(2) 2.2 引数の順序を変更
▶ サンプル:引数順序の入れ替え(難易度 ⭐⭐)
#include <iostream>
#include <functional>
int subtract(int a, int b) {
return a - b;
}
int main() {
// 引数の順序を入れ替え
auto reverse_subtract = std::bind(subtract,
std::placeholders::_2,
std::placeholders::_1);
std::cout << "subtract(10, 3) = " << subtract(10, 3) << std::endl; // 7
std::cout << "reverse(10, 3) = " << reverse_subtract(10, 3) << std::endl; // -7
return 0;
}
3. std::ref——参照渡し
(1) 3.1 問題:値渡しによる状態消失
デフォルトでは、STLアルゴリズムは値渡しで関数オブジェクトをコピーするため、状態が共有されません。
▶ サンプル:std::refで解決(難易度 ⭐⭐)
#include <iostream>
#include <algorithm>
#include <vector>
#include <functional>
struct Counter {
int count = 0;
void operator()(int) { count++; }
};
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
Counter counter;
// ❌ エラー!値渡しなので、counterの状態は更新されない
std::for_each(v.begin(), v.end(), counter);
std::cout << "値渡し後:" << counter.count << std::endl; // 出力:0
// ✅ 正しい!std::refで参照渡し
std::for_each(v.begin(), v.end(), std::ref(counter));
std::cout << "参照渡し後:" << counter.count << std::endl; // 出力:5
return 0;
}
4. std::not_fn——論理否定
(1) 4.1 基本的な使い方
std::not_fn は関数オブジェクトの戻り値を否定します。
▶ サンプル:述語の否定(難易度 ⭐⭐)
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
// 最初の偶数を見つける
auto it1 = std::find_if(v.begin(), v.end(),
[](int x) { return x % 2 == 0; });
std::cout << "最初の偶数:" << *it1 << std::endl; // 2
// 最初の奇数を見つける(否定)
auto it2 = std::find_if(v.begin(), v.end(),
std::not_fn([](int x) { return x % 2 == 0; }));
std::cout << "最初の奇数:" << *it2 << std::endl; // 1
return 0;
}
5. std::mem_fn——メンバ関数ポインタ
(1) 5.1 問題:メンバ関数ポインタの複雑さ
メンバ関数ポインタは構文が複雑ですが、std::mem_fn を使うと簡潔になります。
▶ サンプル:メンバ関数の呼び出し(難易度 ⭐⭐⭐)
#include <iostream>
#include <vector>
#include <algorithm>
#include <memory>
struct Student {
std::string name;
void display() const {
std::cout << "Student: " << name << std::endl;
}
};
int main() {
std::vector<Student> students = {{"田中"}, {"佐藤"}};
// std::mem_fnでメンバ関数を呼び出し
std::for_each(students.begin(), students.end(),
std::mem_fn(&Student::display));
return 0;
}
6. 総合例
▶ サンプル 1:柔軟な成績処理(難易度 ⭐⭐⭐)
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
int main() {
std::vector<int> scores = {85, 92, 78, 90, 88};
int threshold = 90;
// 閾値以上のスコアをカウント
int count = std::count_if(scores.begin(), scores.end(),
std::bind(std::greater_equal<int>(),
std::placeholders::_1,
threshold));
std::cout << threshold << "点以上の人数:" << count << std::endl;
return 0;
}
▶ サンプル 3:std::bindでカスタム比較(難易度 ⭐⭐)
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
int main() {
std::vector<int> values = {15, 8, 23, 4, 16, 42, 7};
// std::bindを使って20より大きい最初の値を見つける
auto it = std::find_if(values.begin(), values.end(),
std::bind(std::greater<int>(), std::placeholders::_1, 20));
if (it != values.end()) {
std::cout << "20より大きい最初の値: " << *it << std::endl;
}
return 0;
}
出力:
TEXT 📖 参照専用20より大きい最初の値: 23
❓ よくある質問
Q:C++11以降もstd::bindは必要? A:はい、しかしラムダの使用が推奨されます。ラムダの方が簡潔で、パフォーマンスも良いです。
// std::bind
auto f1 = std::bind(add, 10, std::placeholders::_1);
// ラムダ(推奨)
auto f2 = [](int x) { return add(10, x); };
Q:いつアダプタを使う? A:既存の関数がほぼ要件を満たすが、引数が合わない場合。
Q:priority_queueで最小ヒープを作るには? A:
std::greater<T>を第2テンプレート引数に指定します:CPPstd::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
Q: STLアダプタについて最も重要なことは何ですか? A: まず基本概念を理解し、それから実践的な例で練習することです。
📖 まとめ
| アダプタ | 機能 |
|---|---|
std::bind |
引数のバインド |
std::ref |
参照渡し |
std::not_fn |
論理否定 |
std::mem_fn |
メンバ関数ポインタ |
📝 練習問題
-
初級(難易度 ⭐):
stack<int>を作成し、1、2、3を順にプッシュし、ループでポップしてすべての要素を出力してください。出力順序を観察してください。 -
中級(難易度 ⭐⭐):
queueを使って「印刷ジョブキュー」を実装してください——複数の印刷ジョブが順番に処理されるのをシミュレートし、各ジョブ処理後に残りのキューサイズを出力してください。 -
上級(難易度 ⭐⭐⭐):
priority_queueを使って「タスクスケジューラ」を実装してください——各タスクは優先度(1〜10)を持ち、優先度が高い順に処理し、同じ優先度なら挿入順で処理します。
- アダプタ:stack/queue/priority_queue は基礎コンテナをカプセル化
- stack(LIFO):push/pop/top
- queue(FIFO):push/pop/front/back
- priority_queue(優先度キュー):最大ヒープ
- アダプタはテンプレートパラメータで基礎コンテナを指定可能
次のレッスン:例外処理(#40)