2013年12月1日日曜日

入れ子関数 その1 (nested function 1)

例外機構例外機構 その2で紹介した言語 Cでの例外処理の機構はマクロと関数を組み合わせて実現しているが、制約が多くまだ実験の初期段階の域を出ない。

制約のうちの幾つかは finally節に関するものである。
  • finallyは省略できない。
  • 同一スコープの中では finallyが2回以上記述できない。
これは、gccの入れ子関数(nested function)機能の使い方に由来する。

同一スコープの中で複数の入れ子関数を定義した場合になにが起こるか、GCCのオンラインドキュメントNested Functionsで調べてみたが、書かれていない。

そこで、幾つかの実験を行って見た。
使用した環境はつぎの通り。

  • GCC 4.4.3 on Ubuntu 4.4.3-4ubuntu5.1 (x86)
  • GCC 4.6.0 on SunOS 5.11 (x86)

異なるスコープで同じ名称の関数を定義した場合の振る舞い

まず同一スコープで複数回定義した場合の実験の前に、同じ名称の関数を異なるスコープで定義しそれを呼び出した場合になにが実行されるかを試してみる。

次のコードの関数 subが実験コードである。
外側と内側のスコープでは各々 "f(1)"、"f(2)"を表示する関数 fを定義している。
その後、fという名前の関数を呼び出すと呼び出したスコープに一番違い f(2)が呼び出されると思われる。

#include <stdio.h>

void sub() {
 void f() { printf("f(1)\n"); }
 {
  void f() { printf("f(2)\n"); }
  f();
 }
}

int main() {
 sub();
 return 0;
}

ビルドして、実行してみる。
$ make NestedFunctionExample1
cc     NestedFunctionExample1.c   -o NestedFunctionExample1
$ ./NestedFunctionExample1
f(2)

予想通り、f(2)が表示された。
念のため、関数 subを書き換えて、内部の定義が無くとも外部の定義が呼ばれることを確認しておく。
void sub() {
 void f() { printf("f(1)\n"); }
 {
  f();
 }
}

$ make NestedFunctionExample2
cc     NestedFunctionExample2.c   -o NestedFunctionExample2
$ ./NestedFunctionExample2
f(1)

期待通り、外側のスコープで定義した入れ子関数が呼び出された。
内側のスコープで 関数 fを定義する前に fという名前で呼び出しを行なおうとしたらコンパイルは通るのか、通ったとしたら何が呼ばれるのかを試してみる。
void sub() {
 void f() { printf("f(1)\n"); }
 {
  f();
  void f() { printf("f(2)\n"); }
  f();
 }
}

$ make NestedFunctionExample3
cc     NestedFunctionExample3.c   -o NestedFunctionExample3
$ ./NestedFunctionExample3
f(1)
f(2)

コンパイルはエラーにならなかった。自動変数の場合もスコープが違えば同じ名前でもちゃんと区別されるので期待通りだ。
内側の定義を行う前と後では呼び出される関数が異なることが判明した。
これは自然な感じがする。
前方参照を用いて、外側の関数定義を呼び出しより後に定義したらコンパイル、実行はどうなるだろうか。
void sub() {
 auto void f();
 {
  f();
  void f() { printf("f(2)\n"); }
  f();
 }
 void f() { printf("f(1)\n"); }
}

$ make NestedFunctionExample4
cc     NestedFunctionExample4.c   -o NestedFunctionExample4
$ ./NestedFunctionExample4
f(1)
f(2)


おお、すばらしい。

同一スコープで同じ名称の関数を定義した場合の振る舞い

いよいよ、同じ関数名で定義したらどうなるか。
void sub() {
 void f() { printf("f(1)\n"); }
 f();
 void f() { printf("f(2)\n"); }
 f();
}

$ make NestedFunctionExample5
cc     NestedFunctionExample5.c   -o NestedFunctionExample5
NestedFunctionExample5.c: In function ‘sub’:
NestedFunctionExample5.c:11:7: error: redefinition of ‘f’
NestedFunctionExample5.c:9:7: note: previous definition of ‘f’ was here
make: *** [NestedFunctionExample5] Error 1

関数fの再定義はコンパイルエラーとなる。
でも諦めない。なぜなら例外機構では try~catch~finallyを 2個並べてもコンパイルは通っているのだから。
1個目の定義のあと、宣言を入れてみよう。
void sub() {
 void f() { printf("f(1)\n"); }
 f();
 auto void f();
 void f() { printf("f(2)\n"); }
 f();
}

vi NestedFunctionExample6.c
$ make NestedFunctionExample6
cc     NestedFunctionExample6.c   -o NestedFunctionExample6
$ ./NestedFunctionExample6
f(2)
f(2)

コンパイルは通った。
だが、どちらも2個目の定義が呼ばれていて、1個目の定義は無視されている。
これが、例外機構での制約の原因だ。

最初の呼び出しでは1個目の定義のが呼ばれても良いような気がするのだが。

gccのソースを見ていないので憶測だが、同一スコープで同じ名前の関数を定義しようとすると1つ前の実験でわかるように名前の衝突が起きてエラーになるが、
宣言を書けば衝突は回避されるようだ。
しかし、定義の前でも後でも2個目に定義した関数が呼ばれているところを見れば、単純に関数定義を上書きしているのだろう。

宣言は前方参照できるはずなので、定義を呼び出しを逆にしても結果は同じである。
void sub() {
 void f() { printf("f(1)\n"); }
 f();
 auto void f();
 f();
 void f() { printf("f(2)\n"); }
}

$ make NestedFunctionExample7
cc     NestedFunctionExample7.c   -o NestedFunctionExample7
$ ./NestedFunctionExample7
f(2)
f(2)

当然だけど、次の例も再定義でエラーになる。
void sub() {
 auto void f();
 f();
 void f() { printf("f(1)\n"); }
 f();
 void f() { printf("f(2)\n"); }
}

$ make NestedFunctionExample8
cc     NestedFunctionExample8.c   -o NestedFunctionExample8
NestedFunctionExample8.c: In function ‘sub’:
NestedFunctionExample8.c:13:7: error: redefinition of ‘f’
NestedFunctionExample8.c:11:7: note: previous definition of ‘f’ was here
make: *** [NestedFunctionExample8] Error 1

これまで定義が2個の例を示したが、次のように3個以上でも同じことである。
void sub() {
 auto void f();
 f();
 void f() { printf("f(1)\n"); }

 auto void f();
 f();
 void f() { printf("f(2)\n"); }

 auto void f();
 f();
 void f() { printf("f(3)\n"); }

 auto void f();
 f();
 void f() { printf("f(4)\n"); }
}

$ vi NestedFunctionExample9.c
$ make NestedFunctionExample9
cc     NestedFunctionExample9.c   -o NestedFunctionExample9
$ ./NestedFunctionExample9
f(4)
f(4)
f(4)
f(4)

以上の結果から、定義と宣言を分けると同一スコープに同じ名称の関数定義を記述することはできるが、最後の定義のみが有効となることが判明した。

これでは現在の例外機構の案では実用にはならない。

gccのバージョンが変わったら挙動が変わる可能性もある。

0 件のコメント:

コメントを投稿