【お知らせ】プログラミング記事の投稿はQiitaに移行しました。

引数と戻り値の省略

PE勉強会(1)で配布した資料(id:n7shi:20110219)のサンプルコードに問題があり、ご指摘を受けました。調べた所、色々な意味で突っ込みどころが満載でした。

int _main() {}

結論から言うと、以下のように書くのが無難なようです。

int _main(void) { return 0; }

ただのmainではなく _ が付いていますが、その理由については後述します。

1. mainのreturn省略

まず _ を付加しない普通のmainについてです。C++とC99ではmainでreturnを省略すると自動的に0を返すコードが付加されます。詳細はid:syohexさんがまとめてくださいました。

念のため再確認してみます。以下のコードを main.c として保存します。

int main() {}

これをデフォルト(ANSI Cをベースに独自拡張)、ANSI C、C89、C99、C++コンパイルしてアセンブリを比較します。

$ gcc -Wall -S -masm=intel -o main-gcc.s main.c
$ gcc -Wall -S -masm=intel -o main-ansi.s -ansi main.c
$ gcc -Wall -S -masm=intel -o main-c89.s -std=c89 main.c
$ gcc -Wall -S -masm=intel -o main-c99.s -std=c99 main.c
$ g++ -Wall -S -masm=intel -o main-g++.s main.c

結果は以下の通りです。

指定 警告 mov eax, 0
デフォルト あり なし
ANSI C あり なし
C89 あり なし
C99 なし あり
C++ なし あり

C99とC++では言語仕様でmainのreturnを省略すると0になると定義されているため、警告なしで0を返すコードが埋め込まれます。それ以外では不定値となり警告されます。

2. 非mainのreturn省略

冒頭のコードでは _ を付けて_mainとしています。これは名前をmainに似せただけの普通の関数です。説明の都合上CRTを外したかったため、特別扱いされるmainを避けて普通の関数で書いたためです。

先ほどと同様の検証を行います。以下を_main.cとして保存します。

int _main() {}

アセンブリを出力して比較します。

$ gcc -Wall -S -masm=intel -o _main-gcc.s _main.c
$ gcc -Wall -S -masm=intel -o _main-ansi.s -ansi _main.c
$ gcc -Wall -S -masm=intel -o _main-c89.s -std=c89 _main.c
$ gcc -Wall -S -masm=intel -o _main-c99.s -std=c99 _main.c
$ g++ -Wall -S -masm=intel -o _main-g++.s _main.c

すべてのパターンで警告され、自動的に0を埋め込むこともなく、不定値が返されます。C99とC++ではmainだけが特別扱いだということが分かります。

戻り型の指定

昔のC言語ではそもそも関数定義で戻り値の型指定が必要なく、値を返すかどうかは任意だったこととも関係があるようです。

最近、別件でLions本の勉強会に参加していますが、確かに当時のpre K&Rでは戻り値の型指定はありません。

/*
 * Test if the current user is the
 * super user.
 */
suser()
{

    if(u.u_uid == 0)
        return(1);
    u.u_error = EPERM;
    return(0);
}

一見、最近の型推論のある言語みたいですが、型チェックそのものがないだけです。

追記

この記事を公開した後、別のご指摘を受けました。pre K&Rまで遡らなくても、今でも戻り型を省略するとint扱いになるとのことです。

3. voidなエントリーポイント

実はこの辺の面倒を避けて、CRTなしのエントリーポイントをvoidにしていました。

void _main() {}

ですがこれ、実は何の解決にもなっていません。出力されるアセンブリコードは以下と完全に同一です。

int _main() {}

つまり実質的に不定値が返されます。通常はvoidから戻ったときのEAXは単純に無視されるため、これが問題になることはありません。

なぜ今回は問題になったかというと、WindowsではエントリーポイントからretしたときのEAXの値がプロセスの戻り値として処理されるためです。

このことを確認してみます。以下のコードをa.sとして保存します。

.intel_syntax
mov eax, 123
ret

アセンブルしてEXEを出力して、戻り値を確認します。

$ gcc -nostdlib a.s
$ ./a.exe
$ echo $?
123

このためエントリーポイントは必ずintにして、明示的に値を返すのが無難だという結論になりました。

実は勉強会で $? をど忘れしていたため、現地で直接確認することができませんでした。これも反省点の一つです。

4. voidな引数

しかし問題は戻り値だけではありません。引数にも突っ込みが来ました。C言語では()と(void)は区別されます。

void test1() {}
void test2() { test1(1); }

このコードは、C言語ではどの規格を指定しても警告なくコンパイルが通ります。C++ではエラーになります。これはC言語では()は可変長扱い【訂正】引数の型指定なしと扱われるため何を渡しても素通りするのに対して、C++では(void)扱いされるため引数を入れるとエラーになるためです。

そのためC言語では引数の型チェック漏れを防ぐため、明示的に(void)と書くことは必須です。恥ずかしながらこのことはまったく知りませんでした。C++ではエラーになることは知っていたので、当然C言語でも同じだろうと勘違いしていました。

追記

この記事を公開した後、ご指摘を受けました。当初は可変長扱いと記述していたのを修正しました。

最後に

アセンブリを見るのが主眼だったので、正直、言語仕様のことは深く考えないで、コンパイルが通ればOKくらいに軽く考えていました。しかし単純なサンプルだからこそ逆に目立つため、最低限、突っ込みを受けない程度の配慮はしておくべきだと思いました。