関数内でreturnを省略することで発生しうる未定義バグ


タイトルの通りです。私が見たやつ共有させてください。
因みにreturn文がないコードってこんな感じの奴です。

15
この15ってなんですかねって話です。

returnを省略してもコンパイル可能だけど未定義のケース

返り値がvoidの場合はreturn文は省略できますが、int型の場合はどうでしょうか。
下記のreturn文を省略した2つの関数を含むコードはclang7.0.0とgcc8.0.1ではコンパイル可能です。
gccも7.2.0以下では-Wallオプションを付けなければWarningすら出ません。


#include <cstdio>

int f() {/*returnがない!*/}

int main()
{
    printf("%d", f());
    //main()はなくても大丈夫
}

f()main()は共に返り値がint型ですが、returnが省略されています。
c++ではmain関数に限り、そのmain関数の戻り値の型が int であれば return 0; があったと見なされるため特に問題ありません。
しかしf()についてはコンパイル可能&関数呼び出し可能ですが、その返り値については未定義となっております。

このような関数は返り値を利用しない限りは正しく動作するというのも厄介なのですが、それ以上に厄介な特性を持っています。

return文を省略された関数の返り値はeaxレジスタの値

例えば上記のf()の返り値を返すコードWandboxさんで実行すると下記のような結果になります。
Wandbox-実行結果

実は、この419632とは恐らくeaxレジスタに格納されている値です。
未初期化の場合もあれば何かしらの値がセットされている場合もあります。
x86アーキテクチャにおいては、関数の返り値は慣例的にeaxレジスタでやり取りされています。
そしてclangやgccにおいては、呼び出し側はeaxの値がいつセットされたものかに関わらずeaxレジスタの値を返り値として受け取ります。
これが非常に厄介。

つまり最初のコードの内部では下記のような操作が行われているのです。

  1. f()が呼ばれる
  2. g()が呼ばれる
  3. g()の内部でeaxレジスタに15をセット。(returnの処理)
  4. g()終了
  5. f()終了 (returnの省略)
  6. eaxのレジスタをf()の返り値として受け取る ←本当はg()の返り値!!

#include <cstdio>
int g() {return 15; //eaxに15をmov}
int f() {
g();// g()の値を返り値としてeaxにmov
//特に後に何も処理がないのでeaxに返り値が残り続ける。
}
int main(){printf("%d", f());}// f()の内部で呼ばれたg()の返り値がeaxに残り続けておりそれを返り値と勘違いして受け取る。

しかしf()のこの動作は未定義です。
試しにWandboxでclang7.0.0でコンパイルすると実行時エラーで死にます。
つまり、関数の返り値を返そうと思ったけどreturn文を書き忘れたコードを書いた場合、ある環境andコンパイラによっては一見正しく動作をするが、別の環境orコンパイラでは実行時エラーor未定義の値が返されるということです。
辛い。
しかもgcc7.2.0ではデフォルトではWarningにならないためデバッグも辛い。。。

安心してください

  • ClangではWarningになります。
  • -WallをつければWarningになります。
  • gccも8以上ではデフォルトでWarningになります。

-Wallつけよう

なぜこのようなコードが許されるのか

一つの可能性というか、return文を省略できる妥当性としては、returnが省略されている箇所が、プログラマのみが知っている事前知識によって、絶対に通らないルートとなっている可能性をコンパイラが排除しきれないからです。

例えば下記のようなコードが考えられます。


int f1() {
  if(check()) return 0;
  Fatal(); //内部でexit()が呼ばれる

  //絶対にここから下は通らない!
  ...
}

int f2() {
switch (num) {
    case 0:
        return 0;
    case 1:
        return 1;
    }
   // numは何らかの制約で上記条件以外の値にならないため、絶対にここは通らない!!
}

このような絶対に通らない領域にreturnを必ず書くべきかと言われると、そうではないという人が存在することも一定の理解を示せます。
ですが絶対に起きないと思っているルートこそ通りがちという話もあります。
私としては必ずreturnを書きつつ、そのルートを通ってしまった事が分かるような処理を用意したほうがよいのかなぁと思います。
というかそもそも上みたいなreturnの書き方しない方が良いです。
てかWarning出るし。

こういうreturnを通らないルートがある可能性がある場合はコンパイルエラーにしてほしいのですが、C++は後方互換を大事にするのでなかなか難しいようです。(とはいえgccがデフォルトでWarning出すのが8.0.1というのは遅すぎのような)

Qiitaはポエムを書く場所です。

何かこのreturnの省略について、明確な仕様が言及されているのをご存知の方がいたら教えて頂きたいです。
言語処理系を作った事がある人は分かると思いますが、仕様や構文解析の緩さにより思わぬコードが正しく動いてしまうこともあります。(そこが面白いところでもあったり怖いところでもあったり。)
c++のテンプレートも現在のTMPのよおうな使われ方は想定していなかった用法だったらしいです。
私個人としてはこのreturn文の省略もそんな過程で生まれたのかなと思ったり思わなかったり。