C言語でのreturn
は他動詞ではなく自動詞
2種類の"return"
C言語において、関数は戻り値を返したり返さなかったりする。
// Function with return value.
int add3(int x) {
return x + 3; // (1)
}
// Function without return value.
void do_nothing(void) {
// Return explicitly.
return; // (2)
}
ここで、(1)のようなreturn
を「値を返す」ものだと思い込んでしまっていると、(2)のreturn;
の意味がわからなくなることがあるらしい。
この記事では、C言語におけるreturnが「返す」ものでなく「帰る(返る)」ものと捉える方が自然であることを、コンパイル結果を見ながら示していく。
アセンブリレベルで見る
ひとまず上記のtest.cをコンパイルした結果をアセンブリ言語で見てみよう。 環境は 64 bit Linux, gcc-5.4.0 である。
$ uname -a Linux veg 4.6.3-gentoo #1 SMP PREEMPT Sun Jul 3 10:47:16 JST 2016 x86_64 Intel(R) Core(TM) i7-4500U CPU @ 1.80GHz GenuineIntel GNU/Linux $ gcc --version gcc (Gentoo 5.4.0 p1.0, pie-0.6.5) 5.4.0 Copyright (C) 2015 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ gcc -O0 -c test.c $ objdump -d -M intel test.o >test.c-objdump
test.o: ファイル形式 elf64-x86-64
セクション .text の逆アセンブル:
0000000000000000 <add3>:
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: 89 7d fc mov DWORD PTR [rbp-0x4],edi
7: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
a: 83 c0 03 add eax,0x3
d: 5d pop rbp
e: c3 ret
000000000000000f <do_nothing>:
f: 55 push rbp
10: 48 89 e5 mov rbp,rsp
13: 90 nop
14: 5d pop rbp
15: c3 ret
これらのうち、コンパイラが自動生成した箇所を除いてC言語っぽく書き直してみると、以下のようになる。
// Get the 1st argument.
EAX = *((DWORD *)(rbp-0x4)); // mov eax,DWORD PTR [rbp-0x4]
// Add 3.
EAX += 3; // add eax,0x3
// Return.
return; // ret
add3
// Do nothing.
; // nop
// Return.
return; // ret
do_nothing
値を返す場合も何もせず返る場合も、全く同じret
命令が呼ばれているのである。
ret
命令は、だいたい「関数の呼び出し元のアドレスにジャンプする」のような動作を行う、言ってみればgotoのようなものである[0]。
では、add3()
では第1引数に3を足した値を返すが、これはどうやって呼び出し元に伝えられているのか。
実は、戻り値は呼び出された関数が特定のレジスタ(CPU内の記憶領域)、ここではEAX
に格納し、呼び出し元がそのレジスタを参照することで値を得る、という決まりになっているのである[1]。
すなわちreturn
には本来、単に「関数の呼び出し元アドレスへ戻る」という自動詞的な意味しかなく、値を返すというのは「約束の場所に値を置いておく」という利便性のための追加機能である、と考えると自然である。
おまけ: 誰が(何が)返るのか
return
が「関数の呼び出し元アドレスへ戻る」という意味なのは良いとして、その主語は何なのか。
これは「処理」であると考えられる。
「処理が進む」とか「処理が止まる」とか、そういう文脈での「処理」であって、プログラムやアルゴリズム自体を指しての「処理」ではない。
もっと言えば、ここでの「処理」とは実際には「実行中の命令が格納されているメモリアドレス上にいる仮想的な存在」のようなもの[2]である。
或いは、コードの実行されている行をちょこちょこ走る小人のようなものを想定しても良いだろう。
プログラマにとって身近なものでは、プログラムカウンタ(x86であればEIP
、x64であればRIP
)が近いかもしれない。