2005年10月10日

リンクと同名のシンボル: C++の場合

前回の記事で紹介した動的リンクの動作は、 C++ では予期せぬメンバ関数が呼ばれるという問題を引き起こします。

 

消費税を計算する a.{h,cpp} というソースコードと、それを利用する main.cpp があるとします。

a.h

class Tax {
public:
    int tax(int price);
    Tax();
private:
    double consumption_tax_;
};

a.cpp

#include "a.h"
Tax::Tax() : consumption_tax_(1.05) {
}

int Tax::tax(int price) {
    return int(price * consumption_tax_);
}

main.cpp

#include <iostream>
#include "a.h"
int main() {
    Tax tax;
    int apple_price = 100;
    std::cout << "apple: " << tax.tax(apple_price) << std::endl;
    return 0;
}

これらのソースコードをコンパイル・リンクして実行すると apple: 105 というメッセージが表示されます。

% g++ -fPIC -shared -o a.so a.cpp
% g++ -fPIC -shared -o main.so main.cpp
% g++ -o main-shared ./a.so ./main.so
% ./main-shared
apple: 105

ここで、a.{h,cpp} とはまったく独立に開発された b.cpp が存在し、同名のクラス Tax を実装していたとします。b.cpp 内の Tax クラスの用途は a.cpp とはまったく異なります。

class Tax {
public:
    int deduct(int income);
    Tax();
private:
    double deduction_rate_;
};

Tax::Tax() : deduction_rate_(0.1) {
}

int Tax::deduct(int income) {
    return int(income * (1.0 - deduction_rate_));
}

そして、b.cpp が b.so としてコンパイルされ、 運悪く b.so, a.so, main.so の順序でリンクされると、厄介なことが起きます。

% g++ -o main-shared2 ./b.so ./a.so ./main.so
% ./main-shared2
apple: 10

今回の実行結果は apple: 10 です。消費税込みの価格を計算するつもりが、 90% ディスカウントになってしまいました。

これは、Tax オブジェクトのコンストラクタとして b.so 内の Tax::Tax() が呼ばれ、Tax オブジェクトのメンバ関数として a.so 内の Tax::tax() が呼ばれているためです。つまり、ここでは予期せぬコンストラクタが呼ばれてしまったことがバグの原因になっています。

同名クラスの衝突によるこのような問題は小さなプログラムではまず発生しませんが、プログラムが巨大になるにつれて発生しやすくなる思います。namespace を使うなどの方法で衝突をさける必要があるでしょう。

シンボルのリンケージを 1つのファイル内に閉じ込めるだけなら無名 namespace が有効です。たとえば b.cpp の全体を namespace { ... } で囲めば、上記の問題は発生しません。無名 namespace で囲った場合の b.so の Tax 関連のシンボルは次のようになります。

% nm --demangle b.so  |grep Tax
00000794 T (anonymous namespace)::Tax::deduct(int)
00000784 T (anonymous namespace)::Tax::Tax()
00000774 T (anonymous namespace)::Tax::Tax()

コンストラクタ Tax::Tax() が 2つあるのは、片方はオブジェクトの作成用、もう片方は継承されたとき用のようです。

まとめ

通常、リンカの動作など気にしなくてもプログラムを書けますが、上記のようなバグが発生した場合は、リンカの知識の有無によってバグ修正にかかる時間が大幅に変わってきそうです。weak シンボル編へと続く…

本記事で用いた環境は Debian GNU/Linux sarge + GCC 3.3.5 です。