若要在 macOS 上將 C++ 範本實作保留在獨立的原始檔中,請在定義可見的位置明確實例化支援的型別,然後將該物件檔連結至可執行檔。本指南會使用 Apple 的 C++ 工具建置一個小型本機範例。
這項任務範圍明確:編譯一個 C++ 程式,讓範本成員的本體位於 .cpp 檔案中。範例支援兩種具體型別。後續章節會說明 Swift 桌面應用程式使用相同 C++ 介面時的其他要求。
為什麼將範本程式碼移至原始檔會導致連結失敗?
範本描述一組型別或函式。編譯其實作檔並不會自動產生所有可能的特化。實例化類別也不會自動實例化每個成員函式本體;通常只有在需要時才會實例化成員。這些差異說明了為什麼呼叫端可能順利編譯,卻在連結時留下未解析的參照。C++ 工作草案:隱式實例化
假設有一個標頭檔定義類別範本的配置,並宣告其建構函式和方法。呼叫端可以理解這些宣告。但如果方法本體只存在於另一個原始檔中,呼叫端就無法利用這些本體產生所需的函式。實作檔必須提供呼叫端需要的具體實例化。
明確實例化定義會要求產生這些實例化。extern template 宣告則指出某個實例化由其他地方提供。提供明確實例化定義的轉譯單元必須能取得實作定義。C++ 工作草案:明確實例化
請根據誰能控制型別引數來選擇配置:
| 讀取者的需求 | 適用配置 | 維護時的影響 |
|---|---|---|
| 呼叫端可提供任意合適的型別 | 將定義放在標頭檔中,或放在由標頭檔引入的實作標頭檔中 | 呼叫端可以實例化新型別 |
| 程式庫支援一組已知型別 | 將本體保留在原始檔中,並明確實例化該型別集合 | 新增支援型別時,也要新增相應的實例化 |
| 桌面應用程式只需要某個具體操作 | 以一般 C++ 包裝函式包住選定的特化 | 將範本選擇保留在 C++ 實作中 |
這份選擇指南是根據上述實例化規則整理而成。本指南的支援型別刻意限定為 int 和 double。
建置完整的三檔範例
建立一個新的空資料夾,並將以下三個檔案一同儲存其中。這個示範程式會將偏移量加到某個值上;簡單的輸入值便於檢查預期結果。
準備 Apple 的命令列工具
Apple 透過 Xcode 或獨立的 Command Line Tools 套件提供編譯器和 macOS SDK。如果兩者都尚未安裝,請開啟終端機並執行以下命令,然後完成安裝程式對話框。如果已安裝 Xcode,就不需要另外安裝工具。安裝命令列工具
xcode-select --install
定義公開介面
將以下完整標頭檔儲存為 Offset.hpp。請讓類別定義(包括其資料成員)對呼叫端可見。只有建構函式和方法本體會移至原始檔中。
#ifndef OFFSET_HPP
#define OFFSET_HPP
namespace demo {
template <typename T>
class Offset {
public:
explicit Offset(T amount);
T apply(T value) const;
private:
T amount_;
};
extern template class Offset<int>;
extern template class Offset<double>;
} // namespace demo
#endif
兩行 extern template 是宣告所提供的實例化;它們並未提供成員本體。採用這種類別層級明確實例化配置時,類別範本定義必須仍可供使用。C++ 工作草案:明確實例化
定義本體並提供實例化
將以下完整實作儲存為 Offset.cpp。請在成員定義之後放置明確實例化定義。
#include "Offset.hpp"
namespace demo {
template <typename T>
Offset<T>::Offset(T amount) : amount_(amount) {}
template <typename T>
T Offset<T>::apply(T value) const {
return value + amount_;
}
template class Offset<int>;
template class Offset<double>;
} // namespace demo
順序很重要:類別層級的明確實例化定義只涵蓋在該位置之前已定義的成員。它也不會自動實例化成員範本。每個明確實例化定義都應由一個專屬原始檔負責;語言規定,程式中每組範本與引數最多只能有一個此類定義。明確實例化、範本實例化與特化:一般規則
呼叫兩種支援型別
將以下完整呼叫端儲存為 main.cpp。
#include "Offset.hpp"
#include <iostream>
int main() {
const demo::Offset<int> whole(3);
const demo::Offset<double> fractional(0.5);
const int first = whole.apply(4);
const double second = fractional.apply(2.0);
std::cout << first << '\n' << second << '\n';
return (first == 7 && second == 2.5) ? 0 : 1;
}
程式會透過結束狀態及輸出結果來檢查計算結果。此範例已檢查一致性,但準備期間並未編譯:研究環境沒有 C++ 編譯器或 macOS SDK。
先編譯兩個原始檔,再連結兩個物件檔
在終端機中切換至包含這三個檔案的資料夾。依序執行以下命令;只有在前一個命令成功後才繼續:
xcrun clang++ -std=c++17 -stdlib=libc++ -Wall -Wextra -pedantic -c Offset.cpp -o Offset.o
xcrun clang++ -std=c++17 -stdlib=libc++ -Wall -Wextra -pedantic -c main.cpp -o main.o
xcrun clang++ -std=c++17 -stdlib=libc++ main.o Offset.o -o template-demo
./template-demo
此處的 -c 會產生物件檔,但不進行連結。最後一次編譯器呼叫省略 -c,並將兩個物件合併成可執行檔。這些命令明確指定 C++17 和 libc++,而不依賴預設值。Clang 編譯器命令指南
根據示範輸入值推算,預期輸出如下:
7
2.5
最後一個建置命令清楚呈現了關鍵相依性:main.o 會要求所需函式,而 Offset.o 則提供這些函式的實例化。在較大型的專案中,請確保包含 Offset.cpp 的目標有建置完成,並且其物件檔或程式庫有加入可執行檔的連結步驟。單獨成功編譯該檔案並不代表連結已完成。
當呼叫端也會單獨編譯 Offset.cpp 時,不要在呼叫端加入 #include "Offset.cpp"。採用這種配置時,請引入公開標頭檔,並將實作檔編譯為獨立的轉譯單元。如果呼叫端需要本體,請有意選擇前表中的引入實作標頭檔配置。
依實用順序診斷缺少的實例化
先找出確切的未解析函式,包括其範本引數和限定詞。接著依照以下順序檢查。這些檢查會運用物件檔產生與連結之間的差異,以及明確實例化的要求。Clang 編譯階段、C++ 明確實例化規則
| 發現的情況 | 檢查或變更項目 |
|---|---|
| 兩種支援型別的成員都未解析 | 確認最後的連結命令中有 Offset.o |
| 新使用的型別失敗 | 在負責的實作檔中新增相應的實例化,並在標頭檔中新增宣告 |
| 缺少某個方法 | 確認其本體位於類別實例化之前,且其宣告與定義相符 |
| 缺少成員範本 | 在定義可見的情況下,另外提供所需的成員範本實例化 |
| 出現重複定義診斷 | 檢查是否重複提供實例化定義,或同時編譯並引入某個實作檔 |
| 診斷指出架構不相容 | 檢查每個連結物件檔和程式庫的架構 |
例如,若將呼叫端改為使用 demo::Offset<float>,就會引入此實作尚未提供的型別。若要支援它,請在標頭檔的命名空間內加入 extern template class Offset<float>;,並在 Offset.cpp 的本體之後加入 template class Offset<float>;,然後重新建置兩個物件檔。只新增宣告不會產生缺少的函式。
如果 LLVM 獨立的 llvm-nm 可執行檔已安裝且位於命令搜尋路徑中,可以選擇執行以下命令檢查範例中的物件檔:
llvm-nm --demangle --undefined-only main.o
llvm-nm --demangle --defined-only Offset.o
請在呼叫端的未解析符號和實作檔的已定義符號中,尋找相符的 demo::Offset 成員名稱。其他項目(包括標準程式庫參照)都是預期會出現的。LLVM 說明了這兩種篩選方式和名稱反修飾;這些命令特別需要 llvm-nm,不應假設安裝 Apple 工具後就一定有它。llvm-nm 命令指南
維持 macOS 架構與 ABI 相容性
明確實例化會提供具體的範本程式碼,但不會讓單一物件檔適用於所有架構。Apple 的通用二進位檔工作流程會先編譯並連結各架構專用的版本,再將它們合併。如果應用程式同時支援 arm64 和 x86_64,請在兩種建置中都納入實例化來源檔。建置 macOS 通用二進位檔
若要建置此範例的通用版本,請使用以下命令檢查產生的可執行檔。確認回報的架構符合預期;前述一般建置命令不會要求產生通用二進位檔。
xcrun lipo -archs ./template-demo
將方法本體分開放置,也會讓類別配置繼續保留在標頭檔中。請將該配置的變更視為介面變更,並重新建置相依程式碼。不要因為有明確實例化就推斷出穩定的二進位介面。libc++ 說明了特定的相容性保證及設定限制,並明確指出這些保證不涵蓋使用者定義函式。libc++ 的 ABI 保證
Swift 應用程式呼叫 C++ 程式碼時有哪些變化?
請將兩項要求分開處理:Swift 必須能匯入受支援的 C++ 宣告,而最終可執行檔也必須連結其實作。Swift 文件說明,可透過 C++ 型別別名匯入具體的類別範本特化;未實例化的範本本身無法直接作為 Swift 型別使用。混合使用 Swift 與 C++:使用類別範本
對於範例介面,以下是可選的標頭檔片段,請插入 namespace demo 內的類別定義之後。它會為具體型別提供一個可供匯入的名稱:
using IntOffset = Offset<int>;
這個別名會命名該型別。Offset.cpp 中的明確實例化仍會提供類別外定義的建構函式和方法。Swift 會透過 Clang 模組匯入 C++ 標頭檔,目前無法匯入 C++20 模組。請依照以標頭檔為基礎的互通性設定來配置專案。混合使用 Swift 與 C++
在 Xcode 中,請為 Swift 目標啟用 C++ and Objective-C interoperability,並將值設為 C++ / Objective-C++。若應用程式目標要使用同一目標中的 C++,請在其橋接標頭檔中加入 C++ 標頭檔。框架目標則透過公開標頭檔公開受支援的宣告。設定混合語言 Swift 與 C++ 專案
混合使用 Swift 和 C++ 的程式碼必須使用相同的 C++ 標準程式庫;Apple 平台使用 libc++。互通性要求 C++14 或更新版本。請讓匯入標頭檔的語言模式符合 C++ 建置設定,並在公開較複雜的範本之前,先查看受支援功能清單。明確實例化不會擴展 Swift 支援的語言對應範圍。C++ 互通性的支援功能與限制
若型別集合固定,請一併維護公開宣告、實例化清單和最終連結輸入項目。若新呼叫端需要其他型別,請明確新增該型別,或讓實作可供呼叫端進行實例化。這項選擇能讓桌面專案逐漸擴大時,原始檔邊界仍清楚易懂。
來源
- 隱式實例化 — C++ 工作草案
- 明確實例化 — C++ 工作草案
- 範本實例化與特化:一般規則 — C++ 工作草案
- 安裝命令列工具 — Apple
- clang — Clang C、C++ 與 Objective-C 編譯器 — LLVM 專案
- llvm-nm — 列出 LLVM 位元碼和物件檔的符號表 — LLVM 專案
- 建置 macOS 通用二進位檔 — Apple
- libc++ 的 ABI 保證 — LLVM 專案
- 混合使用 Swift 與 C++ — Swift.org
- 設定混合語言 Swift 與 C++ 專案 — Swift.org
- C++ 互通性的支援功能與限制 — Swift.org