これらのことを行うことができます。これは主に、実際にはそれほど難しいことではないためです。
コンパイラの観点からは、別の関数内に関数宣言を含めることは、実装するのが非常に簡単です。コンパイラには、関数内の宣言が関数内の他の宣言(たとえばint x;
)を処理できるようにするメカニズムが必要です。
通常、宣言を解析するための一般的なメカニズムがあります。コンパイラを書いている人にとって、そのメカニズムが別の関数の内部または外部のコードを解析するときに呼び出されるかどうかはまったく問題ではありません-それは単なる宣言なので、宣言があることを十分に知っているときは、宣言を処理するコンパイラの部分を呼び出します。
実際、関数内でこれらの特定の宣言を禁止すると、おそらくさらに複雑になります。コンパイラーは、関数定義内のコードを既に調べているかどうかを確認し、それに基づいてこの特定の宣言を許可するか禁止するかを決定するために、完全に無償のチェックを行う必要があるためです。宣言。
それは、入れ子関数がどのように異なるのかという問題を残します。入れ子関数は、コード生成にどのように影響するかによって異なります。ネストされた関数を許可する言語(Pascalなど)では、通常、ネストされた関数のコードが、ネストされた関数の変数に直接アクセスできることを期待します。例えば:
int foo() {
int x;
int bar() {
x = 1;
}
}
ローカル関数がなければ、ローカル変数にアクセスするためのコードはかなり単純です。典型的な実装では、実行が関数に入ると、ローカル変数用のスペースのブロックがスタックに割り当てられます。すべてのローカル変数はその単一のブロックに割り当てられ、各変数はブロックの最初(または最後)からの単なるオフセットとして扱われます。たとえば、次のような関数について考えてみましょう。
int f() {
int x;
int y;
x = 1;
y = x;
return y;
}
コンパイラー(余分なコードを最適化していないと仮定)は、これとほぼ同等のコードを生成する可能性があります。
stack_pointer -= 2 * sizeof(int);
x_offset = 0;
y_offset = sizeof(int);
stack_pointer[x_offset] = 1;
stack_pointer[y_offset] = stack_pointer[x_offset];
return_location = stack_pointer[y_offset];
stack_pointer += 2 * sizeof(int);
特に、ローカル変数のブロックの先頭を指す1つの場所があり、ローカル変数へのすべてのアクセスはその場所からのオフセットとして行われます。
ネストされた関数では、それはもはや当てはまりません。代わりに、関数はそれ自体のローカル変数だけでなく、ネストされているすべての関数にローカルな変数にもアクセスできます。オフセットを計算する「stack_pointer」を1つだけ持つのではなく、スタックをさかのぼって、ネストされている関数にローカルなstack_pointersを見つける必要があります。
さて、ささいなケースでも、それほどひどいわけではありません。bar
がfoo
、の中にネストされている場合はbar
、前のスタックポインタでスタックを検索して、foo
の変数にアクセスできます。正しい?
違う!そうですね、これが当てはまる場合もありますが、必ずしもそうとは限りません。特に、bar
再帰的である可能性があります。その場合、の特定の呼び出しはbar
周囲の関数の変数を見つけるために、スタックをバックアップするほぼ任意の数のレベルを調べる必要がある場合があります。一般的に言えば、次の2つのいずれかを行う必要があります。スタックに追加のデータを配置して、実行時にスタックを検索して周囲の関数のスタックフレームを見つけることができるようにするか、またはポインタを効果的にに渡します。ネストされた関数の非表示パラメーターとしての周囲の関数のスタックフレーム。ああ、しかし、必ずしも周囲の関数が1つだけではありません。関数をネストできる場合は、おそらく(多かれ少なかれ)任意の深さでネストできるため、任意の数の非表示パラメーターを渡す準備ができている必要があります。つまり、通常、スタックフレームのリンクリストのようなものが周囲の関数にリンクされていることになります。
ただし、これは、「ローカル」変数へのアクセスが簡単なことではない可能性があることを意味します。変数にアクセスするための正しいスタックフレームを見つけることは簡単ではない可能性があるため、周囲の関数の変数へのアクセスも(少なくとも通常は)真のローカル変数へのアクセスよりも遅くなります。そしてもちろん、コンパイラは、適切なスタックフレームを見つけたり、任意の数のスタックフレームを介して変数にアクセスしたりするためのコードを生成する必要があります。
これは、Cが入れ子関数を禁止することによって回避していた複雑さです。さて、現在のC ++コンパイラが1970年代のビンテージCコンパイラとはかなり異なる種類の獣であることは確かに真実です。複数の仮想継承のようなものでは、C ++コンパイラは、どのような場合でも、これと同じ一般的な性質のものを処理する必要があります(つまり、そのような場合の基本クラス変数の場所を見つけることも重要です)。パーセンテージベースでは、ネストされた関数をサポートしても、現在のC ++コンパイラにそれほど複雑さは追加されません(gccなどの一部はすでにそれらをサポートしています)。
同時に、それはめったに多くの有用性を追加しません。あなたが何かを定義する場合は特に、働き関数の関数の内部のように、あなたは、ラムダ式を使用することができます。これが実際に作成するのは、関数呼び出し演算子(operator()
)をオーバーロードするオブジェクト(つまり、あるクラスのインスタンス)ですが、それでも関数のような機能を提供します。ただし、周囲のコンテキストからデータをキャプチャする(またはキャプチャしない)ことをより明確にします。これにより、まったく新しいメカニズムとその使用のための一連のルールを発明するのではなく、既存のメカニズムを使用できます。
結論:ネストされた宣言は最初は難しく、ネストされた関数は些細なことのように見えますが、多かれ少なかれ逆です。ネストされた関数は、実際にはネストされた宣言よりもサポートがはるかに複雑です。
one
関数定義で、他の2つは宣言です。