241 Views
September 27, 24
スライド概要
CEDEC2015の講演資料です。
https://cedec.cesa.or.jp/2015/session/ENG/1872.html
クロージャデザインパターン C++11ラムダ式によるデザインパターン 日本工学院八王子専門学校 大圖 衛玄
自己紹介 1992年~1997年 某ゲーム会社 プログラマ SFC,GB,PS1,N64のゲーム開発経験 1998年~現在 @mozmoz1972 日本工学院八王子専門学校 専任講師 プログラミング教育を中心に担当 CEDEC2015で講演したスライド資料に解説を加えたものです。
http://www.slideshare.net/MoriharuOhzu/ss-9224836 CEDEC2011講演。
http://www.slideshare.net/MoriharuOhzu/ss-14083300 CEDEC2012講演。
https://www.youtube.com/watch?v=HooktFpS5CA CEDEC2014ではハートランド・データ様との共同講演。スライドに文字がありませんので動画でご覧ください。
6月に執筆した書籍が発売されました!
「オブジェクト指向できていますか?」の最後のスライド。本セッションの予告のつもりでした。しかし、CEDEC2013の公募で落選。
HEY!閉包! Closure Design Patterns 日本工学院八王子専門学校 大圖 衛玄 公募の採択を願ってインパクトのあるセッション名で再チャレンジ。しかし、わかりずらいとの理由で変更を余儀なくされました・・・
#01 Lambda Expressions C++11のラムダ式の基本文法をおさらいします。ちなみに写真は伝説のλシールドです。
void sample1() {
// 関数内に関数を作成
auto add = [](int a, int b) -> int { return a + b; };
std::cout << add(10, 20) << std::endl;
}
ラムダ式の概要を説明します。
void sample1() {
// 関数内に関数を作成
auto add = [](int a, int b) -> int { return a + b; };
std::cout << add(10, 20) << std::endl;
}
ラムダ式を使うと関数内に関数を作成できます。
void sample1() {
// 関数内に関数を作成
auto add = [](int a, int b) -> int { return a + b; };
std::cout << add(10, 20) << std::endl;
}
void sample2() {
std::vector<int> = {1, 2, 3, 4, 5};
// 関数の引数内に関数を作成
std::for_each(nums.begin(), nums.end(),
[](int n) { std::cout << n << std::endl; });
}
関数の引数内に関数を作ることもできます。おもに関数内で一時的に使用する小さな関数を作成する機能だと考えてください。
[]{}; 何もしないラムダ式 何もしない最小限のラムダ式です。
[]{}; 何もしないラムダ式 [](int a) { std::cout << a; }; 引数あり 引数がある場合は、()の中に引数を書きます。
[]{};
何もしないラムダ式
[](int a) { std::cout << a; };
引数あり
[](int a) -> int { return a * 10; };
引数あり、戻り値あり
戻り値がある場合は->の後ろに戻り値の型を書きます。
[]{};
何もしないラムダ式
[](int a) { std::cout << a; };
引数あり
[](int a) -> int { return a * 10; };
引数あり、戻り値あり
[n](int a) -> int { return a * n; };
変数nをコピーキャプチャ
変数キャプチャはラムダ式の外側にある変数を取込む機能です。コピーキャプチャは変数の内容をコピーして取込みます。
[]{};
何もしないラムダ式
[](int a) { std::cout << a; };
引数あり
[](int a) -> int { return a * 10; };
引数あり、戻り値あり
[n](int a) -> int { return a * n; };
変数nをコピーキャプチャ
[=](int a) -> int { return a * n; };
デフォルトコピーキャプチャ
[=]と書くと、キャプチャする変数はすべてコピーして取込むという意味になります。
[]{};
何もしないラムダ式
[](int a) { std::cout << a; };
引数あり
[](int a) -> int { return a * 10; };
引数あり、戻り値あり
[n](int a) -> int { return a * n; };
変数nをコピーキャプチャ
[=](int a) -> int { return a * n; };
デフォルトコピーキャプチャ
[=](int a) { return a * n; };
戻り値型の省略 (return文のみの場合)
return文だけのラムダ式であれば戻り値型の省略ができます。
[n]() { std::cout << ++n; }; コピーキャプチャの変数は変更不可 コピーキャプチャした変数は通常変更できません。この例はコンパイルエラーとなります。
[n]() { std::cout << ++n; }; コピーキャプチャの変数は変更不可 [n]() mutable { std::cout << ++n; }; mutableで変更可能 mutableキーワードを追加すれば、コピーキャプチャした変数を変更できます。(コピー元の変数は変更されません)
[n]() { std::cout << ++n; }; コピーキャプチャの変数は変更不可 [n]() mutable { std::cout << ++n; }; mutableで変更可能 [&n]() { std::cout << ++n; }; 参照キャプチャ &をつけると参照でキャプチャします。アドレスをキャプチャするので、取り込んだ元の変数の内容も変更されます。
[n]() { std::cout << ++n; }; コピーキャプチャの変数は変更不可 [n]() mutable { std::cout << ++n; }; mutableで変更可能 [&n]() { std::cout << ++n; }; 参照キャプチャ [&]() { std::cout << ++n; }; デフォルト参照キャプチャ [&]とすると変数をすべて参照でキャプチャします。
[n]() { std::cout << ++n; }; コピーキャプチャの変数は変更不可 [n]() mutable { std::cout << ++n; }; mutableで変更可能 [&n]() { std::cout << ++n; }; 参照キャプチャ [&]() { std::cout << ++n; }; デフォルト参照キャプチャ [a, &n]() { return a * ++n; }; 個別指定でキャプチャ可能 コピーと参照は個別に指定できます。この例の場合はaはコピー、nは参照でキャプチャします。
[n]() { std::cout << ++n; }; コピーキャプチャの変数は変更不可 [n]() mutable { std::cout << ++n; }; mutableで変更可能 [&n]() { std::cout << ++n; }; 参照キャプチャ [&]() { std::cout << ++n; }; デフォルト参照キャプチャ [a, &n]() { return a * ++n; }; 個別指定でキャプチャ可能 [a = b](auto n) { return a * n; }; C++14 初期化キャプチャ・ジェネリックラムダ C++14では初期化キャプチャと引数の型を推論するジェネリックラムダが追加されました。
void lambda_sample(std::vector<int>& nums, int a) {
std::for_each(nums.begin(), nums.end(),
[a](int n) { std::cout << n * a << std::endl; });
}
ラムダ式の正体について説明します。このラムダ式は実際にどのような扱いになるのでしょうか?
void lambda_sample(std::vector<int>& nums, int a) {
std::for_each(nums.begin(), nums.end(),
[a](int n) { std::cout << n * a << std::endl; });
}
void lambda_sample(std::vector<int>& nums, int a) {
class lambda {
int a_; // キャプチャした変数
public:
lambda(int a) : a_(a) {}
コンパイラが
関数オブジェクトを生成
void operator()(int n) const {
std::cout << n * a_ << std::endl;
}
};
std::for_each(nums.begin(), nums.end(), lambda(a));
}
変数キャプチャをするラムダ式は、無名の関数オブジェクトに変換されると考えてください。(この例はイメージです)
void lambda_sample(std::vector<int>& nums) {
std::for_each(nums.begin(), nums.end(),
[](int n) { std::cout << n << std::endl; });
}
変数キャプチャなし
変数キャプチャをしないラムダ式はどうなるのか?
void lambda_sample(std::vector<int>& nums) {
std::for_each(nums.begin(), nums.end(),
[](int n) { std::cout << n << std::endl; });
}
変数キャプチャなし
void lambda_sample(std::vector<int>& nums) {
void lambda(int n) {
変数キャプチャなしの
std::cout << n << std::endl;
ラムダ式は通常の関数と同じ扱い
}
std::for_each(nums.begin(), nums.end(), &lambda);
}
※イメージです
変数キャプチャをしないラムダ式は単純な関数と同じ扱いになります。
void lambda_sample(std::vector<int>& nums) {
std::for_each(nums.begin(), nums.end(),
[](int n) { std::cout << n << std::endl; });
}
変数キャプチャなし
void lambda_sample(std::vector<int>& nums) {
void lambda(int n) {
変数キャプチャなしの
std::cout << n << std::endl;
ラムダ式は通常の関数と同じ扱い
}
std::for_each(nums.begin(), nums.end(), &lambda);
}
※イメージです
auto lambda = [](int n) { std::cout << n << std::endl; };
void (*fun_ptr)(int) = lambda;
変数キャプチャなしのラムダ式は
fun_ptr(10);
C言語の関数ポインタに代入可能
変数キャプチャをしないラムダ式はC言語の関数ポインタに代入できます。
void lambda_sample(std::vector<int>& nums, int a) {
int b = 10;
std::for_each(nums.begin(), nums.end(),
[a, b](int n) { std::cout << n * a * b << std::endl; });
}
続けて、クロージャの説明をします。
void lambda_sample(std::vector<int>& nums, int a) {
int b = 10;
std::for_each(nums.begin(), nums.end(),
[a, b](int n) { std::cout << n * a * b << std::endl; });
}
nums
a
b
lambda_sample
n
クロージャは
静的スコープ内にある変数を取込める
a
b
closure
クロージャは通常の関数と異なり、静的スコープ(構文スコープ)内にある「ローカル変数」を取込めます。
void lambda_sample(std::vector<int>& nums, int a) {
int b = 10;
std::for_each(nums.begin(), nums.end(),
[a, b](int n) { std::cout << n * a * b << std::endl; });
}
nums
a
b
lambda_sample
n
クロージャは
静的スコープ内にある変数を取込める
a
b
closure
begin
取込んだ変数と共に他の関数内に入る
end
func
n
a
b
closure
クロージャは小さなクラスと同等の機能を持ちます。
for_each
#02 Higher-Order Function 高階関数について説明します。高階関数は関数型プログラミングの重要なファクタの1つです。
std::for_each(nums.begin(), nums.end(),
[](int n) { std::cout << n << std::endl; });
for_each関数などのC++標準ライブラリの関数は昔から高階関数を利用しています。
std::for_each(nums.begin(), nums.end(),
[](int n) { std::cout << n << std::endl; });
関数の引数に関数
高階関数とは関数の「引数として関数」を持ったり、関数の「戻り値として関数」を返す関数を指します。
std::for_each(nums.begin(), nums.end(),
[](int n) { std::cout << n << std::endl; });
関数の引数に関数
auto result = std::count_if(nums.begin(), nums.end(),
[](int n) { return (n % 2) != 0; });
count_if関数は条件式に一致する要素を数えます。条件を調べる関数を引数として渡します。このような関数を述語と呼びます。
std::for_each(nums.begin(), nums.end(),
[](int n) { std::cout << n << std::endl; });
関数の引数に関数
auto result = std::count_if(nums.begin(), nums.end(),
[](int n) { return (n % 2) != 0; });
auto result = std::accumulate(nums.begin(), nums.end(), 1,
[](int acc, int n) { return acc * n; });
accumulate関数は要素の集計をする関数です。集計に必要な計算部分を関数として渡します。
std::for_each(nums.begin(), nums.end(),
[](int n) { std::cout << n << std::endl; });
関数の引数に関数
auto result = std::count_if(nums.begin(), nums.end(),
[](int n) { return (n % 2) != 0; });
auto result = std::accumulate(nums.begin(), nums.end(), 1,
[](int acc, int n) { return acc * n; });
std::function<int (int)> compose(std::function<int (int)> f,
std::function<int (int)> g) {
return [=](int n) { return g(f(n)); };
関数の戻り値に関数
}
関数の戻り値として関数を返す例です。2つの関数を合成した関数を返す関数になります。
forEach(nums, [](int n) { std::cout << n << std::endl; }); それでは、独自の高階関数の作り方を説明します。ラムダ式などの関数を引数にとる部分をどうすればようでしょうか?
forEach(nums, [](int n) { std::cout << n << std::endl; });
template <typename Func>
void forEach(std::vector<int>& nums, Func func) {
for (int n: nums) {
func(n);
}
templateのパラメータを利用
}
関数テンプレートを使う方法です。関数を引数に取る部分を関数テンプレートのパラメータにします。
forEach(nums, [](int n) { std::cout << n << std::endl; });
template <typename Func>
void forEach(std::vector<int>& nums, Func func) {
for (int n: nums) {
func(n);
}
templateのパラメータを利用
}
void forEach(std::vector<int>& nums, std::function<void (int)> func){
for (int n: nums) {
func(n);
}
std::functionを利用
}
関数テンプレートを使わない場合は、C++標準ライブラリのstd::functionを使います。
// 関数
int add(int a, int b) {
return a + b;
}
// 関数オブジェクト
class Functor {
public:
int operator() (int a, int b) {
return a * b;
}
std::function<戻り値の型(引数の型)>
};
void function_sample() {
std::function<int (int, int)> f;
f = &add;
// 関数ポインタ
f = Functor();
// 関数オブジェクト
f = [](int a, int b) { return a / b; }); // ラムダ式
int n = f(20, 10); // functionから関数を呼び出す
}
std::functionの説明です。テンプレートの型指定部分に、代入したい関数の戻り値の型と引数の型を指定します。
// 関数
int add(int a, int b) {
return a + b;
}
// 関数オブジェクト
class Functor {
public:
int operator() (int a, int b) {
return a * b;
}
std::function<戻り値の型(引数の型)>
};
関数を代入するための変数と考える
void function_sample() {
std::function<int (int, int)> f;
f = &add;
// 関数ポインタ
f = Functor();
// 関数オブジェクト
f = [](int a, int b) { return a / b; }); // ラムダ式
int n = f(20, 10); // functionから関数を呼び出す
}
関数の引数と戻り値の型が一致していれば、ラムダ式の他にも関数ポインタや関数オブジェクトを代入できます。
// 関数
int add(int a, int b) {
return a + b;
}
// 関数オブジェクト
class Functor {
public:
int operator() (int a, int b) {
return a * b;
}
std::function<戻り値の型(引数の型)>
};
関数を代入するための変数と考える
void function_sample() {
std::function<int (int, int)> f;
f = &add;
// 関数ポインタ
f = Functor();
// 関数オブジェクト
f = [](int a, int b) { return a / b; }); // ラムダ式
int n = f(20, 10); // functionから関数を呼び出す
}
std::function型の変数から間接的に関数を呼び出すことができます。
Filter Map Fold 関数型プログラミングで最も有用な高階関数と言われるfilter・map・foldの紹介をします。
1 2 3 4 5 6 7 8 filter・map・foldはリストや配列などのコンテナに対する操作になります。 9
1 2 3 4 5 6 7 8 9 Filter 1 3 5 7 filterはある条件を満たすものだけを抽出する操作になります。 9
1 2 3 4 5 6 7 8 9 Filter 1 3 5 7 9 14 18 Map 2 6 10 mapはデータの変換を行います。
1 2 3 4 5 6 7 8 9 Filter 1 3 5 7 9 14 18 Map 2 6 10 Fold 50 foldはデータを集計して1つにまとめます。畳込み関数と言います。reduceやinjectと呼ばれることもあります。
1 2 3 4 5 6 7 8 9 抽 出 1 3 5 変 2 6 7 9 14 18 換 10 集 計 50
1 2 3 4 5 6 7 8 9 奇数を抽出 1 3 5 7 9 14 18 2倍に変換 2 6 10 合計する 50 今回の例では、奇数を抽出、2倍に変換、最後に合計するという操作を行いました。
int filter_map_fold(const std::vector<int>& nums) { } C++11版のfilter・map・foldを紹介します。
int filter_map_fold(const std::vector<int>& nums) {
// 奇数を抽出 (filter)
std::vector<int> odds;
std::copy_if(nums.begin(), nums.end(),
std::back_inserter(odds),
[](int n) { return (n % 2) != 0; });
}
filterに相当する関数はcopy_if関数になります。back_inserterはコピー先のコンテナにデータを追加するイテレータを作成します。
int filter_map_fold(const std::vector<int>& nums) {
// 奇数を抽出 (filter)
std::vector<int> odds;
std::copy_if(nums.begin(), nums.end(),
std::back_inserter(odds),
[](int n) { return (n % 2) != 0; });
// 2倍に変換 (map)
std::vector<int> doubles;
std::transform(odds.begin(), odds.end(),
std::back_inserter(doubles),
[](int n) { return n * 2; });
}
mapに相当する関数はtransform関数になります。
int filter_map_fold(const std::vector<int>& nums) {
// 奇数を抽出 (filter)
std::vector<int> odds;
std::copy_if(nums.begin(), nums.end(),
std::back_inserter(odds),
[](int n) { return (n % 2) != 0; });
// 2倍に変換 (map)
std::vector<int> doubles;
std::transform(odds.begin(), odds.end(),
std::back_inserter(doubles),
[](int n) { return n * 2; });
// 合計する (fold)
return std::accumulate(doubles.begin(), doubles.end(), 0,
[](int acc, int n) { return acc + n; });
}
foldに相当する関数はaccumulate関数になります。しかし、標準ライブラリ関数の組み合わせは、あまりスマートとは言えません。
LINQ for C++ そこで、C#のLINQ風に書けるC++11のライブラリ「LINQ for C++ 」を紹介します。filter・map・foldをスマートに書けます。
static int filter_map_fold(List<int> nums) {
return nums.Where(n => (n % 2) != 0)
.Select(n => n * 2)
.Aggregate(0, (acc, n) => acc + n);
}
C#のLINQ
C#のLINQで先ほどのC++11の例を書いてみます。とても簡潔に書けます。
static int filter_map_fold(List<int> nums) {
return nums.Where(n => (n % 2) != 0)
.Select(n => n * 2)
.Aggregate(0, (acc, n) => acc + n);
}
C#のLINQ
int filter_map_fold(const std::vector<int>& nums) {
return cpplinq::from(nums)
>> cpplinq::where([](int n) { return (n % 2) != 0; })
>> cpplinq::select([](int n) { return n * 2; })
>> cpplinq::aggregate(0, [](int acc, int n) { return acc + n; });
}
LINQ for C++
LINQ for C++との比較。C#のLINQと同様に無駄なループや中間データの作成をできるだけしないように実装されています。
static int filter_map_fold(List<int> nums) {
return nums.Where(n => (n % 2) != 0)
.Select(n => n * 2)
.Sum();
}
C#のLINQ
int filter_map_fold(const std::vector<int>& nums) {
return cpplinq::from(nums)
>> cpplinq::where([](int n) { return (n % 2) != 0; })
>> cpplinq::select([](int n) { return n * 2; })
>> cpplinq::sum();
}
LINQ for C++
単純な合計であればsum関数を使います。合計・平均・最小値・最大値などを求める集計関数が、あらかじめ用意されています。
struct Person {
std::string name;
// 氏名
int
age;
// 年齢
int
salary; // 月収
};
// 人物データ
std::vector<Person> parsons = {
{"鈴木", 25, 20}, {"田中", 45, 50}, {"佐藤", 55, 60} …
};
もう少し実用的なfilter・map・foldの例を紹介します。人物データの集合から40歳以上の平均月収を求めてみます。
struct Person {
std::string name;
// 氏名
int
age;
// 年齢
int
salary; // 月収
};
// 人物データ
std::vector<Person> parsons = {
{"鈴木", 25, 20}, {"田中", 45, 50}, {"佐藤", 55, 60} …
};
// 40歳以上の平均月収を求める
auto avg = cpplinq::from(persons)
>> cpplinq::where([](const Person& man) { return man.age >= 40; })
>> cpplinq::select([](const Person& man) { return man.salary; })
>> cpplinq::avg();
whereで40歳以上の人物を抽出、selectで月収だけのデータに変換、avgで月収の平均を求めます。
boost range LINQ for C++に類似するライブラリは他にも多数あります。有名ものとしてboost rangeがあります。
https://www.assetstore.unity3d.com/jp/#!/content/17276 リアクティブプログラミングは今後注目の技術になりそうです。イベント処理の流れをデータ列と見立ててLINQ風に処理します。
#03 Closure Design Patterns クロージャを使ったデザインパターンの紹介をします。
Pluggable Behavior Pattern オブジェクトの振る舞いを実行時に指定する Pluggable Behaviorは実行時にオブジェクトの振る舞いを変化させるパターンです。
struct Person {
std::string name;
// 氏名
int
age;
// 年齢
int
salary; // 月収
};
class Persons {
public:
void show() {
for (auto& person : persons_) {
if (person.age >= 20) {
std::cout << person.name << std::endl;
}
}
}
private:
std::vector<Person> persons_;
};
パターン適用前の状態です。20歳以上の人物の名前を表示します。show関数の振る舞いは変更できません。
struct Person {
std::string name;
// 氏名
int
age;
// 年齢
int
salary; // 月収
};
class Persons {
public:
void show() {
for (auto& person : persons_) {
if (person.age >= 20) {
std::cout << person.name << std::endl;
}
}
}
private:
std::vector<Person> persons_;
};
年齢が20という定数になっています。この部分が変更可能になれば、他の条件の人物も表示できます。
class Persons {
public:
void show(int age) {
for (auto& person : persons_) {
if (person.age >= age) {
std::cout << person.name << std::endl;
}
}
}
private:
std::vector<Person> persons_;
};
年齢を引数として指定できるようになれば、振る舞いの変更ができます。
class Persons {
public:
void show(int age) {
for (auto& person : persons_) {
if (person.age >= age) {
std::cout << person.name << std::endl;
}
}
}
private:
std::vector<Person> persons_;
};
// 成人を表示
parsons.show(20);
// 年金受給者を表示
parsons.show(65);
しかし、ある年齢「以上」の判定はできますが、ある年齢「未満」の判定はできません。未成年だけ表示などには対応できません。
class Persons {
public:
void show(std::function<bool(Person&)> closure) {
for (auto& person : persons_) {
if (closure(person)) {
std::cout << person.name << std::endl;
}
}
}
private:
std::vector<Person> persons_;
};
そこで、条件式の部分をクロージャに変更します。これにより、柔軟な振る舞いの変化を与えられます。
class Persons {
public:
void show(std::function<bool(Person&)> closure) {
for (auto& person : persons_) {
if (closure(person)) {
std::cout << person.name << std::endl;
}
}
}
private:
std::vector<Person> persons_;
};
// 未成年を表示
parsons.show([](Person& parson) { return parson.age < 20; });
// 月収100万円以上を表示
parsons.show([](Person& parson) { return parson.salary >= 100; });
年齢だけでなく、月収などを条件にすることもできます。クロージャによって条件式そのものを引数として渡せます。
Daynamical Conditional Execution Pattern 条件付き操作の作成と実行をする
class Persons {
public:
void show(
int age,
std::function<void (Person&)> adults,
std::function<void (Person&)> minors) {
for (auto& person : persons_) {
if (person.age >= age) {
adults(person);
} else {
minors(person);
}
}
}
private:
std::vector<Person> persons_;
};
Dynamical Conditional Execution Patternは条件式に対応する振る舞いをクロージャで変更可能にします。
Execute Around Method Pattern 前後に実行しなければいけない操作のペアを表現する プログラムでは初期化処理や終了処理など常に対に行うことがよくあります。
void setVec2(const char* name, float x, float y) {
Param* param = findParam(name);
if (param != nullptr) {
param->setVec2(x, y);
param->release();
}
}
void setVec3(const char* name, float x, float y, float z) {
Param* param = findParam(name);
if (param != nullptr) {
param->setVec3(x, y, z);
param->release();
}
}
名前でパラメータを検索して、値を設定する。値の設定が終わったらパラメータをreleaseする仕様とします。
void setVec2(const char* name, float x, float y) {
Param* param = findParam(name);
if (param != nullptr) {
必ずペアで実行する定型処理
param->setVec2(x, y);
param->release();
}
}
void setVec3(const char* name, float x, float y, float z) {
Param* param = findParam(name);
if (param != nullptr) {
必ずペアで実行する定型処理
param->setVec3(x, y, z);
param->release();
}
}
必ずペアで実行する定型処理があり、その部分が重複しています。
void setVec2(const char* name, float x, float y) {
Param* param = findParam(name);
if (param != nullptr) {
必ずペアで実行する定型処理
param->setVec2(x, y);
param->release();
}
}
void setVec3(const char* name, float x, float y, float z) {
Param* param = findParam(name);
if (param != nullptr) {
必ずペアで実行する定型処理
param->setVec3(x, y, z);
param->release();
}
}
値を設定する部分だけが異なります。
void setVec2(const char* name, float x, float y) {
Param* param = findParam(name);
if (param != nullptr) {
param->setVec2(x, y);
param->release();
}
}
定型処理の内部にある値を設定する部分をクロージャに変更します。
void setVec2(const char* name, float x, float y) {
Param* param = findParam(name);
if (param != nullptr) {
param->setVec2(x, y);
param->release();
}
}
void setParam(const char* name, std::function<void (Param*)> closure) {
Param* param = findParam(name);
if (param != nullptr) {
closure(param);
param->release();
}
}
クロージャを使うことにより、重複部分をまとめることができます。
void setVec2(const char* name, float x, float y) {
setParam(name, [=](Param* param) { param->setVec2(x, y); });
}
void setVec3(const char* name, float x, float y, float z) {
setParam(name, [=](Param* param) { param->setVec3(x, y, z); });
}
void setParam(const char* name, std::function<void (Param*)> closure) {
Param* param = findParam(name);
if (param != nullptr) {
必ずペアで実行する処理の間に
closure(param);
クロージャを挟み込む
param->release();
}
}
setVec2関数やsetVec3関数は、setParam関数を利用して作成します。これで定型処理がまとまり重複がなくなりました。
Lorn Pattern リソースの確保・解放を確実に行う Lorn Patternは確保したリソースを確実に解放するパターンです。Execute Around Patternと同じ発想のパターンです。
bool write_file(const char* fname, std::function<void (FILE*)> closure) {
FILE* fp;
errno_t error = fopen_s(&fp, fname, "w");
if (error != 0) {
return false;
}
closure(fp);
fclose(fp);
return true;
}
ベタな例になりますが、ファイルはオープンしたら、必ずクローズする必要があります。
bool write_file(const char* fname, std::function<void (FILE*)> closure) {
FILE* fp;
errno_t error = fopen_s(&fp, fname, "w");
if (error != 0) {
return false;
クロージャの前後で
}
リソースの確保・解放
closure(fp);
fclose(fp);
return true;
}
クロージャの前後でオープンとクローズをすれば、確実にリソースの確保と解放ができます。
bool write_file(const char* fname, std::function<void (FILE*)> closure) {
FILE* fp;
errno_t error = fopen_s(&fp, fname, "w");
if (error != 0) {
return false;
クロージャの前後で
}
リソースの確保・解放
closure(fp);
fclose(fp);
return true;
}
write_file("test.txt", [](FILE* fp) { fputs("hello", fp); });
ラムダ式の中にファイルに出力する処理を書きます。C++ではRAIIというコンストラクタとデストラクタを利用した方法もあります。
それでは、クロージャのデザインパターンのまとめをします。どのパターンでも共通する考え方を説明します。
関数 不変部分 可変部分 不変部分 例えば、関数内の一部に可変部分があって再利用しにくい場合があるとします。
関数 不変部分 可変部分 不変部分 可変部分を外側に出せば、関数内は不変部分だけとなり、再利用できるようになります。
関数 不変部分 クロージャ 不変部分 可変部分をクロージャに変更します。
高階関数 不変部分 クロージャ クロージャ 不変部分 クロージャを関数の引数にして、関数内に入り込ませます。関数を引数にもつ高階関数に変更します。
高階関数 ラムダ式 不変部分 クロージャ 不変部分 クロージャ ラムダ式 ラムダ式 関数内の可変部分はラムダ式で作成します。これで関数の振る舞いを間接的に変更できます。
CLOSED OPEN 高階関数 ラムダ式 不変部分 クロージャ 不変部分 変更に対して閉じている クロージャ ラムダ式 ラムダ式 拡張に対して開いている 開放・閉鎖の原則に基づいた設計となります。オブジェクト指向言語では抽象インターフェースを使って解決していました。
クロージャは「軽量な抽象インターフェース」として機能します。振る舞いの変更が簡単にできるようになります。
#04 GoF Design Patterns 古典的なGoFのデザインパターンをクロージャで実装してみます。
Iterator Pattern コンテナ内のオブジェクトを順番にアクセスする手段を提供する コンテナのデータ構造を隠蔽したまま、コンテナ内のデータにアクセスできるようにするパターンです。
class Persons {
public:
typedef std::vector<Person>::iterator iter;
iter begin() {
return persons_.begin();
}
iter end() {
return persons_.end();
}
private:
std::vector<Person> persons_;
};
外部からアクセスするための
手段を提供をする
従来のIterator Patternの実装例です。外部から内部のデータにアクセスできるようにiteratorクラスを作成して返すようにします。
class Persons {
public:
typedef std::vector<Person>::iterator iter;
iter begin() {
return persons_.begin();
}
iter end() {
return persons_.end();
}
private:
std::vector<Person> persons_;
};
外部からアクセスするための
手段を提供をする
for (Persons::iter i = persons.begin(); i != persons.end(); ++i) {
std::cout << (*i).name << std::endl;
利用者側がループしてアクセス
}
利用者側がループを使って内部のデータにアクセスします。
class Persons { public: void each(std::function<void (Person&)> closure) { for (auto& person : persons_) { 内部でループさせる closure(person); } } private: std::vector<Person> persons_; }; クロージャを使った方法に変更します。コンテナ内のデータをクロージャに渡すようにします。
class Persons {
public:
void each(std::function<void (Person&)> closure) {
for (auto& person : persons_) {
内部でループさせる
closure(person);
}
}
private:
std::vector<Person> persons_;
};
利用者側でのループが不要
persons.each(
[](Person& person) { std::cout << person.name << std::endl; });
利用者側でのループが不要になります。ループの大幅な削減ができます。
class Persons {
public:
void each(std::function<void (Person&)> closure) {
for (auto& person : persons_) {
closure(person);
}
}
private:
std::vector<Person> persons_;
};
新スタイル
persons.each(
[](Person& person) { std::cout << person.name << std::endl; });
for (Persons::iter i = persons.begin(); i != persons.end(); ++i) {
std::cout << (*i).name << std::endl;
旧スタイル
}
従来の方法とクロージャを利用した方法の比較になります。
Command Pattern 命令をオブジェクトとして表現する Command Patternは複数の命令をクラス化してあつかうパターンです。
// コマンド抽象インターフェース class Command { public: virtual ~Command() {} virtual void action() = 0; }; 従来の方法から紹介します。まず、複数の命令を1つにまとめるための抽象インターフェースを作成します。
// コマンド抽象インターフェース class Command { public: virtual ~Command() {} virtual void action() = 0; }; class SleepCommand : public Command { virtual void action() { std::cout << "寝る" << std::endl; } }; 具体的なコマンドは抽象インターフェースを実装して作成します。各命令はaction関数をオーバーライドします。
// コマンド抽象インターフェース class Command { public: virtual ~Command() {} virtual void action() = 0; }; class SleepCommand : public Command { virtual void action() { std::cout << "寝る" << std::endl; } }; class AwakeCommand : public Command { virtual void action() { std::cout << "起きる" << std::endl; } }; 「寝るコマンド」と「起きるコマンド」を作成しました。このようにコマンドが増えるたびにクラス数が増加します。
// コマンドリストクラス
class CommandList {
public:
// コマンドの追加
void add(Command* command) {
actions_.push_back(command);
}
// コマンドの実行
void execute() {
for (iter i = actions_.begin(); i != actions_.end(); ++i) {
(*i)->action();
}
}
private:
std::vector<Command*> actions_;
};
コマンドを溜め込んで、一気に実行するコマンドリストクラスを作成します。
CommadList commands; commands.add(new AwakeCommand()); commands.add(new StadyCommand()); commands.add(new PlayCommand()); commands.add(new SleepCommand()); commands.execute(); コマンドリストの使用例になります。
class CommandList {
public:
// コマンドの追加
void add(std::function<void()> closure) {
actions_.push_back(closure);
}
// コマンドの実行
void execute() {
for (auto& action : actions_) {
action();
}
}
private:
std::vector<std::function<void()>> actions_;
};
命令をクロージャ化
クロージャのコンテナ
クロージャを使った例を紹介します。コマンドをクラスからクロージャに変更します。クロージャをコンテナに入れることもできます。
CommadList commands;
commands.add([]{ std::cout << "起きる"<< std::endl; });
commands.add([]{ std::cout << "勉強" << std::endl; });
commands.add([]{ std::cout << "遊ぶ" << std::endl; });
commands.add([]{ std::cout << "寝る" << std::endl; });
commands.execute();
ラムダ式でコマンドを作成します。コマンドごとにクラスを作る必要はありません。コマンドが単純な場合に有効な方法です。
class CommandList { public: // コマンドの追加 void add(std::function<void()> next) { const auto& prev = action_; action_ = [=] { prev(); next(); }; } // コマンドの実行 void execute() { action_(); } private: std::function<void()> action_ = []{}; }; 関数の合成 ループ不要 コンテナ不要 関数の合成を利用するとコンテナを使わずに複数のコマンドを1つにまとめることができます。
class CommandList {
public:
// コマンドの追加
C++14
void add(std::function<void()> command) {
action_ = [prev = action_, next = command] { prev(); next(); };
}
// コマンドの実行
void execute() {
action_();
}
private:
std::function<void()> action_ = []{};
};
C++14の初期化キャプチャを使うと、よりわかりやすく書けます。
class CommandList { public: C#のevent風にしてみる // コマンドの追加 void operator += (std::function<void()> next) { const auto& prev = action_; action_ = [=]() { prev(); next(); }; } // コマンドの実行 void execute() { action_(); } private: std::function<void()> action_ = []{}; }; おまけですが、add関数を+=のオペレータに変更してみます。C#のevent風の実装ができます。
CommadList commands;
commands += []{ std::cout << "起きる"<< std::endl; };
commands += []{ std::cout << "勉強" << std::endl; };
commands += []{ std::cout << "遊ぶ" << std::endl; };
commands += []{ std::cout << "寝る" << std::endl; };
commands.execute();
CommadList commands;
commands.add(new AwakeCommand());
commands.add(new StadyCommand());
commands.add(new PlayCommand());
commands.add(new SleepCommand());
commands.execute();
従来の方法との比較になります。
新スタイル
旧スタイル
Composite Pattern オブジェクトを再帰的な木構造で表現する 続けてコマンドリストに、一工夫加えてComposite Patternを適用します。
class CommandList { public: // コマンドの追加 void operator += (std::function<void()> next) { const auto& prev = action_; action_ = [=] { prev(); next(); }; } // コマンドの実行 void operator()() { 関数オブジェクト化する action_(); } private: std::function<void()> action_ = []{}; }; コマンドを実行するexecute関数をoperator ()に変更して、コマンドリストクラスを関数オブジェクト化します。
CommadList basic_commands; basic_commands += []{ std::cout << "食う" << std::endl; }; basic_commands += []{ std::cout << "寝る" << std::endl; }; basic_commands += []{ std::cout << "遊ぶ" << std::endl; }; CommadList combat_commands; combat_commands += []{ std::cout << "撃つ" << std::endl; }; combat_commands += []{ std::cout << "伏せる" << std::endl; }; combat_commands += []{ std::cout << "ジャンプ" << std::endl; }; CommadList commands; commands += basic_commands; commands += combat_commands; commands(); コマンドリストにコマンドリストを追加 std::functionはラムダ式だけでなく、関数オブジェクトも代入できます。これによりコマンドリストに別のコマンドリストを追加できます。
Abstract Factory Pattern 抽象化されたオブジェクトを生成する手段を提供する 具象クラスを生成して、抽象クラスを返すクラスとファクトリクラスと呼びます。
それでは、さっそくピザ具材工場を作成してみます。
// 具材抽象インターフェース class Ingredients { public: virtual ~Ingredients() {} virtual void draw() = 0; }; まず、Abstract Factoryクラスが作成する具材の抽象インターフェースを作成します。
// 具材抽象インターフェース class Ingredients { public: virtual ~Ingredients() {} virtual void draw() = 0; }; class Tomato : public Ingredients { virtual void draw() { std::cout << "トマト" << std::endl; } }; ピザと言えばトマトですね。
// 具材抽象インターフェース class Ingredients { public: virtual ~Ingredients() {} virtual void draw() = 0; }; class Tomato : public Ingredients { virtual void draw() { std::cout << "トマト" << std::endl; } }; class Cheese : public Ingredients { virtual void draw() { std::cout << "チーズ" << std::endl; } }; チーズも忘れてはいけません。
// 抽象具材工場 class Factory { public: virtual ~Factory() {} virtual Ingredients* create(const std::string& name) = 0; }; 具材工場を抽象化したクラスを作成します。工場そのものも抽象化してしまうのでAbstract Factoryと呼ばれます。
// 抽象具材工場 class Factory { public: virtual ~Factory() {} virtual Ingredients* create(const std::string& name) = 0; }; // ピザ具材工場 class PizzaFactory : public Factory { public: virtual Ingredients* create(const std::string& name) { if (name == "Cheese") return new Cheese(); if (name == "Tomato") return new Tomato(); if (name == "Dough" ) return new Dough(); return 0; } }; ピザ具材工場の簡単な実装例です。具材名を指定すると、対応する具材を作成してくれます。
// 抽象具材工場 using Factory = std::function<Ingredients* (const std::string&)>; 次はクロージャを使った実装例を紹介します。工場のクラスは作成せずにstd::functionを使います。usingで別名を付けておきます。
// 抽象具材工場
using Factory = std::function<Ingredients* (const std::string&)>;
// ピザ工場
Ingredients* pizzaFactory(const std::string& name) {
using Creator = std::function<Ingredients* ()>;
static const std::unordered_map<std::string, Creator> creators = {
{ "Dough", [] { return new Dough(); } },
{ "Cheese", [] { return new Cheese(); } },
{ "Tomato", [] { return new Tomato(); } }
};
return creators.at(name)();
}
ピザ工場の実装も単なる関数となります。具材を作成する部分にラムダ式を使っています。このような使い方もできます。
// ピザ工場を作成 Factory factory = &pizzaFactory; // チーズを作成 Ingredients* cheese = factory("Cheese"); 新スタイル // ピザ工場を作成 Factory* factory = new PizzaFactory(); // チーズを作成 Ingredients* cheese = factory->create("Cheese"); 旧スタイル 従来の方法との比較になります。
GoFデザインパターンのクロージャによる実装のまとめをします。
<<interface>> CommandList AwakeCommand + action() Command + action() PlayCommand + action() Command Patternの例を使って説明します。 SleepCommand + action()
<<interface>> CommandList AwakeCommand + action() Command + action() PlayCommand + action() コマンドが増えるたびに 新しいクラスが必要になる SleepCommand + action() クラスを使った従来の方法では、コマンドの種類が増加するたびにクラス数が増加します。
<<interface>>
CommandList
AwakeCommand
+ action()
CommandList
[]{cout <<"起きる"; }
Command
+ action()
PlayCommand
+ action()
コマンドが増えるたびに
新しいクラスが必要になる
SleepCommand
+ action()
std::function<void()>
コマンドが増えても
クラス数の増加はない
[]{cout <<"遊ぶ"; }
[]{cout <<"寝る"; }
std::functionとラムダ式を使うとクラスが不要になります。抽象インターフェースも不要。コマンドが増えてもクラス数は増加しません。
<<interface>>
CommandList
CommandList
Command
+ action()
抽象インターフェース
AwakeCommand
+ action()
実装クラス
std::function<void()>
抽象インターフェースの役割
[]{cout <<"起きる"; }
実装クラスの役割
std::functionによって、クラスを使わない簡易的な実装に置き換えが可能です。
<<interface>>
Command
+ action()
AwakeCommand
+ action()
std::function<void()>
[]{cout <<"起きる"; }
1つのメンバ関数しか持たない小さな抽象インターフェースは、std::functionに置き換えが可能です。
<<interface>> Context SingleAbstractMethod + method() ConcreateClass + method() このクラス図のようなパターンであれば、std::functionとラムダ式による簡易的な実装に置き換えが可能です。
<<interface>> Context SingleAbstractMethod + method() ConcreateClass + method() Context std::function<> [](){ } 変 換 可 能
小さな抽象インターフェースをたくさん作る必要がなくなります。
#05 Conclusion 本セッション全体のまとめです。
void lambda_sample(std::vector<int>& nums, int a) {
std::for_each(nums.begin(), nums.end(),
[a](int n) { std::cout << n * a << std::endl; });
}
void lambda_sample(std::vector<int>& nums, int a) {
class lambda {
int a_; // キャプチャした変数
public:
lambda(int a) : a_(a) {}
コンパイラが
関数オブジェクトを生成
void operator()(int n) const {
std::cout << n * a_ << std::endl;
}
};
std::for_each(nums.begin(), nums.end(), lambda(a));
}
C++11のクロージャの正体は、関数オブジェクトになります。変数キャプチャがないラムダ式は従来の関数と同じ扱いです。
struct Person {
std::string name;
// 氏名
int
age;
// 年齢
int
salary; // 月収
};
// 人物データ
std::vector<Person> parsons = {
{"鈴木", 25, 20}, {"田中", 45, 50}, {"佐藤", 55, 60} …
};
// 40歳以上の平均月収を求める
auto avg = cpplinq::from(persons)
>> cpplinq::where([](const Person& man) { return man.age >= 40; })
>> cpplinq::select([](const Person& man) { return man.salary; })
>> cpplinq::avg();
ラムダ式はおもに高階関数の引数として利用します。LINQ for C++は便利なので一度試してみてください。
CLOSED OPEN 高階関数 ラムダ式 不変部分 クロージャ 不変部分 クロージャ ラムダ式 ラムダ式 変更に対して閉じている 拡張に対して開いている 関数内の可変部分をクロージャにして関心事の分離をしましょう。
<<interface>>
Command
+ action()
CommandList
AwakeCommand
+ action()
CommandList
[]{cout <<"起きる"; }
PlayCommand
+ action()
コマンドが増えるたびに
新しいクラスが必要になる
SleepCommand
+ action()
std::function<void()>
コマンドが増えても
クラス数の増加はない
[]{cout <<"遊ぶ"; }
[]{cout <<"寝る"; }
std::functionを使えば、クラスを使わずに簡易的な実装ができます。
http://arturoherrero.com/closure-design-patterns/ クロージャデザインターンの参考資料です。GroovyとRubyのサンプルコードがあります。
https://isocpp.org/blog/2013/10/patterns GoFデザインパターンの参考資料です。英語の資料ですがサンプルコードが豊富でわかりやすいです。
https://www.assetstore.unity3d.com/jp/#!/content/17276 これから注目すべき技術になるかもしれません。関数型プログラミングのスタイルをゲームプログラミングで応用した例になります。
Javaによる関数型プログラミングはラムダ式を効果的に使用する例がコンパクトにまとまっています。
本書でもC++11のラムダ式の効果的な使用例があります。学生や新入社員対象の書籍になっております。
本スライドはラムダ計算騎士団の皆様に、ご協力いただきました。