0x19f (Shinya Kato) の日報

主にプログラミング関連の話をします

【C言語】引数なしの関数には void を書いた方がよいという話

C言語で引数なしの関数を書くときに void を書かないのと書くのとで挙動が違うなんて話を聞いたことはないでしょうか? つまり void func() {}void func(void) {} で挙動が違うという話ですね。 自分も話だけ聞いたことがあったものの2つがどう違うのかはわかっていなかったため、C言語の規格を読みながら何が違うのかを調べてみました。 結果だけ述べると、この2つの書き方は同じように見えて実は明確な違いがあり、引数がない関数を定義/宣言する場合には後者を使うのが適切です。 とは言え、2つの書き方で違いがあるとかほんとかよ?と思う方もいると思うので、まずはこの二つがどう違うのか見ていきましょう。

2つの関数の書き方の違い

早速ですが、以下のプログラムを見てみましょう。

// func_empty.c

void func() {}

int main(void) {
  func(1, 2, 3);
  return 0;
}

このプログラムでは void func() {} と定義した関数に対して main 関数から3つの引数を渡して呼び出しを行なっています。 おそらく多くの方はこのプログラムを見てコンパイルの結果はエラーになると予想するでしょう。 ところが、実際にコンパイルしてみると驚くべき結果が得られます。

$ gcc -std=c11 -Wall --pedantic-errors func_empty.c

なんと、エラーにならずコンパイルできてしまいました。 C言語の規格(C11)に沿った挙動にするためのオプションも指定しているので gcc拡張機能がこの結果を引き起こしている訳ではなさそうです。 不思議な挙動ですね......

一方で、C言語では引数の括弧の中に void を書くことで明示的に引数を受け付けないことを示すこともできます。 int main(void) { … } の void はまさにこの void です。

先ほどの func_empty.c の func の定義に void を足してみましょう。

// func_void.c

void func(void) {}

int main(void) {
  func(1, 2, 3);
  return 0;
}

これを先ほどと同様にコンパイルしてみます。

$ gcc -std=c11 -Wall --pedantic-errors func_void.c
func_void.c: In function 'main':
func_void.c:4:3: error: too many arguments to function 'func'
   func(1, 2, 3);
   ^~~~
func_void.c:1:6: note: declared here
 void func(void) {}
      ^~~~

こちらは期待していた通り「引数が多すぎる」というエラーが出ます。 謎ですねぇ......

関数引数の括弧の中に void を書かないのと書いておくのではこのような挙動の違い、すなわち関数の呼び出しの時に引数がチェックされるか否かという違いがでます。 前者では呼び出し時に引数の数がチェックされません。 そのため func_empty.c では func にいくら引数が渡されていてもエラーになりません。 一方で、後者では呼び出し時に引数のチェックが行われます。 その結果、上に示したようなエラーが出てコンパイルに失敗します。

関数の引数がないときは void を書いておく方が安全

上記のような違いが出るため、関数の引数がないときは void を書いておくのがベターです。

例えば、セキュアコーディングのスタンダードの中でも void を書くことが推奨されているようです。

DCL20-C. 引数を受け付けない関数の場合も必ず void を指定する

ちなみに『プログラミング言語C』(いわゆるK&R)の第二版の中では以下のように書かれています。

古いCプログラムとの互換性のために互換性のために、標準規格では、空にリストは旧式の宣言として扱い、引数リストのチェックは一切やらないことになっている。 リストが明らかに空であるときには、予約語 void を使うべきである。 (p.41)

また、以下のようにも書かれています。

double atof(); のように、関数の宣言が引数を含まないときには、この atof の引数については何も仮定しないものと受け取られ、すべてのパラメータ・チェック機能はオフにされる。 空の引数リストが許される特別な理由は、古いCプログラムを新しいコンパイラコンパイルできるようにすることにある。 しかし、新しいプログラムでこれを使うのはよくない。 関数に引数があるのなら、それは宣言すべきである。 もしなければ、void を使うのがよい。 (p.89)

というわけなので void を書きましょう。

なぜこんな規格になっているのか

そもそもなぜ void を書くのと書かないのとでこんな挙動の差が出るように規格が定められているんでしょうか? 調べてみたところ、どうやらこの挙動は ANSIC言語の規格が定められる以前の K&R 時代の関数定義/宣言との互換を保つためのもののようです。

