pfi seminar 2010 1 7
TRANSCRIPT
Caution!
本発表には次のものは含まれません
C++の有用なプログラミング技法
広くつかわれるべきテクニック
その他なにか視聴者に役立つもの
そういうのを期待された方はあしからず
自己紹介
田中英行 (@tanakh, id:tanakh)
Haskell愛好家
C++歴 1998年 ~
2005年ぐらいまでは漫然と使っていました
社内用C++ライブラリ(pficommon)を作成 C++のきもさを再認識
その辺で得られた知見を話します○ ※実際に使われているわけではないです
pficommon
Boostの広く利用できる部分(tr1)
データ構造いろいろ
サーバー書く向け機能
マルチスレッド
各種プロトコル
RPC
ウェブアプリ向け機能
DSL
本日の内容
ユーザー定義構文
Serializable万能説
Stream Fusion
の三本立て
概要
C++の構文のように扱える構文を定義してみようとすると、それがどういう意味を持つのだろうか、というお話
ユーザー定義構文
ユーザー定義する組み込み構文のように扱えるもの
例えば
synchronized (Java)
using (C#)
actor (Scala)
のようなものがC++でも書けると嬉しいですね
ほかの言語の例
Lisp
マクロ・高階関数
Haskell
高階関数・モナドを引数にとる関数
Ruby
ブロックをとる関数
Scala
最後の引数にラムダ式をとる関数を書くとそれっぽく見える
例:foreach
(for-each (lambda (x)(display x)
‘(1 2 3 4 5))
(1..5).each{|x|p x
}
forM_ [1..5] $ ¥x ->print x
List(1,2,3,4,5).foreach{ x =>println(x)
}
C++でのユーザー定義構文(1)
statementを引数に取るマクロを書き、組み込みの構文要素に置換する
#define foreach(v, c, s) ¥for(typeof(c.begin()) it=c.begin(); it!=c.end(); it++){ ¥v = *it; ¥s ¥
}
…
vector<int> v; v.push_back(1); …foreach(int x, v, { cout<<x<<endl; })
問題点
簡単に書けて、扱いやすいが、
かっこ悪い foreach(, , {})
↑ 括弧の中に{}があるのが耐えられない}) って…
のように書きたい
foreach(int x, v) {cout<<x<<endl;
}
C++でのユーザー定義構文(2)
statementが後ろに来て整合性が取れるようなマクロを書く
#define foreach(v, c) ¥for (bool b=true; b; ) ¥for (v; b; b=false) ¥for(typeof(c.begin()) it=c.begin();
it!=c.end() && (v=*it, true);it++)
…
vector<int> v; v.push_back(1); …foreach(int x, v){ cout<<x<<endl; }
C++でのユーザー定義構文(2)
この書き方は美しいが、記述力を制限される
既存の制御構文の後ろに文を置けるだけ
後処理
synchronized(v){ … } のようなことを実現するには、{ … } の後に処理をさせなければならない
forを使う方法
後続の文より後に処理をさせる方法
forを使う
break, continueが食われてしまう
#define synchronized(v) ¥for (bool b=true; ¥
b && (v.lock(), true); ¥b=false, v.unlock())
#define synchronized(v) ¥for (bool b=true; b; ) ¥for (scoped_lock lk(v); b; b=false)
ifを使う方法
ifの中で変数を宣言すれば、後続の文の後にデストラクタを動かせる
#define synchronized(v) ¥if (scoped_lock lk=scoped_lock(v))
ifを使う方法(2)
コピー可能なクラスを定義する
trueを返すoperator bool() を定義する
デストラクタを定義する
class hoge { … };
if (hoge h=hoge()) stat;
処理の流れ hoge::hoge() hoge::operator bool() stat hoge::~hoge()
改良
operator bool() はfalseを返したほうがいい
#define synchronized(v) ¥if (scoped_lock lk=scoped_lock(v)); ¥else
活用例:CGI DSL
class my_cgi : public cgi{
public:
void run(){
html__{
head__{
title__{ text__("タイトル"); }
}
body__{
a__{ href__ = "http://kzk9.net/blog/";
text__("super blog!");
}
br__;
}
}
}
};
議論
hoge(…){ … } の形で、前処理→処理→後処理 の形のユーザー定義構文が書けることが分かった
だがそれだけだろうか?
こういうことができるということは、つまりどういうことなのだろうか
見えざる継続
継続とは
ある計算過程のある瞬間における、その過程の未来全体を表すもの、あるいは計算過程の実行スナップショットと説明される。(Wikipedia)
Schemeなどでは言語レベルでサポート
継続は、実行中のどんな計算機プログラムにも存在する
関数呼び出しと継続
関数呼び出しとはすなわち、継続をともなう手続きへのジャンプ
関数が呼ばれた=関数から返った後の継続が手に入る
int foo(){ return 1; }
void bar(){cout<<foo()*2<<endl;
}
foo()が呼ばれる際に渡される継続・返された値に2を書けて・それを表示する
実際のところC++では、・スタック・リターンアドレスの対として表現される
ユーザー定義構文では
hoge::operator bool() が呼ばれた瞬間の継続に注目する
class hoge { … };
if (hoge h=hoge()) stat;
処理の流れ hoge::hoge() hoge::operator bool() ← trueを返せば stat → hoge::~hoge() → … stat falseを返せば hoge::~hoge() → … hoge::~hoge() なる継続
継続を取り出す
callee save レジスタ、スタック、リターンアドレスを取り出す
cont get_ret_cont(){regs=get_callee_save_regs();stack=get_stack();ret=get_ret_addr();return (regs, stack, ret);
}
継続が取り出せるなら
operator bool()にて、継続を取り出し、さらに文実行後の継続を設定し、stat後の処理の流れを決められる
hoge::operator bool(){cont c=get_ret_cont();next=…;c(true);
}hoge::~hoge(){next();
}
文を後ろに置く=継続を渡す
つまるところ、if (hoge h=hoge()) { … } は
後ろの文を引数に関数を呼び出しているのと同じである
Schemeの高階関数や、Rubyのブロック構文と同じことができる
例:スレッド
thread{ … } でスレッドを立てる後続の文が終了するとスレッド終了mutex m; int n;
int main(){thread{for (int i=0; i<10; i++)
synchronized(m)n++;
}for (int i=0; i<10; i++)synchronized(m)
n--;}
実装
class thread_forker{ … };
thread_forker::operator bool(){cont c=get_ret_cont();pthread_create(&tid, NULL,
bind(apply_cont, c, true));c(false);
}
thread_forker::~thread_forker(){pthread_exit(NULL);
}
別スレッドでの継続の起動
スタックを別のスタックにコピーする
ポインタの張り替えなどめんどい
retframe
retframe
retframe
retframe
retframe
retframe
まとめ
C++でユーザー定義構文は意外ときれいに作れるんじゃなかろうか
ポータブルな実装ができるかどうか
概要
シリアライザというものがあります
シリアライザが意外と色々なところに使えるという話
シリアライザとは
データをバイト列に変換したり(シリアライズ)、バイト列からデータに変換したり(デシリアライズ)するもの
クラスやコンテナの内容をファイルに保存したり、ネットワーク越しに転送したりするのにとても便利
boost::serialization
Boostに入っているシリアライズライブラリclass hoge{private:string a;vector<int> b;
friend class boost::serialization::access;template <class Archive>void serialize(Archive &ar){ar & a & b;
}};
boost::serializationの特徴
シリアライズとデシリアライズを共通のコードで記述
テンプレートでディスパッチ
hoge h;text_oarchive oa(cout);oa << h; // text_oarchiveを引数にserializeが呼ばれる
hoge g;text_iarchive ia(cin);ia >> h; // text_iarchiveを引数にserializeが呼ばれる
仕組み
serialize()関数をオーバーロード
template <class Archive>Archive &operator &(Archive &ar, T &v){serialize(ar, v);return ar;
}
void serialize(text_iarchive &ar, int n){ar.read_int(n);
}
void serialize(text_oarchive &ar, int n){ar.write_int(n);
}
シリアライズするというのはどういうことか?
クラスのシリアライズ
メンバの列挙
コンテナのシリアライズ
データの列挙
テンプレートでのシリアライズ
serializeがテンプレートメンバ関数
シリアライズ・デシリアライズを共通化
色々なシリアライザ(text, binary)に適用可能
拡張可能
→ シリアライザじゃなくてもいいのでは?
型を書きだす
データの代わりに、型を書きだしてみる
class type_oarchive{ … };
template <class T>void serialize(type_oarchive &oa, T &v){ // デフォルトoa.enter_struct();serialize(oa, v);oa.leave_struct();
}
void serialize(type_oarchive &oa, int &n){ // 特殊化oa.add(new int_type(true, sizeof(int)));
}…
型を書きだす(2)
コンテナ型は特殊化する
template <class T>void serialize(type_oarchive &oa, vector<T> &v){oa.add(new array_type(get_type<T>());
}
template <class K, class V>void serialize(type_oarchive &oa, map<K, V> &v){oa.add(new map_type(get_type<K>(), get_type<V>());
}
型を書きだす(3)
型情報取得関数
template <class T>type_info *get_type(){T v;type_oarchive oa;oa << v;return oa.get();
}
Serializable = Reflectable
値の代わりに型を書きだすことにより、Serializableなクラスは(部分的には)
Reflectableなクラスとなる
Reflectableなクラスは明らかにSerializableに出来るのでこれはそうおかしな話ではない
つまり、(部分的には)SerializableとReflectableは等価である
応用例:RPC
// シグニチャRPC_PROC(add, int(int, int)) // メソッド定義
RPC_GEN(calc, add) // クラス定義
// サーバint add(int x, int y){ return x+y; }
int main(){
hoge_server serv;
serv.set_add(&add);
serv.serv(12345, 10);
}
// クライアントhoge_client cli(“localhost”, 12345);
cout<<cli.call_add(1,2)<<endl;
// ↑ 3が返ってくるはず
RPC説明
適当に関数のシグニチャを定義して、それに合う関数をセットする
関数の引数、返り値はソケットでやり取りされる → シリアライズ可能でなければならない
RPC:クライアントコード生成
言語bindingを自動で生成
C++ソースを読み込む
パーズするのは大変すぎる
g++にやらせる
リフレクションする
RPCの型情報が取れる
好きなコード生成できる
RPCでやり取りするデータはすべてSerializable
でなければならないはずなので、すべて何もしなくてもリフレクション出来るはずである
for
RPC:テスト生成
おなじ原理でRPCテスト用のWebサーバーをC++コードから自動で生成
まとめ
Serializableなデータ構造は思ったより有用だ
概要
ストリームを扱う計算がテンプレートで速くなるという話
ストリーム
(ここでは)何かデータの列 配列とか
外部メモリ上のデータとか
ストリームに対する演算 map
filter
fold
sort
merge
など…
例
ストリームに対する演算の繰り返し
中間配列ができる
配列を二回なめる
外部メモリを扱う時に特に顕著for
vector<int> v, w, x;for (int i=0; i<100; i++) v.push_back(i);
remove_copy_if(v.begin(), v.end(), back_inserter(w), is_even());transform(w.begin(), w.end(), back_inserter(x), div2());
1パスでやるには
一か所に書けばいい
モジュラリティが低い
さっきのように書いて、こうなってほしい
vector<int> v, x;for (int i=0; i<100; i++) v.push_back(i);
for (int i=0; i<v.size(); i++)if (v[i]%2==0)x.push_back(v[i]/2);
遅延ストリーム
それぞれの処理で一気に配列を舐めるのがいけない → 遅延させてやればいいtemplate <class T>class stream{ … };
template <class S, class F>class filter_stream{public:typedef S::elem_type elem_type;filter_stream(S &s, F f=F()): s(s), f(f) {}elem_type get(){for (;;){
elem_type r=s.get();if (f(r)) return r;
}}
};
遅延ストリーム(2)
stream<int> s;
filter_stream<typeof(s), is_even> t(s);map_stream<typeof(t), div2> u(t);
while(!u.empty())cout<<u.get()<<endl;
template <class S, class F>class map_stream{public:typedef S::elem_type elem_type;map_stream(S &s, F f=F()): s(s), f(f) {}elem_type get(){elem_type r=s.get();return f(r);
}};
インライン化
テンプレートで書かれているのでインライン化できる
1パスでできる
stream<int> s;
filter_stream<typeof(s), is_even> t(s);map_stream<typeof(t), div2> u(t);
u.get();// => r=t.get(); return div2(r);// => int r; for (;;){ r=s.get(); if (!is_even(r)) break; }// return div2(r);
yieldとの対比
C#でのyield
ループを回すだけのようなことなら同じようにできる
class FilterStream{public FilterStream( … ) { … }public IEnumerator<int> GetEnumerator(){foreach (int t in s)
if (!f(t))yield return i;
}…
コルーチン=遅延評価
yield=コルーチンで遅延評価はできる
遅延評価でyieldのようなこともできる
列挙のようなときにはうまくいく
まとめ
C++の謎テクをいくつか紹介
ブロック=継続
Serializable=Reflectable
コルーチン=遅延評価
pficommon近日リリース予定