ヘッダファイルでの定義と分割コンパイル

ヘッダファイルに関数などの(宣言ではなく)定義を記述している場合の,分割コンパイルを行ったときの挙動をイマイチ把握していなかったので整理してみました.

C/C++ では,多重定義エラーを防ぐために,ヘッダファイルを記述するときにはインクルードガードと呼ばれるテクニックを使用します.ですが,インクルードガードはコンパイル単位内でのみ有効なテクニックであるため,複数のソースファイルを分割コンパイルする場合には,以下のように多重定義エラーとなる問題が発生します.

hello.h
#ifndef HELLO_H
#define HELLO_H

#include <iostream>

extern void proxy();

void hello() {
    static int count = 0;
    std::cout << "Hello, world! (called: " << count << ")" << std::endl;
    ++count;
}

#endif // HELLO_H
proxy.cpp
#include "hello.h"

void proxy() {
    hello();
}
test.cpp
#include "hello.h"

int main(int argc, char* argv[]) {
    hello();
    proxy();
    
    return 0;
}
コンパイル結果
$ g++ -Wall proxy.cpp test.cpp 
/cygdrive/c/DOCUME~1/clown/LOCALS~1/Temp/ccG9dn8J.o:test.cpp:(.text+0x74):
multiple definition of `hello()'
/cygdrive/c/DOCUME~1/clown/LOCALS~1/Temp/ccyWu4GI.o:proxy.cpp:(.text+0x74):
first defined here
collect2: ld returned 1 exit status

static 変数

static 変数についても注意が必要です.私は,何となくこれまで同じ static 変数はプログラムで 1 つしか生成されないと思っていましたが,下記のコードを(分割)コンパイルして実行すると,どうやらコンパイル単位数分の static 変数が生成されるようです.

hello.h
#ifndef HELLO_H
#define HELLO_H

#include <iostream>

extern void hello();

class foo {
public:
    foo() { std::cout << "constructed." << std::endl; }
    ~foo() { std::cout << "destroyed." << std::endl; }
};

static foo foo_;

#endif // HELLO_H
hello.cpp
#include "hello.h"
#include <iostream>

void hello() {
    std::cout << "Hello, world!" << std::endl;
}
test.cpp
#include "hello.h"

int main(int argc, char* argv[]) {
    hello();
    return 0;
}
実行結果
$ g++ -Wall hello.cpp test.cpp 

$ ./a
constructed.
constructed.
Hello, world!
destroyed.
destroyed.

この結果は意識しておかないと,いつかこれが原因となるバグを混在させてしまうかもなぁと思いました.

inline 関数

inline 関数については,特に注意を払わなくても意図した結果を得られるようです.最初,inline 関数の中でstatic 変数を定義した場合にどうなるのだろうと疑問に思いましたが,コンパイル & リンク時に以下のような処理が施されるようです.

inline指定子を付けた場合でも、関数はデフォルトでは外部結合になります。そして、インライン置換されなかったインライン関数(例えば、再帰呼出しを行ったり、インライン関数へのポインタを取得した場合)や、インライン関数の中で定義された静的記憶域期間を持つオブジェクトは、リンク時に一箇所にまとめられます。

http://okwave.jp/qa3809238.html

テストに使用したコードは,最初のソースコードの hello.h 中で定義した hello() 関数を inline 化しただけです.実行結果は,以下の通り.

実行結果
$ g++ -Wall proxy.cpp test.cpp 

$ ./a
Hello, world! (called: 0)
Hello, world! (called: 1)
Reference