何とは言わない天然水飲みたさ

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)
}
      
test.c

ここで、(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
      
test.c-objdump

これらのうち、コンパイラが自動生成した箇所を除いて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)が近いかもしれない。