実はC言語には標準化以前の頃からの伝統的な関数定義/宣言の方法が今でも残されています。 K&R スタイルの関数定義/宣言なんて呼ばれていたりするようです。 このスタイルでは以下のようにして関数を定義します。

int max(a, b)
int a, b;
{
  return a > b ? a : b;
}

このように、関数名の後ろの括弧の中には仮引数の識別子だけを書いておき、関数本体の前に仮引数の型を宣言するという構文になっています。 自分は以前 UNIX V6 のソースコードを眺めていた時にこの書き方を見たことがあって「昔のC言語にはこんな書き方があったのか〜」なんて思っていたので、今でもこのスタイルの書き方が規格に定められていると知ってかなり驚きました。

さらに驚くべきことに、K&R スタイルでは呼び出し側からは引数の情報が一切わかりません。 上記の max 関数を現在のスタイルで書くと int max(int a, int b) { ... } となるのですが、この場合の max関数 は「int 型の引数を2つ受け取って、int型を返す関数」という具合に型が定められます。 一方で、K&R スタイルで書かれた場合の関数 max は「引数については情報がなく、int型を返す関数」という風に型が定められます。 そして、後者ように定義された関数を呼び出す時、コンパイラは引数の数のチェックを行いません。 つまり、K&R スタイルで定義された max を以下のように呼び出してもエラーにはならないわけです。

int max(a, b)
int a, b;
{
  return a > b ? a : b;
}

int main(void) {
  max(1, 2, 3);
  return 0;
}

そして、実は引数の括弧の中を空にした関数は K&R スタイルの関数定義とみなされます。 最初の void func() {} の例がエラーにならないのはこういう理由です。

ちなみに、K&R スタイルの関数宣言は関数名を書いてその後ろの空の括弧をおいておくという記法しか用意されていません。 細かい話ですが、関数の定義と宣言は明確に違っていて、定義は関数本体を伴うもの、宣言は伴わないものです。 すなわち、上記の max 関数の宣言は int max(); となります。 当然ながらこの書き方ではコンパイラから見て引数の情報は一切わかりません。

標準化以前はこのような関数定義/宣言しか存在しなかったのですが、標準化の際に現在も使われているような引数の型を明示する構文が導入されました。 これがいわゆるプロトタイプ(関数原型)と呼ばれるものです。 おなじみの int max(int a, int b) { ... }int max(int, int); という関数定義/宣言ですね。 これによって関数呼び出しの際の型のチェックがより強力にできるようになりました。 ただし、新しいスタイルは引数が空の時は K&R スタイルと同じ形になってしまいます。 そこで、K&R スタイルとの互換性を保つために、引数がないことを明示する機能として void を書くという記法が導入されました。 これが引数なしを明示する void が導入された歴史的な経緯のようです。

なお、この K&R スタイルの関数定義/宣言は C99 の規格で廃止予定とされていたらしいのですが、C11 の規格でもまだ廃止されずに残されています。 互換性って大変ですね......

まとめ

というわけで、引数の括弧が空の関数は古い形式の関数定義/宣言とみなされてしまい、呼び出し時の引数の数や型のチェックが行われません。 うっかり変な呼び出しが正常にコンパイルされないようにちゃんと void を書くのが安全という話でした。

なお、この記事は K&R の第二版と C11 の最終ドラフトを読み漁って書いたのですが、もしかしたら僕が勘違いしている箇所もあるかもしれないので、その辺りはご注意ください。

おまけ1

C11 の規格の 6.7.6.3.14 を読む限りだと、関数定義の時は関数宣言と挙動が違うという解釈もありえそうなのですが、今回は以下のツイートにあるような解釈をしてこの記事を書いています。 この辺りは規格の書き方が曖昧でどう解釈すればいいのか自分も迷っているのが正直なところです......

おまけ2

ちなみに、gcc だとコンパイル時に -Wstrict-prototypes という警告オプションをつけると関数定義/宣言ともに K&R スタイルで書かれている場合に警告を出してくれるようです。 -Wold-style-definition という警告オプションもあるんですが、こちらは関数定義に対してしか警告を出してくれないようですね。