GCCの入れ子関数(Nested Functions)の仕組みを深掘り!C++ラムダ式との決定的な違いとは?
Implementation of GCC's Nested Functions (vs. C++ Lambdas)
Implementation of GCC's Nested Functions (vs. C++ Lambdas)
C言語の拡張機能として知られるGCCの「入れ子関数(Nested Functions)」は非常に強力ですが、現代的なC++のラムダ式とは内部実装や挙動に大きな違いがあります。本稿では、GCCがどのようにこのスタックベースの関数を実装しているのか、その舞台裏とC++の関数オブジェクトとの比較を解説します。
この記事、GCCのネストされた関数が持つ複雑さとメリットの80%をなぜか無視していて、これだと使う意味が全くないよ。具体的には、関数ポインタにキャストして、sort()関数の比較関数として渡すような使い方ができるはずなんだ。ネストされた関数が呼び出される際に、呼び出しスタック上の正しい親フレームのパラメータを差し替えるのはかなり厄介で、ABI(ia-64)での明示的なサポートか、トランポリンを構築するための実行可能スタック、あるいは関数ポインタを特別なビットセットでラップする特殊なロジックが必要になる。[1]
これがないと、ネストされた関数は記事にあるような「書き直された」例と同じで、コンパイラや言語のサポートなしでも簡単に自力で実装できちゃうレベルだね。
C++のラムダ式なら、生の関数ポインタではなく特殊なオブジェクトとして扱えるから、この問題は発生しないよ。
[1] https://gcc.gnu.org/onlinedocs/gccint/Trampolines.html (https://gcc.gnu.org/onlinedocs/gccint/Trampolines.html)
GCCでは、ネストされた関数は初期のミドルエンドパスでローレベルに変換される。この過程で、ネストされた関数からアクセスされる親の変数はすべて単一の合成構造体にまとめられ、その構造体へのポインタが隠し引数としてネストされた関数に渡される
一般的にこれは明示的なラムダより少しスマートだけど、ベストなのはGCCが呼び出し側のスタックレイアウトを把握して、保存されたスタック変数(やレジスタ)を直接操作できるようになることじゃないかな。結局のところ、デバッガはコードの各行でどの変数がどこにあるかを追跡できるわけだし、技術的には可能だよね。
実用的かどうかは分からないけど、実現できたらかなりの技術的勲章になりそう。
D言語のネストされた関数は「スタティックリンク」と「ダイナミックリンク」で実装されているよ。ダイナミックリンクはみんな知ってる通り、呼び出し元のスタックフレーム(x86_64ならEBP)を指すポインタのこと。面白いのはスタティックリンクの方で、これは静的に囲んでいるスタックフレームへのポインタなんだ。
だから、2つ上の階層にある関数内のスタック変数にアクセスしたい場合は、スタティックリンクを2回辿るだけでいい。
D言語においてネストされた関数への参照は、関数へのポインタとスタティックリンクのペアで表現される(D言語では「デリゲート」と呼ばれている)。面白いことに、これはメンバー関数への参照と同じレイアウトで、「this」ポインタがスタティックリンクの代わりを務める形になっているんだ。
つまり、ネストされた関数への参照は、メンバー関数への参照とABIレベルで互換性があるってこと。D言語のラムダ式は、単にネストされた関数のよりコンパクトな記法に過ぎないんだよ。