C/C++の関数プロトタイプ、暗黙ルールデータベースの話(その2)
前回の記事で公開したMakefileでは、gccのコンパイルオプションに-Wallを付けていなかった.
極めて非人道的である.
現在はMakefileを修正してコンパイル時に-Wallを付けるようにしたが、そのままmake
を叩くとコンパイラにしっかり警告を食らった.
run.c: In function ‘main’: run.c:4:2: warning: implicit declaration of function ‘print_hello’ [-Wimplicit-function-declaration]
これは、前回のコードではmain関数でprint_hello()を呼び出しているが、それに先立って行うべき関数のプロトタイプ宣言を行っていないからである.
しかし、gccでは警告を食らうだけで、コンパイルは通ってしまう.
関数プロトタイプ(Function prototype)
Cで、もといgccで、関数プロトタイプがなくてもコンパイルが通るのは以下の理由による.
関数が事前に宣言されていない状況で、左括弧付きで式の中に現われた場合、その関数は暗黙のうちに int を返すものと判断され、引数については何の想定もなされない。
Wikipediaにも書いてある通り、書かない場合に問題が発生するので、基本的に関数プロトタイプを省略してはならない.
C++ではコンパイルが通らない
C++では、もといg++では、そもそもコンパイルが通らない.
前回のMakefile中の変数CC
をg++にしてmake
を叩くと警告が出るだけでコンパイルが通ってしまうgccとは違って、コンパイラがエラーを吐く.
run.c: In function 'int main(int, char**)': run.c:4:14: error: 'print_hello' was not declared in this scope make: *** [run] Error 1
どうもg++では、関数プロトタイプが必須らしい.
g++でコンパイルが通るようにする
以下は、前回のコードをC++(g++)に合わせて若干変更したもの.
makeでライブラリ(.a)のビルドを管理するサンプル(g++.ver)
前回と違って.cファイルを.ccファイルとした上で、hello.hに関数プロトタイプを追加し、run.ccでhello.hを#includeするようにしている.
暗黙ルールデータベースの活用
上記のC++バージョンのコードは、Makefileも前回のCのMakefileと少し違う.
ここではそれぞれのMakefileの記述が暗黙ルールデータベースにどのように作用しているかを比較してみることで、もう少し暗黙ルールデータベースに対する理解を深める.
CのMakefile
CのMakefileでは変数CCにgccをセットして、変数CFLAGSに-Wallをセットしている.
CC := gcc RM := rm -f CFLAGS := -Wall
実行結果は以下の通り.
$ make gcc -Wall -c -o hello.o hello.c ar rv libhello.a hello.o ar: creating libhello.a a - hello.o gcc -Wall run.c libhello.a -o run
C++のMakefile
一方、C++のMakefileでは変数CXXFLAGSに-Wallをセットしている.
RM := rm -f CXXFLAGS := -Wall
実行結果は以下の通り.
$ make g++ -Wall -c -o hello.o hello.cc ar rv libhello.a hello.o ar: creating libhello.a a - hello.o g++ -Wall run.cc libhello.a -o run
Makefile上でg++という記述がないにも関わらずコンパイラがg++に切り替わっているのは、暗黙ルールデータベースのルールが切り替わっているからである.
ソースファイルの名前(拡張子)で適用するルールを変更する
make -p
を叩いて表示される暗黙ルールデータベースは、暗黙に定義された型ルールの塊である.
型ルールを書けば暗黙のルールをあなた自身で定義する事ができます。型ルールはターゲット(のうち、正確には一つ)が
%
という文字を含んでいるという事意外は普通のルールと同じです。この文字を含んだターゲットはファイル名を一致させるための型として認識され、%
の部分は空っぽでなければどんな部分文字列にも一致し、かつ一方でその他の文字は全く一致していなければなりません。同様に%
を使った依存関係ではターゲット名にどういう名前が関連しているか、という事を示します。だから
%.o : %.c
という型ルールは語幹 .c
という全てのファイルから語幹 .o
という別のファイルを作成する方法を命令します。
ちなみに、makeに関する解説記事で良くあるサフィックスルール(.o:.c
みたいなの)は古いタイプの書式なので、型ルール(%.o:%.c
)を使うべき.
makeで暗黙のルールを定義する古い手法としてサフィックスルールというものがありますが、型ルールのほうが一般的で明瞭なので、サフィックスルールは時代遅れとなりました。
CとC++で設定する変数が異なるのは、適用されるルールが異なる、というのを実際にルールを追って確認してみる.
以下で示すルールは全てターミナル上でmake -p
を叩けば確認できる.
Cの暗黙ルールを追ってみる
リンク時に適用されるルールが以下.
%: %.c # commands to execute (built-in): $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@
.cファイルから実行ファイル(同じ語幹でサフィックスがないファイル)を生成する際にこのルールが適用される*1.
各変数もそれぞれデフォルト値が暗黙的に定義されており、LINK.cは以下の通り.
# default LINK.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
また、オブジェクトファイル生成時(.cから.oファイルを生成する際)には以下のルールが適用される.
%.o: %.c # commands to execute (built-in): $(COMPILE.c) $(OUTPUT_OPTION) $<
COMPILE.cも見てみると以下のような感じ.
# default COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
LINK.cにもCOMPILE.cにも変数CCやCFLAGSが含まれていることが確認できる.
Makefile上で変数CFLAGSに-Wallをセットするだけで、コンパイル時にもリンク時にも-Wallオプションが付加されるのは、この暗黙ルールによることがわかる.
手元のmakeでは、変数CCはデフォルトでccが設定されていた.
ここではgccを使いたいので明示的にgccをセットしている.
C++の暗黙ルールを追ってみる
C++のMakefileで扱うソースファイルのサフィックスを.cから.ccに変更したため、適用される型ルールも変化する.
リンク時に適用されるルールは以下の通り.
%: %.cc # commands to execute (built-in): $(LINK.cc) $^ $(LOADLIBES) $(LDLIBS) -o $@
LINK.ccの定義は以下.
# default LINK.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
コンパイル時は以下.
%.o: %.cc # commands to execute (built-in): $(COMPILE.cc) $(OUTPUT_OPTION) $<
COMPILE.ccも見てみる.
# default COMPILE.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
上記の.ccファイルに対してコンパイル時やリンク時に適用されるルールを見てみると、デフォルトではCXXやCXXFLAGSが用いられていることが確認できる.
そのため、-WallオプションのセットはCXXFLAGSに対して行った.
また、CXXはデフォルトでg++なので明示的にg++をセットする必要がない.
g++じゃなくてclangを使いたければCXXにclangをセットするだけで良い.
ここでは暗黙ルールデータベースをなるべく活用していこうという方針なので、適用されるルールに応じて扱う変数も変わってくるわけである*2.
ちなみに、変数CXXFLAGS、CPPFLAGS、TARGET_ARCHにデフォルト値はない.これらの変数についてはGNU make 第3版に以下のような解説がある.
これらの変数は構築処理をカスタマイズするために利用者が変更することを意図していて、それぞれC++コンパイラのフラグ、Cプリプロセッサのフラグそしてアーキテクチャ特有のコンパイルオプションを指定するために使います。
C++のソースファイル名は.cc?.C?.cpp?
余談だが、世のC++のソースファイルの拡張子は完全に統一されているわけではないようで、.ccだったり.cppだったりする.
Stack Overflowでも議論になっている.
上記のStack Overflowでの議論で付いているコメントで歴史的な背景が解説されていた.
それによると、UNIX上ででC++が開発された頃は.ccであり、.Cや.c++は早い段階で使われなくなり、.cppはDOSやWindowsのコンパイラ由来で出てきた拡張子らしい.
ただ、ポータビリティの観点から、主に使われているのは.cppとのこと*3.
実際、Githubで.ccと.cppで検索をかけると、拡張子に.cppを使っているプロダクトが圧倒的に多い*4.
何にせよ、ここで言いたいのはmakeはこれらの互換性を暗黙ルールデータベースで担保しているということである.
関連する暗黙ルールを以下に示す.
# default COMPILE.cpp = $(COMPILE.cc) # default LINK.cpp = $(LINK.cc) # default COMPILE.C = $(COMPILE.cc) # default LINK.C = $(LINK.cc)
対象のソースファイル名が.cppや.Cだとしても結局.ccのルールが適用されることがわかる.
暗黙ルールデータベースに依存すべきではないかもしれない
さらに余談.
ここまで暗黙ルール、暗黙ルールと言って来たけど、チームでMakefileを共有する場合にはあまり暗黙的なものに依存しない方が良いのかな、なんて書きながら思った.
暗黙ルールを積極的に用いるとMakefileの記述量は減るが、一方で何をやっているのかパッと見ではわからないようになってしまうわけで、そうなるくらいだったら明示的に型ルールを全てMakefile上に書いた方が親切という考え方もある気がする.
今まで他人とMakefileを共有したことがないのでわからないが、実際の現場ではどうなのだろうか.
馬鹿なこと言ってないでAutotools使えよって感じなんだろうか.
実際、OSSのプロダクトではAutotoolsを使ってることが多いっぽい.
インクルードガード
前回の記事のコードでは、-Wallオプションをつけていなかったが、インクルードガードもしていなかった.
ファイル分割をやり始めると、コンパイルが煩わしい問題に加えて、ヘッダを2重に読み込んでしまう問題が発生する.
これはC、C++両方で適用される常套手段と考えて良いと思う*5.
ちなみに「インクルードガード」でググると、ヘッダファイルの書き方みたいな記事もヒットする.
例えば、ヘッダに外部変数の宣言を含めることの弊害なんかは、ヘッダには外部変数のextern宣言のみ含めることによって回避される.
まぁヘッダに何を書くべきかとかはプロジェクト毎に決まってそうなものだけど、どう書くべきかわからなくなったら、この辺のキーワードでググってみると有益な情報が転がっている.
二十五日半狂乱、9日目(の分)の記事
*1:ライブラリを用いない場合、通常.oファイルから実行ファイルを生成すると思うので、その場合は%:%.oのルールを見る
*2:もちろんMakefile上で型ルールを自分で自由に定義しても良い.ただ、変数については、慣例に従うべきというか、他の人が見ても誤解が無きよう、と考えるとなるべくこの手の変数をカスタムすべきではないかと思う.
*3:Windowsではファイルタイプを識別する拡張子が重要な概念になっているが、Unix/Linuxにはそもそも拡張子という概念がない
*4:UNIX由来というだけでなんとなく.ccを使っていたけど、デファクトに従うべきなのだろうか...
*5:ないと問題になるけど、あっても問題になることはない、はず