複数の C++ ファイルからネイティブな Mac 実行ファイルをビルドするには、必要な実装ファイルをすべて clang++ に渡すか、それらをオブジェクトファイルにコンパイルしてからリンクします。この手順では、小さな Terminal プログラムをビルドし、定義が不足している場合の診断方法を説明します。

Mac のコンパイラツールを準備する

この手順は、Terminal でローカルに実行する小さな C++ プログラム向けです。Apple は、Xcode または単体の Command Line Tools パッケージを通じて、コンパイラと macOS SDK を提供しています。Xcode がすでにインストールされている場合、別のパッケージは必要ありません。それ以外の場合は、次のコマンドを実行し、インストールダイアログを完了してください。Apple のコマンドラインツールのインストールを参照してください。

xcode-select --install

インストール後、コンパイラが利用できることを確認します。

clang++ --version

最終リンクには、入力がすでにコンパイル済みのオブジェクトファイルであっても clang++ を使います。Clang のドキュメントでは、言語モードによって自動的にリンクされるランタイムライブラリが決まるため、C++ プログラムにはこのドライバを使うことが明示的に推奨されています。完全なツールチェーンの構築を参照してください。

3 ファイルからなる完全な C++ プログラムをビルドする

この架空の例では、デスク上にある 3 つのトレイの品物を数えて結果を整形します。エントリポイントが 1 つと、補助的な実装ファイルが 2 つあります。新しい空の作業ディレクトリを作成し、次の 4 つのファイルをその中に直接保存します。

desk-demo/
  desk.hpp
  main.cpp
  label.cpp
  totals.cpp

共有宣言を desk.hpp として保存します。

#ifndef DESK_DEMO_DESK_HPP
#define DESK_DEMO_DESK_HPP

#include <string>

std::string make_label(int count);
int total_items(int trays, int per_tray);

#endif

エントリポイントを main.cpp として保存します。

#include "desk.hpp"
#include <iostream>

int main() {
    const int count = total_items(3, 4);
    std::cout << make_label(count) << '\n';
    return 0;
}

整形の実装を label.cpp として保存します。

#include "desk.hpp"

std::string make_label(int count) {
    return "Desk items: " + std::to_string(count);
}

カウント処理の実装を totals.cpp として保存します。

#include "desk.hpp"

int total_items(int trays, int per_tray) {
    return trays * per_tray;
}

各実装ファイルは同じヘッダーをインクルードするため、コンパイラは関数定義が共有宣言と一致するか確認できます。ヘッダーガードは、1 つの翻訳単位内での重複インクルードを防ぎます。これらの通常の非 inline 関数本体は実装ファイルに置いてください。C++ では、単一定義規則により、プログラム全体にわたる定義要件が定められています。

1 つのコマンドでコンパイルとリンクを行う

4 つのファイルがあるディレクトリで Terminal を開き、次を実行します。

clang++ -std=c++17 main.cpp label.cpp totals.cpp -o desk-demo && ./desk-demo

期待される出力は次のとおりです。

Desk items: 12

このコマンドは 3 つの実装ファイルをすべて指定し、-o で出力ファイル名を設定します。処理段階を停止するオプションがないため、Clang は入力をコンパイルしてリンカーを呼び出します。明示的な -std=c++17 を指定しているので、コンパイラの既定値には依存せず言語バージョンを選択できます。これらのオプションについては、Clang コンパイラコマンドガイドに記載されています。

コマンドは && でつないでいるため、ビルドが成功した場合にのみシェルが実行ファイルを起動します。再ビルド時にはこれが重要です。ビルドに失敗した後、古い実行ファイルを誤って実行し、その出力を新しい結果だと思い込む事態を防げます。

ワイルドカードを使うのが適切なのはどんなときですか?

このディレクトリ構成に限れば、明示的なソース一覧の代わりに次の方法を使えます。

