なぜブロックなのか? - 後編 [プログラミング]
昨日GCDでブロックを使うといかに簡単になるか、というのをコードの例を使ってブロックを使わない場合との比較で示してみた。
今日はその続きで、ブロックを動かすためにコンパイラが何をしているか、ということからプログラマにとってのブロックの位置づけを考えてみる。
コンパイラはブロックの宣言が現れたとき、レキシカルスコープを実現するためにちょっとした構造体をヒープにとるだろう。
無名ブロックの場合、どんなデータが入った構造体かというと
構造体のデータのひとつめはブロックがオブジェクトとしても扱えることを保証するためのものである。Core Foundationと同じメカニズムを使えばいい。
4番目の「ブロックの実行状態に関する情報」というのは
構造体にはローカルな自動変数の値のコピーを含んでいて、これがレキシカルスコープを実現する。もともとコンパイラは自動変数の表を作って領域を確保するということをやっているので、これはコンパイラにとってちょっとした付加作業でしかない。
__block修飾子というのが導入されていて、これで修飾された自動変数はブロック内で値を書き換えることができる、となっている。これはその値がブロックの構造体にコピーされるのではなく、その変数そのものがブロック構造体のなかに確保される。自動変数だから本来はスタックに確保されるはずだけど、それではつじつまが合わなくなる(例えばブロックの実行中にその変数が宣言された関数が終わったらどうなるか?)。__block修飾された変数はブロックの内側と外側の両方とも、同じブロック構造体の変数を参照する。
コンパイラがソースを読み込んでブロックの宣言に到達したとき、
「ブロックを使わないGCD」ではdispatch_wob_async()を呼ぶ前にrefConに自動変数の値をコピーしたけど、ブロックではこれもコンパイラが自動でやることになる。
ということで、やってることは実質的にはコールバックとrefConと同じことだけど、「レキシカルスコープ」というスタイルを使うことで、プログラマの手間を激減させることができる、というメリットが手に入る。
これらすべてをコンパイラがしている、というわけではないだろう。GCDの本体のdispatchライブラリと共同で作業することになる。例えばisaポインタの設定はコンパイラがやるべき仕事ではない。しかし、Cocoa/Objective-Cのレベルでは言語仕様がCocoaのランタイムやFoundationフレームワークに依存していて切り分けがあいまいになっている。Cの古き良きシンプルさはすでに守られていない、ということを思い出せばAppleにとっては仕事の切り分けは「どうでもいいこと」なのかもしれない。
どうせここまでやらせるんだったら、これまでコールバックを使っていたCore Foundationのオブジェクトは全部ブロックを使うようにしたほうがいいだろう。他にもCoreなんとかと言う名前のCレベルのフレームワークはいっぱいあってそこでもコールバックは使われている。それもブロックにすればいい。
また、Cocoa/Objective-Cのレベルでも、コールバックと似たような動作をさせることがある。そういうところではブロックを使うほうが簡単に書ける場合が多い。実際にCore Foundationではコールバックとブロックの両方を使える状態になっているオブジェクトがあるし、Cocoa/Objective-CでもNSNotificationCenterやNSSavePanelにすぐ導入されている。いずれはコールバックはブロックに淘汰されることになるんだろう。
さっきの例で、メソッド内部でセットアップするマトリクスに__block修飾子をつけているのは、それがないとコンパイラがwarningを出すせいである。
また、多次元配列の自動変数をブロック内部でアクセスしようとすると、コンパイラがエラーを吐く。そうしないといけない理由がよくわからないんだけど、さっきの例ではほんとはマトリクスの定義を
ブロックはある局面ではたしかに使いやすいんだけど、参照可能な変数を増やすことになって、バグを生む温床にもなりえる。また、生存期間の微妙な問題も起こりえる。Cのスコープは単純だった。関数の自動変数はその関数が実行されている間だけ生存していて、関数の外では見えないだけでなくそもそも存在しない、というのがCの動作なんだけど、その関数内部でブロックが宣言されたとたんに自動変数はブロックの生存期間に依存するようになってしまう。スコープはCに較べてずっと複雑になる。便利な機能なら何でも追加すればいい、というものではない。
普通に使っていれば問題ない、というようなもんなんだけど、特殊な局面で微妙なバグを生む、ということはありえる。プログラマはブロックに対してはトリッキーなことをせずに、「普通に使う」ということを意識する必要がある。
例えば、言語仕様の拡張ではなく、単なるCのマクロ、あるいは専用のプリプロセスにするという解決策もあったのではないか、という気もする。その場合完全なレキシカルスコープを実現するのは難しい(不可能ではないがコンパイラがするのと同じことを前もってする必要がある)。完全なレキシカルスコープのかわりに、ブロック内部で参照する自動変数を宣言してから使う、ということにでもすればコンパイラをいじる必要はない。
もちろんその場合はプログラマの負担は増えるし、自動変数の生存期間の問題は解決しない(あるいは、プログラマが制御しなければいけなくなる)、ということになってしまってまったく無意味である。そんな中途半端なことをするぐらいなら、Cに「クロージャ」を導入すればいいじゃん、というわりきりは今のAppleらしいと言える。
ブロック導入の功罪はいずれ問われるだろう。
今日はその続きで、ブロックを動かすためにコンパイラが何をしているか、ということからプログラマにとってのブロックの位置づけを考えてみる。
1.4 ブロックのためにコンパイラは何をしているか
それではGCDでブロックを使うとき、コンパイラはどんな作業をしているか、考えてみる。これからは僕の想像で、実際に何をしているのかは知らない。コンパイラもGCDもソースが公開されているのでそれを見ればいいんだろうけど、そういうのは若い人に任せて、「もし僕がブロックを実装するならどうするか」を書くことにする。コンパイラはブロックの宣言が現れたとき、レキシカルスコープを実現するためにちょっとした構造体をヒープにとるだろう。
無名ブロックの場合、どんなデータが入った構造体かというと
- ブロックであることを示すためのisaポインタと参照カウンタ
- ブロックの実行コードへのポインタ
- ローカルな自動変数すべての値のコピー
- ブロックの実行状態に関する情報
- その他キューへの付加的な情報
構造体のデータのひとつめはブロックがオブジェクトとしても扱えることを保証するためのものである。Core Foundationと同じメカニズムを使えばいい。
4番目の「ブロックの実行状態に関する情報」というのは
- ブロックの実行が終わったか
- 呼び出し元の関数から出たか(スタックがポップされたか)
- (名前付きブロックの場合)ブロックがコピーされたか
構造体にはローカルな自動変数の値のコピーを含んでいて、これがレキシカルスコープを実現する。もともとコンパイラは自動変数の表を作って領域を確保するということをやっているので、これはコンパイラにとってちょっとした付加作業でしかない。
__block修飾子というのが導入されていて、これで修飾された自動変数はブロック内で値を書き換えることができる、となっている。これはその値がブロックの構造体にコピーされるのではなく、その変数そのものがブロック構造体のなかに確保される。自動変数だから本来はスタックに確保されるはずだけど、それではつじつまが合わなくなる(例えばブロックの実行中にその変数が宣言された関数が終わったらどうなるか?)。__block修飾された変数はブロックの内側と外側の両方とも、同じブロック構造体の変数を参照する。
コンパイラがソースを読み込んでブロックの宣言に到達したとき、
- ブロック構造体を確保する
- 自動変数の値をコピーするようなコードを挿入する
- ブロック本体をコンパイルして、そこへのポインタを構造体にセットする
- ブロックの参照カウンタを制御するコードを呼び出し元の関数に挿入する(これはARCがやってることと同じ)
1.5 つまりどういうことかというと
ようするにコンパイラは何をしているかと言うと、さっきの「ブロックを使わないGCD」の場合にやることを自動的に(コンパイラが勝手に)やっている、ということである。ブロックの構造体にあるデータの、2番目はコールバック関数へのポインタであり、3番目はrefConデータである(「ブロックを使わないGCD」の場合、ブロックはオブジェクトではないので残りのデータは不要だった)。「ブロックを使わないGCD」ではdispatch_wob_async()を呼ぶ前にrefConに自動変数の値をコピーしたけど、ブロックではこれもコンパイラが自動でやることになる。
ということで、やってることは実質的にはコールバックとrefConと同じことだけど、「レキシカルスコープ」というスタイルを使うことで、プログラマの手間を激減させることができる、というメリットが手に入る。
これらすべてをコンパイラがしている、というわけではないだろう。GCDの本体のdispatchライブラリと共同で作業することになる。例えばisaポインタの設定はコンパイラがやるべき仕事ではない。しかし、Cocoa/Objective-Cのレベルでは言語仕様がCocoaのランタイムやFoundationフレームワークに依存していて切り分けがあいまいになっている。Cの古き良きシンプルさはすでに守られていない、ということを思い出せばAppleにとっては仕事の切り分けは「どうでもいいこと」なのかもしれない。
どうせここまでやらせるんだったら、これまでコールバックを使っていたCore Foundationのオブジェクトは全部ブロックを使うようにしたほうがいいだろう。他にもCoreなんとかと言う名前のCレベルのフレームワークはいっぱいあってそこでもコールバックは使われている。それもブロックにすればいい。
また、Cocoa/Objective-Cのレベルでも、コールバックと似たような動作をさせることがある。そういうところではブロックを使うほうが簡単に書ける場合が多い。実際にCore Foundationではコールバックとブロックの両方を使える状態になっているオブジェクトがあるし、Cocoa/Objective-CでもNSNotificationCenterやNSSavePanelにすぐ導入されている。いずれはコールバックはブロックに淘汰されることになるんだろう。
1.6 ブロックの功罪
便利なブロックだけど、いくつか制限がある。ブロック内から自動変数をアクセスする場合は、const修飾されているとみなされる。普通の変数の場合はそれほど問題にならないけど、ポインタは微妙になる。呼び出し元の関数(あるいはメソッド)の実行が終わってスタックからポップされると自動変数は存在しなくなる。普通の変数はブロック構造体にコピーされているので問題ないけど、ポインタの場合に実体がスタックの上にあると問題が発生する。さっきの例で、メソッド内部でセットアップするマトリクスに__block修飾子をつけているのは、それがないとコンパイラがwarningを出すせいである。
また、多次元配列の自動変数をブロック内部でアクセスしようとすると、コンパイラがエラーを吐く。そうしないといけない理由がよくわからないんだけど、さっきの例ではほんとはマトリクスの定義を
const int dimension = 4; typedef double vector[dimension]; typedef vector matrix[dimension];としたかった。ようするに単なるベクトルの配列。ところがこれをブロック内部で使おうとするとコンパイラが「定義がネストしてるとダメ」と怒るのでしょうがなしに上のように構造体として定義した。
ブロックはある局面ではたしかに使いやすいんだけど、参照可能な変数を増やすことになって、バグを生む温床にもなりえる。また、生存期間の微妙な問題も起こりえる。Cのスコープは単純だった。関数の自動変数はその関数が実行されている間だけ生存していて、関数の外では見えないだけでなくそもそも存在しない、というのがCの動作なんだけど、その関数内部でブロックが宣言されたとたんに自動変数はブロックの生存期間に依存するようになってしまう。スコープはCに較べてずっと複雑になる。便利な機能なら何でも追加すればいい、というものではない。
普通に使っていれば問題ない、というようなもんなんだけど、特殊な局面で微妙なバグを生む、ということはありえる。プログラマはブロックに対してはトリッキーなことをせずに、「普通に使う」ということを意識する必要がある。
例えば、言語仕様の拡張ではなく、単なるCのマクロ、あるいは専用のプリプロセスにするという解決策もあったのではないか、という気もする。その場合完全なレキシカルスコープを実現するのは難しい(不可能ではないがコンパイラがするのと同じことを前もってする必要がある)。完全なレキシカルスコープのかわりに、ブロック内部で参照する自動変数を宣言してから使う、ということにでもすればコンパイラをいじる必要はない。
もちろんその場合はプログラマの負担は増えるし、自動変数の生存期間の問題は解決しない(あるいは、プログラマが制御しなければいけなくなる)、ということになってしまってまったく無意味である。そんな中途半端なことをするぐらいなら、Cに「クロージャ」を導入すればいいじゃん、というわりきりは今のAppleらしいと言える。
ブロック導入の功罪はいずれ問われるだろう。
2013-06-01 23:48
nice!(0)
コメント(2)
トラックバック(0)
面白くて、かつ(必然性を感じて)とても納得でき・わかりやすいです。ユーザ(プログラマ)がAppleのルールを覚えるのは面倒な気もしますが、確かに便利そうですね。
by jun Hirabayashi (2013-06-02 09:28)
コメントありがとうございます。
おほめいただき恐縮です。
実は、最初に「ブロック」を見たときに、僕は「こんなの掟破りの反則だよ、Appleはいい気になって好き勝手なことをやるんだから、まったくもう」と思いました。
ブロックなんか使ってやるもんか、としばらく意地を張っていたのですが、試しにちょっと使ってみるとバカみたいに簡単なので、こりゃCallback関数には戻れんわ、と思ってしまいました。
恥ずかしながら僕はAppleに懐柔されたかっこうです。
修飾子がかえって煩わしいせいで僕はARC(Automatic Reference Counting)も使っていないのですが、これも使いだすとあっさり宗旨替えしてしまうかもしれません....
by decafish (2013-06-02 13:06)