staticメンバ変数の undefined reference の原因
クラス内でstaticメンバ変数を宣言しただけで、クラスの外側での実体定義を書き忘れたときにリンクの段階で出るエラーです。コンパイル自体は通りますが、実行ファイルを作る最後のリンク工程で失敗します。
エラーメッセージの読み方
undefined reference to `Counter::total'
undefined reference- エラー種別 — コンパイルではなくリンク段階で検出されました。行番号は出ません
Counter::total- シンボル名 — 宣言はあるが実体(定義)が見つからないもの
見つけ方- staticメンバ変数はクラス内の宣言だけでは実体を持ちません。クラスの外で一度だけ定義(初期化)する行が抜けていないか確認します
このエラーが出る典型パターン
パターン1
1 #include <iostream> 2 class Counter { 3 public: 4 static int total; 5 Counter() { total++; } 6 }; 7 ^ 8 int main() { 9 Counter a, b; 10 std::cout << Counter::total << std::endl; 11 return 0; 12 }
クラス内のstatic int total;はあくまで宣言です。実体(メモリ上の場所)はクラスの外側で一度だけ定義する必要があり、忘れるとコンパイルは通ってもリンクで失敗します。
直し方: (空) を int Counter::total = 0; にします。
パターン2
1 #include <iostream> 2 class Logger { 3 public: 4 static int count; 5 Logger() { count++; } 6 }; 7 ^ 8 int main() { 9 Logger a, b, c; 10 std::cout << Logger::count << std::endl; 11 return 0; 12 }
エラーはコンパイラ(g++)ではなくリンカ(ld)から出ます。コンパイル自体は各行が文法的に正しいので通ってしまいます。
直し方: (空) を int Logger::count = 0; にします。
パターン3
1 #include <iostream> 2 class Id { 3 public: 4 static int next; 5 Id() { id = next; next++; } 6 int id; 7 }; 8 ^ 9 int main() { 10 Id a, b; 11 std::cout << b.id << std::endl; 12 return 0; 13 }
staticメンバ変数の実体定義はクラス定義の外側、通常はグローバルスコープに1行書くだけで済みます。
直し方: (空) を int Id::next = 0; にします。
パターン4
1 #include <iostream> 2 class Inventory { 3 public: 4 static int total; 5 Inventory() { total++; } 6 }; 7 ^ 8 int main() { 9 Inventory a, b, c; 10 std::cout << Inventory::total << std::endl; 11 return 0; 12 }
クラス内のstatic宣言は実体を持たないため、クラスの外側で一度だけ定義する必要があります。
直し方: (空) を int Inventory::total = 0; にします。
パターン5
1 #include <iostream> 2 class Session { 3 public: 4 static int active; 5 Session() { active++; } 6 }; 7 ^ 8 int main() { 9 Session a, b; 10 std::cout << Session::active << std::endl; 11 return 0; 12 }
実体定義を忘れるとコンパイルは通ってもリンクの段階で失敗します。
直し方: (空) を int Session::active = 0; にします。
よくある誤解
「クラスの中に書けばそれで変数が実在する」というのは誤解です。クラス内のstaticメンバ変数の宣言はあくまで宣言であり、実体(メモリ上の場所)はクラスの外側で一度だけ定義する必要があります。忘れるとコンパイルは通ってもリンクで失敗します。
実務での勘所
C++17以降は、staticメンバ変数の宣言にinlineを付けることで、クラスの外側での実体定義を省略できるようになりました(static inline int count = 0;のようにクラス内で完結できます)。これはヘッダファイルが複数の翻訳単位からインクルードされても実体が重複しないことを保証する仕組みで、この種のリンクエラーが起きる根本原因(実体の定義忘れ)そのものを、言語機能で解消しています。