clang++ -std=c++17 ./*.cpp -o desk-demo && ./desk-demo

シェルは ./*.cpp を、現在のディレクトリ内で一致するファイルに展開します。サブフォルダは検索せず、.cc または .cxx で終わるファイルも含めません。パスを明示して指定する場合、スペースを含むパスは個別に引用符で囲んでください。

一致するファイルがすべてこの実行ファイルに属している間だけ、ワイルドカードを使ってください。独自の main を持つ別のデモプログラムや、プラットフォーム固有の代替実装を後から追加する場合は、ソース一覧を明示してください。ここでいう「すべての実装ファイル」とは、1 つのターゲットに必要なすべてのファイルであり、それぞれの必要な定義が適切に提供されていることを意味します。

先にオブジェクトファイルをコンパイルしてからリンクする

段階を分けると、ソースファイルのコンパイル中に失敗したのか、プログラムを結合するときに失敗したのかを見分けやすくなります。同じ 4 つのファイルと作業ディレクトリを使い、次を実行します。

(
    set -e
    mkdir -p build
    clang++ -std=c++17 -c main.cpp -o build/main.o
    clang++ -std=c++17 -c label.cpp -o build/label.o
    clang++ -std=c++17 -c totals.cpp -o build/totals.o
    clang++ build/main.o build/label.o build/totals.o -o build/desk-demo
    ./build/desk-demo
)

-c オプションはリンクせずにオブジェクトファイルを生成します。最後のコンパイラ呼び出しに 3 つのオブジェクトファイルを渡して、実行ファイルをリンクします。これは、Clang のドキュメントにあるコンパイルの各段階に沿った方法です。サブシェル内の set -e により、直線的に実行されるこの一連のコマンドは、いずれかが失敗すると停止します。

ここでもオブジェクトファイル名は明示してください。再利用しているビルドディレクトリ内のすべてのオブジェクトファイルをリンクすると、以前のバージョンのプログラムに属する古いファイルが誤って含まれることがあります。異なるフォルダにある 2 つのソースファイルのベース名が同じ場合は、両方を同じ出力ファイル名に書き込まず、別々のパスを指定してください。

label.cpp だけを変更した場合は、そのオブジェクトファイルを再コンパイルして、リンクコマンドをもう一度実行できます。desk.hpp を変更した場合は、それをインクルードしているすべてのソースを再ビルドしてください。この手作業の方法では、そうした依存関係を自分で把握する必要があります。プロジェクトが大きくなれば、ビルドシステムを使う方が実用的です。

ビルドコマンドを変更する前に失敗を診断する

次の手順では、この例にコンパイルの各段階と C++ の定義規則を適用します。診断メッセージの正確な文言は、インストールされているツールチェーンによって異なります。

症状 最初に確認すること 次に行う適切な手順
ヘッダーが見つからないと報告される インクルードの記述とヘッダーの場所 示された配置に戻すか、正しいヘッダー検索ディレクトリを設定します。
make_label または total_items がリンク時に未定義と報告される その実装が入力に含まれているか 不足している .cpp ファイルまたはコンパイル済みオブジェクトファイルをリンクに追加します。
ファイルをインクルードしているのに関数が未定義のまま 宣言と定義のシグネチャ 名前空間、パラメーター型、修飾子を比較し、実装ファイルに共有ヘッダーをインクルードします。
シンボルの重複が発生する 実装入力の重複 重複したオブジェクト、インクルードされた .cpp、または意図しない代替定義を取り除きます。
main が未定義または重複と報告される エントリポイントのファイル 意図した main.cpp を 1 回だけ含め、他のプログラムのエントリポイントは除外します。
C++ ライブラリのシンボルが未定義と報告される 最終リンクに使うドライバ C++ ランタイムが提供されるよう clang++ を使います。

たとえば、main.cpp だけをコンパイルしても、ヘッダーで両方の関数が宣言されているため、呼び出しは受け付けられることがあります。しかし、最終リンクにはそれらの定義が必要です。ヘッダーをさらにインクルードしても、不足している実装オブジェクトファイルは補われません。根拠となる要件については、C++ の単一定義規則で説明されています。

不足しているシンボルを解決するために、label.cpp を main.cpp にインクルードしながら、label.cpp も独立してコンパイルする方法は避けてください。同じ通常の関数定義が、2 つの翻訳単位から提供されてしまいます。共有ヘッダーと独立した実装ファイルを入力として使えば、意図した構成になります。

ビルドを実行せずにツールコマンドを確認するには、Clang のドキュメントに記載されている -### オプションを使います。シェルがそのまま渡せるよう、引用符で囲んでください。

clang++ '-###' -std=c++17 main.cpp label.cpp totals.cpp -o desk-demo

これにより、ドライバが選択したコンパイラとリンカーの呼び出しが表示されます。意図したコマンドと、ツールチェーンが実行するコマンドを区別するのに役立ちます。完全なツールチェーンの構築を参照してください。

アーキテクチャ、プラットフォーム、ABI に互換性を持たせる

ソースファイルをすべて追加しても、互換性のないバイナリ入力は修復できません。ローカルビルドでは、まずインストール済みの Apple ツールチェーンを 1 つ使い、プログラムのソースをまとめて再ビルドします。ビルド済みライブラリを追加する場合は、ソース一覧を変更する前に、対応するプラットフォームとアーキテクチャを確認してください。

任意のユニバーサルビルドについて、Apple はアーキテクチャごとに個別にコンパイルとリンクを行い、完成した実行ファイルを lipo で結合する方法を説明しています。両方のターゲットに対応したツールチェーンと SDK があれば、同じサンプルを次のようにビルドできます。以下に示す macOS 11 のデプロイメントターゲットは例示です。

(
    set -e
    mkdir -p build
    clang++ -std=c++17 -target arm64-apple-macos11 \
        main.cpp label.cpp totals.cpp -o build/desk-arm64
    clang++ -std=c++17 -target x86_64-apple-macos11 \
        main.cpp label.cpp totals.cpp -o build/desk-x86_64
    lipo -create build/desk-arm64 build/desk-x86_64 \
        -output build/desk-universal
    lipo -archs build/desk-universal
)

リンクする依存関係はすべて、各アーキテクチャのビルドに対応する入力を提供する必要があります。iOS バイナリは、両方が arm64 を使っているというだけで macOS バイナリにはなりません。Apple のユニバーサル macOS バイナリのビルドでは、アーキテクチャスライス、プラットフォームの違い、実行ファイルの検査について説明されています。

C++ の言語バージョンは、バイナリ互換性、すなわち ABI とは別のものです。-std=c++17 が一致しているだけでは、別々にビルドされたライブラリのレイアウトと構成に互換性があるとは限りません。LLVM は ABI に影響するライブラリ構成フラグについて説明し、ユーザー自身がそのベンダーフラグを設定すべきではないと述べています。この作業では、ツールチェーンの通常のライブラリ構成を維持するのが実用的です。libc++ の ABI 保証を参照してください。

Xcode または Swift 混在プロジェクトで同じソース一覧を適用する

既存の Xcode ターゲットで、ターゲットを選択して Build Phases を開きます。その実行ファイルに必要な実装ファイルが Compile Sources に含まれているか確認します。独自のライブラリ依存関係は Link Binary with Libraries から追加します。Apple は、これらの異なる役割についてターゲットのビルドフェーズをカスタマイズするで説明しています。

このサンプルでは、ソース一覧に main.cpp、label.cpp、totals.cpp が含まれるようにします。ヘッダーをプロジェクトに追加しても、実装のコンパイルの代わりにはなりません。

周囲のアプリケーションで Swift を使っている場合、上記の純粋な C++ コマンドで対象となるのは C++ プログラムだけです。Swift のドキュメントにある手順では、言語間で API を公開するために C++ 相互運用を有効にします。この境界を越えて使えるのは、対応している言語機能だけです。Swift と C++ の混在を参照してください。

Swift と C++ の混在コードでは、同じ C++ 標準ライブラリを使う必要があります。Apple プラットフォームでは、Swift は libc++ を使います。また、Swift のドキュメントでは、相互運用には C++14 以降が必要であることなど、コンパイラと言語バージョンの制約も説明されています。既存のライブラリを互換性のある依存関係として扱う前に、これらの要件を確認してください。C++ 相互運用でサポートされる機能と制約を参照してください。

ローカルの Terminal 作業では、まず 3 つのソースを明示したビルドから始めます。失敗した場合は、どの段階で失敗したかを特定し、不足または重複している定義を確認し、バイナリ入力に互換性があることを確かめます。そのコマンドで期待どおりの結果が得られたら、大きなビルドに引き継ぐための具体的なソース一覧として使えます。

出典