どうもです、タドスケです。
先日、仕事で C++ のコードを書いていてハマったポイントがあったので共有します。
起きた問題
現在僕がいるプロジェクトでは、C++ のプロジェクトをビルドする際にデフォルトで最適化を有効にしています。
(仮にこれを「Release」ビルドと呼ぶことにします)
その環境で実装し、動作確認も行ったコードである日、
最適化を無効にしているビルド(Debug ビルドと呼ぶことにします)で、動作がおかしくなります。
という報告がありました。
原因を順にたどっていったところ、あるクラスが持っている構造体メンバーの初期値が、想定と違ったものになっていることがわかりました。
Release ビルド時と Debug ビルド時でコードに違いは一切ありません。
(Debug 限定の #define で囲んでいたりはしない)
散々調べた結果、不具合が起きている cpp とは別の cpp ファイル内で、同じ名前の構造体を定義しているのが原因でした。
再現サンプル
この問題を再現できる(かもしれない)サンプルは以下です。
#include "ClassA.h"
#include "ClassB.h"
int main()
{
ns::A a;
ns::B b;
/*
実行すると、
A = A
B = A
と表示されることがある。
*/
}
#include <iostream>
#include "ClassA.h"
namespace ns {
struct S {
char name = 'A';
};
A::A() {
S s;
std::cout << "A = " << s.name << "\n";
}
} // namespace ns
#include <iostream>
#include "ClassB.h"
namespace ns {
struct S {
char name = 'B';
};
B::B() {
S s;
std::cout << "B = " << s.name << "\n";
}
} // namespace ns
ClassA と ClassB 内で、同じ名前の構造体 S を定義しています。
.h ファイルではなく .cpp ファイル内で定義しているので、コンパイルエラー・リンクエラーは出ません。
しかし実行してみると、
A = A
B = B
と表示されてほしいところが
A = A
B = A
と表示されることがあります。
この「ことがある」というのが厄介で、最適化などのビルド・リンク設定によって、起きたり起きなかったりするのです。
.cpp 内での定義なので、僕としては「他の .cpp は影響しない」という思い込みがあり、チームメンバーから指摘いただくまで気付きませんでした…
原因は?
コンパイラの問題に思えますが、その辺りは詳しくないので、ChatGPT に聞いてみたところ、以下の回答をもらいました。
プログラムの内容を確認したところ、以下のような構造になっています。
- ClassA.h と ClassA.cpp:
A
クラスが定義されており、そのコンストラクタでは"A = A"
と出力するように設計されています。- コンストラクタ内で
S
構造体のインスタンスを作成し、そのname
メンバを出力しています。S
構造体のname
は"A"
に初期化されています。
- ClassB.h と ClassB.cpp:
B
クラスが定義されており、そのコンストラクタでは"B = B"
と出力するように設計されています。- こちらも
S
構造体のインスタンスを作成し、そのname
メンバを出力しています。この場合、S
構造体のname
は"B"
に初期化されています。
- defferent_by_optimize.cpp:
main
関数ではA
クラスとB
クラスのインスタンスを作成しています。
このプログラムを実行すると、A
と B
の両方のインスタンスが作成され、それぞれのコンストラクタが呼ばれます。ただし、問題は S
構造体の扱いにあります。S
構造体は ClassA.cpp
と ClassB.cpp
の両方で定義されており、名前が同じでも異なる内容(name
メンバが "A"
と "B"
)を持っています。
このような場合、コンパイラの最適化やリンクの仕方によっては、最初に読み込まれた S
構造体の定義が使用される可能性があります。つまり、どちらの S
構造体が最初に読み込まれるかによって、出力が異なる場合があります。
この問題を解決するためには、以下のような方法が考えられます:
S
構造体をA
とB
のクラス内に移動するか、異なる名前をつける。- 同じスコープ内で同名の構造体を定義しない。
これらの変更によって、A
と B
のそれぞれのインスタンスが正しい S
構造体の定義を使用し、期待通りの出力が得られるようになるはずです。
ChatGPT の言うことなので鵜呑みにはできませんが、筋は通っていそうです。
対策は?
二つの構造体が同じ namespace(名前空間) 以下にあることが問題なのですが、むやみに名前空間を増やすのは良くないと思ったので、それぞれの構造体を無名名前空間に入れることで解消しました。
namespace ns {
namespace {
struct S {
char name = 'B';
};
} // namespace
教訓
.cpp 内でしか使わない定義は無名名前空間に入れる。
コメント