C++と比べて、Cの関数まわりの規定は、K&R時代の記法への後方互換性を保証するため、カオスになっています。「関数原型を含む/含まない関数宣言子」、「関数原型を含む/含まない関数定義」、「関数定義の一部でない関数宣言子」…。最初、何を言っているのかさっぱり分かりませんでした。
僕も、C90規格票の「6.5.4.3 関数宣言子」「6.7.1 関数定義」「6.3.2.2 関数呼出し」について精読していましたが、言葉の意味が分からないと、はっきり言って読めたものではないですね。Cリファレンスマニュアルの9章や、JPCERT/CCの関連規約(参考文献・サイト)に一通り目を通すことで、おぼろげに全体像がつかめてきました。
一言で「関数定義」「関数呼出し」と言っても、関数定義の一部でない関数宣言子と、関数定義の一部である関数宣言子の書き方の組合せによって、以下の6パターンに分かれます。
- 【パターン1】関数呼出しから見て、関数宣言が可視でなく、かつ関数定義が関数原型形式である場合。
- 【パターン2】関数呼出しから見て、関数宣言が可視でなく、かつ関数定義が非原型形式である場合。
- 【パターン3】非原型形式の関数宣言が可視であり、関数定義が関数原型形式をもつ場合。
- 【パターン4】非原型形式の関数宣言が可視であり、関数定義が非原型形式をもつ場合。
- 【パターン5】原型形式の関数宣言が可視であり、関数定義が原型形式をもつ場合。
- 【パターン6】原型形式の関数宣言が可視であり、関数定義が非原型形式をもつ場合。
これだけ見ても、何のことやらさっぱり分からないと思うので、手始めに「関数定義」「関数呼出し」の説明を読む上で最低限必要となる用語についてまとめます。
上の6パターンについては、本記事の最後で総まとめとして、サンプルコードを記載し、各々を脳内コンパイルしてみます。
#基本的な用語の解説
##関数原型形式の関数定義
もっともよく見かける、オーソドックスな形式の関数定義です。「関数原型を含む関数定義」と言う場合、こちらの関数定義を指します。
extern int max(int a, int b)
{
return a > b ? a : b;
}
###用語解説
-
extern
の部分が記憶域クラス指定子(関数に指定可能な記憶域クラス指定子は、extern
かstatic
のいずれか。省略時の既定はextern
)。 -
int
の部分が型指定子(省略時の既定は型int
)。 -
max(int a, int b)
の部分が関数原型宣言子。 -
int a, int b
の部分が仮引数型並び。 -
{}
で囲まれたブロックが関数本体。
###関数呼出しに関する規定
- 実引数の型は、対応する仮引数の型への代入変換と同様の型変換が行われます。
- 省略記号(
, ...
)の位置にあたる実引数の型は、既定の実引数拡張(汎整数拡張および、型float
の型double
への拡張)が行われます。
##非原型形式の関数定義
C99では「将来廃止予定」とうたわれていますが、C90で許容されるK&R形式の関数宣言子もあります。このような形式をもつ関数定義を「関数原型を含まない関数定義」と言います。書籍によっては「伝統的形式」「K&R形式」と呼んだりもします。
extern int max(a, b)
int a;
int b;
{
return a > b ? a : b;
}
###用語解説
-
extern
の部分が記憶域クラス指定子(関数に指定可能な記憶域クラス指定子は、extern
かstatic
のいずれか。省略時の既定はextern
)。 -
int
の部分が型指定子(省略時の既定は型int
)。 -
max(a, b)
の部分が関数宣言子。 -
a, b
の部分が識別子並び。 -
int a; int b;
の部分が宣言並び(宣言の無い仮引数の規定の型はint
)。 -
{}
で囲まれたブロックが関数本体。
###関数呼出しに関する規定
非原型形式の関数宣言子には、仮引数の型に関する情報がないため、実引数の型は(代入変換ではなく)既定の実引数拡張が行われます。
実引数と仮引数の型適合性が以下の場合、未定義の動作となります。
-
関数定義が関数原型形式の場合
-
拡張後の実引数の型と、仮引数の型が適合しない場合。
-
関数原型が省略記号で終わっている場合。
-
関数定義が非原型形式の場合
-
拡張後の実引数の型と、拡張後の仮引数の型が適合しない場合。
また、関数定義が関数原型形式/非原型形式にかかわらず、関数原型をもたない関数の呼出しにおいて、実引数と仮引数の個数が一致しない場合、未定義の動作となります(gcc4.9.2では、-std=c90 -pedantic
を指定しても警告さえ出ませんが)。仮引数の型適合性の検査は、関数定義が関数原型形式/非原型形式かによって条件が異なります(上の通り)。
###K&R形式(非原型形式)の将来の行方
非原型形式の関数定義は、C99では将来的に廃止予定とされています(以下「JISX3010:2003 (ISO/IEC 9899:1999)」「6.11 今後の言語の方針」より引用)。
6.11.6 関数宣言子 空の括弧を伴う関数宣言子(関数原型形式の仮引数型並びではない。)の使用は,廃止予定事項とする。
6.11.7 関数定義 仮引数の識別子並びと宣言並びを別々に与える関数定義(関数原型形式の仮引数の型及び識別子の宣言ではない。)の使用は,廃止予定事項とする。
しかし、C11の最終ドラフト(N1570)では(以下「ISO/IEC 9899:201x」より引用)、
6.11.6 Function declarators
1 The use of function declarators with empty parentheses (not prototype-format parameter
type declarators) is an obsolescent feature.6.11.7 Function definitions
1 The use of function definitions with separate parameter identifier and declaration lists (not prototype-format parameter type and identifier declarators) is an obsolescent feature.
また言うてますがな。trigraph sequenceとともに、お逝きなさい。
##「関数定義の一部でない関数宣言子」について
「関数定義の一部でない関数宣言子」という言葉が出てきますが、これは「関数本体をもたない関数宣言子」のことです。本記事では、この関数宣言子を指して「関数宣言」と呼ぶようにします。
なお、関数宣言で注意したいのは、非原型形式の関数宣言です。非原型形式の関数宣言は、つねにT f();
の形式(識別子並びが空の形式)となります。T f(x, y, z);
のように識別子並びを指定することはできません。
関数定義においては、もちろんT f(x, y, z);
の形式でもよいです(空も可能)。
##そもそも関数宣言が可視でない場合
関数呼出しから見て前方参照が可能な場合に「関数宣言が可視である」と言いますが、そもそも関数宣言が可視でない場合、どうなるのかという話ですが。
#include <stdio.h>
/* int max(int, int); */
int main(void)
{
max(1,0);
}
この場合、関数呼出しを含むもっともと内側のブロックに、extern int max();
という関数宣言が暗黙に置かれます。
#include <stdio.h>
int main(void)
{
extern int max(); /* <= 暗黙の関数宣言を可視化してみた */
max(1,0);
}
言い換えれば、呼び出される関数の関数定義の返却値型が型int
であれば、関数宣言が不可視であっても結果的に整合性がとれることになります。その代わり、実引数の型は不問となります(後述)。
#ここがややこしいのだ
関数定義の一部でない関数宣言子void f()
について考えて見ます。
()
の中が空ですが、これをvoid f(void)
と書くつもりであったのであれば、誤りです。これはセキュアコーディングのためのお作法としての推奨ではなく、「関数原型形式の関数宣言子の仮引数型並びは省略不可」という規定に違反するためです。(void)
の意味で()
と書いた「つもり」ならば、規定違反の書き方になります。
一方、関数定義の一部でない非原型形式の関数宣言子においては、()
内の識別子並びは省略可能です。
ということは、つまり。void f()
のように、()
内の仮引数型並びを空としたつもりでも、それは意図せずして「非原型形式の関数宣言子において識別子並びが空である」ものとして、手短に言えば「K&R形式で書いた」ものとして解釈されてしまうのです(ああ、ややこしい!)。
それだけではありません。空の()
も、コンテキストによって意味が変化します。そのコンテキストとは「関数定義の一部の関数宣言子」の場合と、「関数定義の一部でない関数宣言子」の場合です。
6.5.4.3 関数宣言子(原型を含む)
〔...〕
識別子並びは, 関数の仮引数の識別子だけを宣言する。関数定義の一部の関数宣言で並びが空の場合, 関数は仮引数をもたない。関数定義の一部でない関数宣言の並びが空の場合, 仮引数の個数及び型の情報をもたない(71)。
つまり、これ↓は合法で
void f(); /* 「仮引数の個数及び型の情報をもたない」という意味 */
int main(void)
{
f(42);
return 0;
}
void f(int n){}
これ↓は、実引数と仮引数の個数が不一致ということになります。
void f(){} /* 「関数fは仮引数をもたない」という意味 */
int main(void)
{
f(42); /* 関数定義に反して引数を指定 */
return 0;
}
#総まとめ(全パターンを網羅してみる)
自分の練習も兼ねて、本記事冒頭に挙げた6パターンについて、サンプルコードを脳内コンパイルしてみます。
参考までに、一部のパターンについて、サンプルコードをgcc 4.9.2
で実際にコンパイルした際に出力されるメッセージも記載しました。処理系の環境は以下となります。
$ gcc --version
gcc (Debian 4.9.2-10) 4.9.2
Copyright (C) 2014 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.
$
$ uname -a
Linux debian 3.16.0-4-686-pae #1 SMP Debian 3.16.7-ckt11-1+deb8u6 (2015-11-09) i686 GNU/Linux
$
$ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description: Debian GNU/Linux 8.2 (jessie)
Release: 8.2
Codename: jessie
##【パターン1】関数呼出しから見て、関数宣言が可視でなく、かつ関数定義が関数原型形式である場合。
int main(void)
{
float r = 1;
f(r);
return 0;
}
int f(float x){return 0;}
一見すると、実引数も仮引数も型float
なので、このコードには問題が無いように見えます。しかし、実引数の式評価時に「規定の実引数拡張」が発生するため、型のコンフリクトエラーが発生します。
- 呼び出される関数
f
から見て関数宣言が不可視のため、extern int f();
が暗黙的に宣言される。 - そのため、呼び出される関数
f
の記憶域クラス指定子はextern
扱い、返却値型はint
扱いとなる。関数定義の返却値型も型int
なので問題なし。 - 暗黙に宣言された関数宣言は非原型形式である。そのため、実引数の式
r
の結果に「規定の実引数拡張」が適用され、実引数の結果の型は型double
に拡張される。 - しかし、関数定義は関数原型形式であるため、実引数と仮引数の型の適合性は型
double
(前者)と型float
(後者)で型適合性がチェックされる。結果、コンフリクトを生じる。
以上より、サンプルコードは未定義の動作を含みます。
$ gcc -o sample -std=c90 -pedantic -Wall sample.c
sample.c: In function ‘main’:
sample.c:6:2: warning: implicit declaration of function ‘f’ [-Wimplicit-function-declaration]
f(r);
^
sample.c: At top level:
sample.c:10:5: error: conflicting types for ‘f’
int f(float x){return 0;}
^
sample.c:10:1: note: an argument type that has a default promotion can’t match an empty parameter name list declaration
int f(float x){return 0;}
^
sample.c:6:2: note: previous implicit declaration of ‘f’ was here
f(r);
^
注目すべきメッセージは以下(他はgcc
の老婆心メッセージです)。
$ gcc -o sample -std=c90 -pedantic -Wall sample.c
sample.c:10:5: error: conflicting types for ‘f’
int f(float x){return 0;}
^
##【パターン2】関数呼出しから見て、関数宣言が可視でなく、かつ関数定義が非原型形式である場合。
int main(void)
{
float r = 1;
f(r);
return 0;
}
int f(x)
float x;
{return 0;}
- こちらも関数宣言
extern int f();
が暗黙に宣言される。 - 実引数も仮引数も規定の実引数拡張が行われるため、両者の型は型
double
となり、適合する。問題なし。
コンパイルの結果も「暗黙の関数宣言が行われてるよ」という老婆心メッセージのみで、未定義の動作を示唆するようなメッセージは出力されませんでした。
$ gcc -o sample -std=c90 -pedantic -Wall sample.c
sample.c: In function ‘main’:
sample.c:6:2: warning: implicit declaration of function ‘f’ [-Wimplicit-function-declaration]
f(r);
^
##【パターン3】非原型形式の関数宣言が可視であり、関数定義が関数原型形式をもつ場合。
暗黙の関数宣言の有無を除けば、結果は【パターン1】と同様に、規定の実引数拡張が適用された結果、実引数と仮引数で型が適合しないこととなります。
int f();
int main(void)
{
float r = 1;
f(r);
return 0;
}
int f(float x)
{return 0;}
$ gcc -o sample -std=c90 -pedantic -Wall sample.c
sample.c:13:5: error: conflicting types for ‘f’
int f(float x)
^
sample.c:14:1: note: an argument type that has a default promotion can’t match an empty parameter name list declaration
{return 0;}
^
sample.c:3:5: note: previous declaration of ‘f’ was here
int f();
^
##【パターン4】非原型形式の関数宣言が可視であり、関数定義が非原型形式をもつ場合。
暗黙の関数宣言の有無を除けば、結果は【パターン2】と同様、実引数と仮引数の型は適合します(問題なし)。
int f();
int main(void)
{
float r = 1;
f(r);
return 0;
}
int f(x)
float x;
{return 0;}
##【パターン5】原型形式の関数宣言が可視であり、関数定義が原型形式をもつ場合。
関数宣言も関数定義も原型形式であるという、推奨されるパターンですね。
int f(float x);
int main(void)
{
float r = 1;
f(r);
return 0;
}
int f(float x)
{return 0;}
説明不要で問題なし。
##【パターン6】原型形式の関数宣言が可視であり、関数定義が非原型形式をもつ場合。
これは一瞬、迷うパターンですね。
int f(float);
int main(void)
{
float r = 1;
f(r);
return 0;
}
int f(x)
float x;
{return 0;}
関数呼出しから見た可視の関数宣言がint f(float);
、すなわち関数原型形式であるため、非原型形式とは違って規定の実引数拡張は発生しないはず。と思いきや、別のところで警告が発生しました。
$ gcc -o sample -std=c90 -pedantic -Wall sample.c
sample.c: In function ‘f’:
sample.c:14:7: warning: promoted argument ‘x’ doesn’t match prototype [-Wpedantic]
float x;
^
sample.c:3:5: warning: prototype declaration [-Wpedantic]
int f(float);
^
関数定義の仮引数の宣言float x
について、規定の実引数拡張を適用した結果の型(型double
ですね)が、関数宣言int f(float);
で指定された型float
とアンマッチだ、と言っているようです。
手短に言うと、関数定義と関数宣言とで仮引数の型が適合しないと怒られているようです。
ここで、関数型の適合条件を確認してみます。
6.5.4.3 関数宣言子(原型を含む)
〔...〕
二つの関数型が適合するためには, 次の条件をすべて満たさなければならない。
両方が適合する返却値型をもつ(72)。
両方が 仮引数型並びをもつ場合, 仮引数の個数及び省略記号の有無に関して一致し, 対応する仮引数の型が適合する。
一方の型が仮引数型並びをもち, 他方の型が関数定義の一部でない関数宣言子によって指定され, 識別子並びが空の場合, 仮引数型並びは省略記号以外で終わる。各仮引数の型は, 規定の実引数拡張の適用の結果の型と適合する。
一方の型が仮引数型並びをもち、他方の型が関数定義によって指定され, 識別子並び(空でもよい)をもつ場合, 両方の仮引数の個数は一致する。更に各原型仮引数の型は, 対応する識別子の型に規定の実引数拡張を適用した結果の型と適合する。
パターン6の場合、4つ目の条件に違反します。すなわち、関数定義の一部である関数宣言子(非原型形式)の仮引数float x;
に規定の実引数拡張を適用した結果の型double
が、関数原型形式の関数宣言int f(float);
の仮引数の型float
と適合しないということです。
以下のように、int f(float);
をint f(double);
に修正すると、警告メッセージは出力されなくなりました。
/* int f(float); */
int f(double);
int main(void)
{
float r = 1;
f(r);
return 0;
}
int f(x)
float x;
{return 0;}
#まとめ
くぅ疲です。ここまで深追いしておいてなんですが、関数定義も関数宣言子も、関数原型形式で書きましょう。
#参考文献・サイト
- 「プログラム言語C JISX3010-1993 (ISO/IEC 9899:1990)」
- 「S・P・ハービソン3世とG・L・スティール・ジュニアのCリファレンスマニュアル 第5版」(玉井浩 訳)
- 「DCL07-C. 関数宣言子には適切な型情報を含める」(CERT C コーディングスタンダード)
- 「DCL11-C. 可変引数関数に関連する型問題について理解する」(CERT C コーディングスタンダード)
- 「DCL20-C. 引数を受け付けない関数の場合も必ず void を指定する」(CERT C コーディングスタンダード)
- 「DCL40-C. 同一の関数やオブジェクトに対して適合(compatible)しない宣言をしない(CERT C コーディングスタンダード)
- 「EXP37-C. 正しい引数の数と型で関数を呼び出す」(CERT C コーディングスタンダード)