ヘッダーでexternを使ってグローバル変数を宣言し、1つの.cppファイルだけで定義して、そのファイルのオブジェクトファイルをすべての呼び出し元とリンクします。このガイドでは、macOSでのビルド全体、アドレスの確認方法、C++17のinline変数が適切な場合を説明します。
ヘッダー内のグローバル変数が重複定義を引き起こすのはなぜですか?
小規模なネイティブMacプログラムでの目的は、別々にコンパイルされる複数のソースファイルから、同じオブジェクトを読み書きできるようにすることです。まず宣言と定義を分けます。extern int itemCount;はオブジェクトを宣言し、int itemCount = 0;は定義します。externに初期化子を加えてextern int itemCount = 0;のようにすると、それも定義になります。C++ワーキングドラフトの宣言と定義では、この違いが説明されています。
通常の名前空間スコープの定義を含むヘッダーを複数のソースファイルがインクルードすると、その定義が各ファイルに取り込まれることがあります。外部リンケージを持つ非インライン・非テンプレート変数について、翻訳単位をまたいで複数の定義があると、単一定義規則に違反します。リンカーがシンボルの重複を報告する場合もありますが、言語仕様上、すべての違反を必ず診断する必要はありません。ワーキングドラフトの単一定義規則を参照してください。
インクルードガードは、1つの翻訳単位内での重複インクルードを防ぎます。別々にコンパイルされるソースファイル同士を調整するものではありません。そのため、ガード付きヘッダーでも、オブジェクトファイル間で複数の定義が生じることがあります。Clangのコンパイラコマンドガイドでは、前処理時のヘッダー展開と、その後のオブジェクトファイルのリンクについて説明されています。
Macで共有グローバル変数の完全な例をビルドする
Xcode、またはAppleのXcode Command Line Toolsパッケージを使います。Appleは、Xcode以外で開発する場合の選択肢として単独パッケージを案内しています。Xcodeをフルインストールしていれば、ツールはすでに含まれています。どちらもインストールされていない場合は、次のコマンドを実行し、インストールダイアログを完了してください。コマンドラインツールのインストールを参照してください。
xcode-select --install
ビルド前に、選択されている開発者ディレクトリとコンパイラを確認します。
xcode-select --print-path
xcrun clang++ --version
Appleのアーカイブ版Building from the Command Line with Xcode FAQでは、選択された開発者ディレクトリの確認と、xcrunを使った開発者ツールの検索や実行の両方が説明されています。この例では、最後まで同じツールチェーンを使ってください。
shared-global-demoという名前の新しい空フォルダーを作成し、エディターで開いて、以下の4つのファイルをそこに保存します。これは合成的な単一スレッドのC++プログラムです。1つのファイルがグローバル変数を所有し、別のファイルがそれを変更し、エントリーポイントが値と同一性の両方を確認します。
共有ヘッダーでオブジェクトを宣言する
shared_state.hppという名前で保存します。
#ifndef SHARED_STATE_HPP
#define SHARED_STATE_HPP
namespace demo {
extern int itemCount;
void addItem();
int* itemCountAddress();
}
#endif
このヘッダーは、各ソースファイルで使う宣言を提供します。名前空間と型を1つの共有ヘッダーにまとめることで、別々に手書きした宣言が食い違うのを防げます。
オブジェクトを1か所だけで定義する
shared_state.cppという名前で保存します。
#include "shared_state.hpp"
namespace demo {
int itemCount = 0;
}
これがdemo::itemCountの唯一の定義です。定義を置くソースファイルでヘッダーをインクルードすると、定義が宣言と一致するかをコンパイラが確認できます。明示的な初期化子により、初期値も明確になります。
別の翻訳単位から値を変更する
worker.cppという名前で保存します。
#include "shared_state.hpp"
namespace demo {
void addItem() {
++itemCount;
}
int* itemCountAddress() {
return &itemCount;
}
}
main.cppという名前で保存します。
#include "shared_state.hpp"
#include <iostream>
int main() {
std::cout << "before: " << demo::itemCount << '\n';
demo::addItem();
const bool sameObject =
demo::itemCountAddress() == &demo::itemCount;
std::cout << "after: " << demo::itemCount << '\n';
std::cout << std::boolalpha
<< "same object: " << sameObject << '\n';
return (demo::itemCount == 1 && sameObject) ? 0 : 1;
}
アドレス比較により、リンクの成功だけでは得られない有用な確認ができます。worker.cppは参照しているアドレスを返し、main.cppはそれを自身のアドレスと比較します。終了ステータスでは変更後の値も確認します。どちらの確認も、アサーションが有効かどうかには依存しません。
個別にコンパイルして、3つのオブジェクトファイルをすべてリンクする
ターミナルで、4つのファイルがあるフォルダーに移動し、次のビルド一式を実行します。
(
set -e
xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
-c shared_state.cpp -o shared_state.o
xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
-c worker.cpp -o worker.o
xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
-c main.cpp -o main.o
xcrun clang++ shared_state.o worker.o main.o -o shared-demo
./shared-demo
)
Clangの説明によると、-cはリンクせずにオブジェクトファイルを生成します。段階選択オプションを省略すると、ドライバーはリンクを行います。明示的な-std=c++17で言語モードを選択します。Clangコンパイラコマンドガイドを参照してください。サブシェルはコマンドが失敗すると停止するため、ビルドに失敗したときに古い実行ファイルを実行せずに済みます。
ソースから導いた予想出力は次のとおりです。Macで実行して取得したものではありません。
before: 0
after: 1
same object: true
調査環境にはMac用コンパイラがなかったため、この例は実行せずに確認しました。3つのコンパイルコマンドでは、オブジェクトファイルの境界が明確になります。shared_state.oが定義を提供し、worker.oとmain.oがそれを使用します。
C++17のインライン変数はいつ使うべきですか?
変数の定義を共有ヘッダーに置く場合は、C++17のインライン変数を選びます。Clangはインライン変数をC++17の言語機能の1つとして挙げています。言語仕様では、外部リンケージを持つインライン変数は複数の翻訳単位で定義でき、1つのアドレスを持つ単一の実体として扱われると定められています。inline指定子を参照してください。
同じ例を使う場合は、shared_state.hppの内容全体を次のように置き換えます。
#ifndef SHARED_STATE_HPP
#define SHARED_STATE_HPP
namespace demo {
inline int itemCount = 0;
void addItem();
int* itemCountAddress();
}
#endif
worker.cppとmain.cppは変更しません。このバリエーションのビルドからshared_state.cppを外します。別ファイルでの定義は不要になったためです。2つの使用側を再コンパイルし、それらのオブジェクトファイルだけをリンクします。
(
set -e
xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
-c worker.cpp -o worker.o
xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
-c main.cpp -o main.o
xcrun clang++ worker.o main.o -o shared-demo
./shared-demo
)
このバリエーションでも予想出力は同じです。リンクコマンドに古いshared_state.oを含めないでください。明示的なリンク対象から不要になったオブジェクトファイルを除くことで、所有関係の変更が明確になります。
インライン定義は、トークン列の一致や対応する名前検索を含む、単一定義規則の一貫性要件を満たす必要があります。ソースファイルごとに異なる初期化子を生成するマクロは避けてください。リンクに成功しただけでは、規則への準拠は確認できません。単一定義規則を参照してください。
| 要件 | 適切な方法 | 主な条件 |
|---|---|---|
| 1つの実装ファイルが変更可能な状態を所有する | externを使ったヘッダー宣言と、1つの.cpp定義 |
所有するオブジェクトファイルを1回だけリンクする |
| 共有定義をヘッダーに置く | C++17のインライン変数 | 使用する各翻訳単位で互換性のある言語モードを使い、定義を一致させる |
| 各ソースファイルが意図的に個別の状態を所有する | 名前空間スコープのstaticまたは無名名前空間 |
これらは内部リンケージを作るため、共有オブジェクトの要件は満たさない |
内部リンケージの違いは、ワーキングドラフトのプログラムとリンケージに基づいています。この小規模な複数ファイルプログラムでは、externの方法を使うと、所有関係を特に確認しやすくなります。
コンパイラフラグを変更する前に失敗の原因を調べる
次の手順で、所有関係の問題とビルド入力の不足を区別します。確認項目は、前述の宣言規則とコンパイル段階に基づいています。診断メッセージの表現はツールチェーンによって異なる場合があります。
- コンパイルに失敗したら、まず宣言を確認します。 各ソースが
shared_state.hppをインクルードし、同じ名前空間、型、綴りを使っていることを確認してください。リンクでは、未宣言の名前や競合する宣言は修正できません。 - リンクでシンボルの重複が報告されたら、定義とリンク入力の数を確認します。 ヘッダー内の初期化子、2つ目の
.cpp定義、所有するオブジェクトファイルの二重指定を探してください。インライン版では、古い非インライン定義が残っていないことを確認します。 - リンクで未定義シンボルが報告されたら、定義を提供するファイルを探します。
extern版では、shared_state.cppが正常にコンパイルされ、最終リンクコマンドにshared_state.oが含まれていることを確認してください。宣言だけでは定義は提供されません。 - リンクに成功しても同一性の確認に失敗したら、リンケージを調べます。 名前空間スコープの
static変数や、無名名前空間内の定義を探してください。それらは翻訳単位ごとに別のオブジェクトを作ります。 - ヘッダーや所有方法を変更したら、影響するすべてのオブジェクトファイルを再ビルドします。 該当するビルド一式を最初から実行し、呼び出し側と定義側が最新の宣言を使うようにします。
2つの簡便な方法には特に注意が必要です。1つ目は、ヘッダー内のグローバル変数を名前空間スコープのstatic int itemCount = 0;に置き換える方法です。これは翻訳単位ごとに別々のオブジェクトを作ります。2つ目は、通常の名前空間スコープのconst変数です。明示的なextern宣言やinlineなどの例外が適用されない限り、一般に内部リンケージを持ちます。意図するリンケージを明確に使ってください。該当する例外はプログラムとリンケージに記載されています。
言語リンケージも維持してください。extern "C"はC言語リンケージを指定するものであり、定義の不足を補ったり、重複定義を許可したりするものではありません。このC++のみの例では、既定のC++リンケージを使います。ワーキングドラフトのリンケージ指定では、言語リンケージと、実装依存の名前付けや呼び出し規約に関する性質が説明されています。
MacアプリにSwiftも含まれる場合、何が変わりますか?
C++のストレージ構成は引き続きC++実装側で管理します。ヘッダーをSwiftにインポートするとインターフェースは提供されますが、非インラインのグローバル変数を所有するC++実装もビルドに含める必要があります。Swiftの混在言語プロジェクトの設定ガイドでは、フレームワークの公開ヘッダーまたはアプリターゲットのブリッジングヘッダーを通じて、サポート対象のC++ APIを公開する方法が説明されています。
SwiftからC++を呼び出す必要があるXcodeターゲットでは、説明されている設定はC++ and Objective-C interoperabilityで、値をC++ / Objective-C++にします。呼び出し側ターゲットでこの設定を有効にし、適切なヘッダーを公開してください。これは、グローバル変数の定義を1つにする要件とは別のものです。Setting Up Mixed-Language Swift and C++ Projectsを参照してください。
ドキュメントに記載された互換性の制限を守ってください。Swift C++相互運用にはSwift 5.9以降が必要です。SwiftとC++の混在コードでは同じC++標準ライブラリを使う必要があり、Appleプラットフォームではlibc++を使います。この記事のインライン変数を含むヘッダーをインポートする場合、C++ソースだけでなくインポーターにもC++17以降を選択する必要があります。Swiftの言語標準設定とコンパイラの制約については、C++相互運用でサポートされる機能と制約に記載されています。
ここでの例は、1つのネイティブ実行ファイルにリンクされる、個別にコンパイルされたC++ファイルを対象としています。アドレス確認を適用する際も、この範囲を守ってください。別々に読み込まれるフレームワークやライブラリには、それぞれ独自のエクスポートとリンクの設計が必要です。ここで示した要件では、定義の所有者を特定し、それをリンクに1回だけ含め、両方の呼び出し元が同じアドレスと更新後の値を参照していることを確認します。
参考資料
- コマンドラインツールのインストール — Apple。
- Building from the Command Line with Xcode FAQ — Apple Documentation Archive。
- clang — Clang C、C++、Objective-Cコンパイラ — Clang/LLVM。
- ClangのC++サポート — Clang/LLVM。
- 宣言と定義 — eel.isで公開されているC++ワーキングドラフト。
- 単一定義規則 — eel.isで公開されているC++ワーキングドラフト。
- inline指定子 — eel.isで公開されているC++ワーキングドラフト。
- プログラムとリンケージ — eel.isで公開されているC++ワーキングドラフト。
- リンケージ指定 — eel.isで公開されているC++ワーキングドラフト。
- Setting Up Mixed-Language Swift and C++ Projects — Swift.org。
- C++相互運用でサポートされる機能と制約 — Swift.org。