6. 汎用的なアルゴリズムを適用しようとしてみる
では、いよいよ元のプログラムでこれらを使ってみます。
次のようにしたいと思います。
| 要素の型 | Book |
|---|---|
| コレクション | list |
| 抽出条件 | IsMatch(タイトルか出版社名に "リ" を含む) |
| 各要素に対してやること | Show(コンソールに表示、但し Book の出版社名は出版社名リスト内にあるものを表示) |
ところが、単純にIsMatchやShowを作って、汎用的なアルゴリズムであるAppendIf関数やForEach関数に渡そうとしてもうまくいきません。
例えば、次のソースコードの場合、IsMatch関数ではsearchWordとpublisherListがスコープ外、Show関数ではpublisherListがスコープ外となり、コンパイルできません。
書籍のリストの中から、タイトルか出版社名に "リ" を含むものをコンソールに表示(コンパイル不可)
#include <iostream>
#include <list>
#include <vector>
#include <string>
using namespace std;
#include "Book.h"
#include "Publisher.h"
#include "MyCollection.h"
using namespace MyCollection;
class Program
{
public:
void Run()
{
const vector<Publisher> publisherList = {
Publisher(L"技術評論社" ),
Publisher(L"翔泳社" ),
Publisher(L"オライリー・ジャパン"),
Publisher(L"SBクリエイティブ" )
};
const list<Book> bookList = {
Book(L"4774157155", L"C++ ポケットリファレンス" , 0),
Book(L"4798108936", L"C++ の絵本" , 1),
Book(L"4798119768", L"独習 C++ 第4版" , 1),
Book(L"4873110637", L"C++ プログラミング入門" , 2),
Book(L"4797376686", L"C++ テンプレートテクニック 第2版", 3)
};
const wstring searchWord = L"リ";
list<Book> filteredBookList;
AppendIf(bookList, filteredBookList, IsMatch);
ForEach(filteredBookList, Show);
}
private:
static bool IsMatch(const Book& book)
{
// コンパイル エラー! searchWord と publisherList がスコープ外!
return book.GetTitle().find(searchWord) != wstring::npos ||
publisherList[book.GetPublisherIndex()].GetName()
.find(searchWord) != wstring::npos;
}
static void Show(const Book& book)
{
// コンパイル エラー! publisherList がスコープ外!
wcout << L"コード: " << book.GetCode()
<< L", タイトル: " << book.GetTitle()
<< L", 出版社: " << publisherList[book.GetPublisherIndex()].GetName()
<< endl;
}
};
int main()
{
wcout.imbue(locale("Japanese", locale::ctype));
Program().Run();
return 0;
}
どうすれば汎用的なアルゴリズムを使って記述できるのでしょうか。
7. 関数オブジェクトを使った汎用的なアルゴリズムの適用
関数オブジェクト
ラムダ式がなかった頃でも、C++では関数オブジェクトというものを使って、この問題を解決することができました。
上の例の場合、IsMatchやShowの中で、Run関数内のsearchWordやpublisherListが参照できればよいわけです。
そこで、searchWordやpublisherListをカプセル化したオブジェクトを作り、そのオブジェクトを関数の代わりに渡してやるのです。
ただし、渡されたオブジェクトは、AppendIf関数やForEach関数の中で関数のように扱われます。()を付けて「呼ばれる」ことになります。
そこで、オブジェクトにoperator()を用意してやり、その中に関数としての処理を書くようにします。
これが関数オブジェクトと呼ばれるものです。あたかも関数のように()を付けて「呼ぶ」ことができるオブジェクトのことです。
AppendIf関数やForEach関数はtemplateとして書かれていますが、C++のtemplateは高性能なマクロのようなもので、template内がtemplate引数で 置き換えられた結果が実際のプログラムになります。 例えば、上記の関数を渡すところに、「関数に見えるもの」を渡してプログラムとして成立するなら、問題なく使うことができます。 実際に関数であるかどうかではなく、関数のように()を付けて実行できる形になれば良いのです。いわゆるダックタイピング(duck typing)です。
書籍のリストの中から、タイトルか出版社名に "リ" を含むものをコンソールに表示(関数オブジェクト版)
先のプログラムをこの関数オブジェクトを使って書いてみると、例えば次のようになり、無事コンパイルと実行ができるようになります。
#include <iostream>
#include <list>
#include <vector>
#include <string>
using namespace std;
#include "Book.h"
#include "Publisher.h"
#include "MyCollection.h"
using namespace MyCollection;
class Program
{
public:
void Run()
{
const vector<Publisher> publisherList = {
Publisher(L"技術評論社" ),
Publisher(L"翔泳社" ),
Publisher(L"オライリー・ジャパン"),
Publisher(L"SBクリエイティブ" )
};
const list<Book> bookList = {
Book(L"4774157155", L"C++ ポケットリファレンス" , 0),
Book(L"4798108936", L"C++ の絵本" , 1),
Book(L"4798119768", L"独習 C++ 第4版" , 1),
Book(L"4873110637", L"C++ プログラミング入門" , 2),
Book(L"4797376686", L"C++ テンプレートテクニック 第2版", 3)
};
const wstring searchWord = L"リ";
list<Book> filteredBookList;
// 関数オブジェクトとして publisherList への参照と searchWord のコピーを内包した
// IsMatch のインスタンスを渡す
AppendIf(bookList, filteredBookList, IsMatch(publisherList, searchWord));
// 関数オブジェクトとして publisherList への参照を内包した
// Show のインスタンスを渡す
ForEach(filteredBookList, Show(publisherList));
}
private:
// AppendIf 関数に渡す関数オブジェクトのクラス
// (publisherList への参照と searchWord のコピーを内包する)
class IsMatch
{
const vector<Publisher>& publisherList;
wstring searchWord ;
public:
IsMatch(const vector<Publisher>& publisherList, wstring searchWord)
: publisherList(publisherList), searchWord(searchWord)
{}
// 関数のように呼ばれるための operator()
// これを用意することで、オブジェクトの後ろに () を付けて「呼ぶ」ことができる
bool operator()(const Book& book) const
{
// オブジェクトが publisherList への参照と
// searchWord のコピーを内包しているため、問題なく使える
return book.GetTitle().find(searchWord) != wstring::npos ||
publisherList[book.GetPublisherIndex()].GetName()
.find(searchWord) != wstring::npos;
}
};
// ForEach 関数に渡す関数オブジェクトのクラス
// (publisherList への参照を内包する)
class Show
{
const vector<Publisher>& publisherList;
public:
Show(const vector<Publisher>& publisherList)
: publisherList(publisherList)
{}
// 関数のように呼ばれるための operator()
// これを用意することで、オブジェクトの後ろに () を付けて「呼ぶ」ことができる
void operator()(const Book& book) const
{
// オブジェクトが publisherList への参照を内包しているため、問題なく使える
wcout << L"コード: " << book.GetCode()
<< L", タイトル: " << book.GetTitle()
<< L", 出版社: " << publisherList[book.GetPublisherIndex()].GetName()
<< endl;
}
};
};
int main()
{
wcout.imbue(locale("Japanese", locale::ctype));
Program().Run();
return 0;
}
実行結果は、元々のプログラムと同じです。
コード: 4774157155, タイトル: C++ ポケットリファレンス, 出版社: 技術評論社 コード: 4873110637, タイトル: C++ プログラミング入門, 出版社: オライリー・ジャパン コード: 4797376686, タイトル: C++ テンプレートテクニック 第2版, 出版社: SBクリエイティブ
クロージャー
上の関数オブジェクトでは、Run関数内の変数への参照またはコピーを内包することによって、コールバックされたときに、それを利用できるようになっています。
このように、用意されたところでの環境を、呼ばれたときにそのまま利用できる仕組みを、クロージャーと呼びます。
クロージャーは、C++以外の言語にもよく見られる仕組みで、例えば、C# は匿名メソッドやラムダ式、Javaでは無名クラスやラムダ式を、クロージャーとして利用できます。
後述しますが、ラムダ式はC++でもクロージャーとしての利用が可能です。
