C++では返り値の型がvoidでない関数からreturn文を使わずに戻ろうとしてはダメ、ゼッタイ。

概要

C++では、返り値の型がvoidでない関数からreturn文を使わずに戻ろうとしてはいけません。

なぜいけないか

C++では、返り値の型がvoidでない関数の実行がreturn文を通らずに最後に到達すると、未定義動作になります。
未定義動作になると何が起こってもおかしくありません。

C言語では?

C言語では、返り値の型がvoidでない関数からreturn文を使わずに戻ろうとしただけでは未定義動作にはなりません。
ただし、その戻り値を使うと未定義動作になります。

根拠は?

C言語では

C言語の規格書に近いとされるN1570 6.9.1 Function definitions の 12 に、以下の記述があります。

If the } that terminates a function is reached, and the value of the function call is used by
the caller, the behavior is undefined.

関数最後の}に到達し(すなわち、return文をつかわずに戻り)、
かつその返り値が呼び出し手によって使われると未定義動作になる、とされています。

C++では

C++の規格書に近いとされるN3337 6.6.3 The return statement の 2 に、以下の記述があります。

Flowing off the end of a function is equivalent to a return with no value; this results in undefined behavior in a value-returning function.

関数の最後まで実行する(すなわち、return文をつかわずに戻ろうとする)と、
値を返す関数(すなわち、返り値の型がvoidでない関数)では未定義動作になる、とされています。

実際のコンパイル例

Compiler Explorer を用いて以下のコードをコンパイルしてみます。
hogeは返り値の型がvoidでなくreturn文が無い関数(危険)、
fugaは返り値の型がvoidである関数、fooは返り値の型がvoidではなくreturn文がある関数です。

#include <stdio.h>

int hoge(int a) {
    printf("hoge %d\n", a);
}

void fuga(int a) {
    printf("fuga %d\n", a);
}

int foo(int a) {
    printf("foo %d\n", a);
    return 0;
}

C言語の場合

関数hogefugaでは別の関数をjmpで呼び出すことでその関数からの戻りを利用して戻り、
関数fooではret命令を用いて戻っています。
これだけでは、危険は無いようです。

.LC0:
        .string "hoge %d\n"
hoge:
        movl    %edi, %esi
        xorl    %eax, %eax
        movl    $.LC0, %edi
        jmp     printf
.LC1:
        .string "fuga %d\n"
fuga:
        movl    %edi, %esi
        xorl    %eax, %eax
        movl    $.LC1, %edi
        jmp     printf
.LC2:
        .string "foo %d\n"
foo:
        subq    $8, %rsp
        movl    %edi, %esi
        xorl    %eax, %eax
        movl    $.LC2, %edi
        call    printf
        xorl    %eax, %eax
        addq    $8, %rsp
        ret

https://gcc.godbolt.org/z/fWKToh

C++の場合

関数fugaでは別の関数をjmpで呼び出すことでその関数からの戻りを利用して戻り、
関数fooではret命令を用いて戻っています。

一方、この例においては、未定義動作である関数hogeには関数から戻る命令が無く、
その次にある文字列を機械語命令として実行しに行ってしまいます。
その結果、デタラメな実行結果になったり、不正な命令があったとして強制終了したりする可能性があります。

.LC0:
        .string "hoge %d\n"
hoge(int):
        movl    %edi, %esi
        subq    $8, %rsp
        movl    $.LC0, %edi
        xorl    %eax, %eax
        call    printf
.LC1:
        .string "fuga %d\n"
fuga(int):
        movl    %edi, %esi
        xorl    %eax, %eax
        movl    $.LC1, %edi
        jmp     printf
.LC2:
        .string "foo %d\n"
foo(int):
        subq    $8, %rsp
        movl    %edi, %esi
        xorl    %eax, %eax
        movl    $.LC2, %edi
        call    printf
        xorl    %eax, %eax
        addq    $8, %rsp
        ret

https://gcc.godbolt.org/z/hqcaoK

結論

C言語ではreturn文を使わずに戻るだけでは未定義動作にならないとはいえ、
せっかく値を返すように宣言されているのにその値を全く使わない、というのはおかしいです。
また、C++ではreturn文を使わずに戻るだけで不正動作の原因になり得ます。
したがって、値を返さない関数の戻り値の型はC言語、C++ともにvoidにしましょう。
また、うっかり値を返す関数でreturn文を実行させるのを忘れても気付けるよう、
コンパイラを警告を出す設定にしましょう。

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
コメント
この記事にコメントはありません。
あなたもコメントしてみませんか :)
すでにアカウントを持っている方は
ユーザーは見つかりませんでした