#分割コンパイル
巨大なプログラムになるとたいていは複数のファイルにプログラムを分けて記載します。その場合はどのような形で関数の情報が渡ってるのでしょうか?
本文はLinuxとgccの環境を想定しています。他のOSでは実装方法が違うかもしれません。(実行例はCentOS7.4/64bit)
まず、最終的にはC++になりますが、事前準備としてCの話をします。
##C言語の例
Cの簡単な分割コンパイルの例を挙げておきますと、
#include <stdio.h>
extern int square(int);
int main() {
int i = 10;
printf("square(%d)=%d\n", i, square(i));
return 0;
}
int square(int i) {
return i*i;
}
上記のような二つのファイルからなるプログラムがあった場合、コンパイル時は以下のようにするかと思います。
$ gcc ex1-main.c ex1-func
$ ./a.out
square(10)=100
このgccコマンドは内部で次のような処理をします。
初めにex1-main.cをコンパイルし「-c」オプションでオブジェクトファイル ex1-main.oを生成
$ gcc -c ex1-main.c
オブジェクトファイルにどのような関数が含まれるかはnmコマンドで表示できます。(nm: オブジェクトファイルのシンボルをリストする)
mainはファイル中に定義されている関数。printfとsquareはファイル中に定義されてない関数です。
$ nm ex1-main.o
0000000000000000 T main
U printf
U square
次にex1-func.oを生成
$ gcc -c ex1-func.c
他の関数を使ってないのでシンボルリストはsquareのみです。
$ nm ex1-func.o
0000000000000000 T square
nmで表示されたシンボルリストの情報をもとに、gccがCの標準ライブラリ関数のprintfなどをリンクして実行ファイルのa.outを生成します
$ gcc -o a.out ex1-main.o ex1-func.o
$ ./a.out
square(10)=100
蛇足:gccが内部的に指定したライブラリの一覧はlddコマンドで確認できます。libc.so.6にC言語の標準ライブラリ関数が入っています。
$ ldd a.out
linux-vdso.so.1 => (0x00007ffc52db1000)
libc.so.6 => /lib64/libc.so.6 (0x00007fb8d485c000)
/lib64/ld-linux-x86-64.so.2 (0x0000560874958000)
蛇足終わり。
本題に戻りまして、C言語の場合は関数の型の情報がシンボルリストに含まれません。
なので次のような型情報が間違った分割コンパイルでもgccが実行ファイルを生成するときにはエラーになりません。
double square(double d) {
return d*d;
}
ex1-mainではsquareはint型、ex2-funcではsquareはdouble型。
$ gcc ex1-main.c ex2-func.c
実行結果は壊れています。
$ ./a.out
square(10)=0
C言語ではこのような事が起きないようにヘッダファイルを定義して、分割コンパイルしたそれぞれのファイルに正しい型情報が伝わるようにします。
##C++の例
前述のようにC言語では分割コンパイル時に型情報は伝わりません。
あれ、でも不思議に思いませんか? C++ではこんな事ができますよね。同じ関数名なのに引数が違う関数、関数のオーバーロードです。
#include <stdio.h>
extern int square(int);
extern double square(double);
int main() {
int i = 10;
double d = 20;
printf("square(%d)=%d\n", i, square(i));
printf("square(%lf)=%lf\n", d, square(d));
return 0;
}
int square(int i) {
return i*i;
}
double square(double d) {
return d*d;
}
同じ名前の関数であっても間違わずに呼び出されています。
$ g++ ex3-func.cc ex3-main.cc
$ ./a.out
square(10)=100
square(20.000000)=400.000000
これどうやってint型のsquareとdouble型のsquareを区別しているんでしょう? シンボルリストを見てみましょう。
$ g++ -c ex3-func.cc
シンボルリストがC言語ではsquareだったのがC++では「_Z6squared」と「_Z6squarei」になっています。C++では引数の情報をもとに内部的なシンボルリストを変更しています。関数の区別ができるのはこれです。
$ nm ex3-func.o
0000000000000010 T _Z6squared
0000000000000000 T _Z6squarei
上記のシンボルリストですがc++filtコマンドで引数の復元ができます。
$ nm ex3-func.o | c++filt
0000000000000010 T square(double)
0000000000000000 T square(int)
##C++からC言語の関数の呼び出し
上記のような方法で関数を区別していたわけなんですが、C++からC言語の関数を呼びたい事は多々あると思います。先ほどもC言語のprintfを呼び出しています。どのような仕組みなのでしょうか?
例えばC言語のsin(double)を呼び出したいとします。
#include <stdio.h>
#include <math.h>
int main(){
double d = 0.33;
printf("sin(%lf)=%lf\n", d, sin(d));
return 0;
}
オブジェクトファイルを作成してシンボルリストを確認してみてください。
$ g++ ex4-main.cc -c
$ nm ex4-main.o
0000000000000000 T main
U printf
U sin
あれれ?「sin」のままです。関数名を特殊加工して「_Z3sind」とかになっていません。
これなんですが、math.hに加工がしてあります。実際のファイルを挙げるのは長いのでやめておきますが、次のような行がヘッダファイルに挿入されています。
extern "C" { 関数定義 }
これを挿入するとC言語と同じシンボルリストが生成されます。
サンプルを挙げておきます。
#include <stdio.h>
extern "C"
{
int square(int i);
}
int main()
{
int i = 10;
printf("square(%d)=%d\n", i, square(i));
return 0;
}
extern "C"
{
int square(int i);
}
int square(int i) {
return i*i;
}
シンボルリストが加工されていません。
$ g++ ex5-func.cc -c
$ nm ex5-func.o
0000000000000000 T square
##蛇足
再掲しますが、C++の分割コンパイルの出力例です。以下の出力、何か情報抜けてのるに気がつきませんでしたか?
$ g++ -c ex3-func.cc
$ nm ex3-func.o | c++filt
0000000000000010 T square(double)
0000000000000000 T square(int)
返り値の型情報は? (関数オーバーロードのとある制限はこれに起因します。)
##追記:蛇足の補足
C++の関数オーバーロードではシンボルリストに返り値の型情報をもってないと述べたが、コメントで、関数テンプレートや型変換演算子では返り値の情報を持つことを教えていただきました。ありがとうございます。
試しに以下のコードで確認しました。
(すいませんがtemplateはあまり使った事が無いため、ネットで検索して出てきたコードを参考に作りました。以下のコードがg++以外の処理系で動作するかは不明です。)
以下のサンプルでは、返り値の型がtemplateになっています.引数はintに固定されています。
template <class T> T square(int i);
template<> int square(int i) {
return i*i;
}
template<> double square(int i) {
double d = i;
return d*d;
}
#include <stdio.h>
template <class T> T square(int i) {
T x = i;
return -x*x; // 呼び出し元の確認のため,負数にする.
}
//テンプレートの特殊化.
template<> int square(int i);
template<> double square(int i);
int main()
{
int i = 10;
printf("square(%d) = %d\n", i, square<int>(i));
i = 20;
printf("square(%d) = %lf\n", i, square<double>(i));
return 0;
}
シンボルリストの確認。返り値の型情報を保持している事が確認できました。
$ g++ -c ex6-func.cc
$ nm ex6-func.o | c++filt
0000000000000010 T double square<double>(int)
0000000000000000 T int square<int>(int)
$ g++ -c ex6-main.cc
$ nm ex6-main.o | c++filt
U double square<double>(int)
U int square<int>(int)
0000000000000000 T main
U printf
$ g++ -o a.out ex6-main.o ex6-func.o
$ ./a.out
square(10) = 100
square(20) = 400.000000
以上です。