Om C++-template-implementaties op macOS in een afzonderlijk bronbestand te houden, instantieer je de ondersteunde typen expliciet op een plek waar hun definities zichtbaar zijn en link je dat objectbestand vervolgens aan je uitvoerbare bestand. In deze handleiding bouw je een klein lokaal voorbeeld met de C++-tools van Apple.
De taak is specifiek: compileer een C++-programma waarvan de bodies van templateleden in een .cpp-bestand staan. Het voorbeeld ondersteunt twee concrete typen. Een later gedeelte legt uit welke aanvullende vereisten gelden wanneer een Swift-desktopapp dezelfde C++-interface gebruikt.
Waarom mislukt het linken wanneer je templatecode naar een bronbestand verplaatst?
Een template beschrijft een familie van typen of functies. Het compileren van het implementatiebestand genereert niet automatisch elke mogelijke specialisatie. Ook instantieert een class-instantiatie niet automatisch de bodies van alle lidfuncties; leden worden doorgaans geïnstantieerd wanneer dat nodig is. Dit verklaart waarom een aanroeper succesvol kan compileren en toch bij het linken onverwijzingen overhoudt. C++-werkdocument: impliciete instantiatie
Denk aan een header die de layout van een class-template definieert en de constructor en methoden declareert. Een aanroeper kan die declaraties begrijpen. Maar als de bodies van de methoden alleen in een ander bronbestand staan, kan de aanroeper die bodies niet gebruiken om de benodigde functies te genereren. Het implementatiebestand moet de concrete instanties leveren die de aanroeper nodig heeft.
Een expliciete instantiatiedefinitie vraagt om die instanties. Een extern template-declaratie geeft aan dat een instantiatie elders wordt geleverd. De implementatiedefinities moeten beschikbaar zijn in de translation unit die de expliciete instantiatiedefinitie bevat. C++-werkdocument: expliciete instantiatie
Kies de indeling op basis van wie de typeargumenten beheert:
| Vereiste van de gebruiker | Geschikte indeling | Gevolg voor onderhoud |
|---|---|---|
| Aanroepers mogen willekeurige geschikte typen opgeven | Zet de definities in de header of in een implementatieheader die daarin wordt opgenomen | Aanroepers kunnen nieuwe typen instantiëren |
| De bibliotheek ondersteunt een bekende verzameling typen | Houd de bodies in een bronbestand en instantieer die verzameling expliciet | Voeg een instantiatie toe wanneer je een ondersteund type toevoegt |
| Een desktopapp heeft slechts een concrete bewerking nodig | Bied een gewone C++-wrapper rond een gekozen specialisatie aan | Houd templatekeuzes binnen de C++-implementatie |
Deze besliswijzer volgt uit de bovenstaande instantiatieregels. In deze handleiding is de ondersteunde verzameling bewust beperkt tot int en double.
Bouw een compleet voorbeeld met drie bestanden
Gebruik een nieuwe, lege map en sla de volgende drie bestanden daarin op. Het synthetische programma telt een offset op bij een waarde; dankzij de kleine invoerwaarden zijn de verwachte resultaten eenvoudig te controleren.
Bereid de opdrachtregeltools van Apple voor
Apple levert de compiler en macOS SDK via Xcode of het zelfstandige pakket Command Line Tools. Als geen van beide is geïnstalleerd, open je Terminal en voer je de volgende opdracht uit. Voltooi daarna het installatievenster. Als Xcode al is geïnstalleerd, zijn afzonderlijke tools niet nodig. De opdrachtregeltools installeren
xcode-select --install
Definieer de publieke interface
Sla deze volledige header op als Offset.hpp. Houd de classdefinitie, inclusief het datalid, zichtbaar voor aanroepers. Alleen de constructor- en methodebodies worden naar het bronbestand verplaatst.
#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
De twee regels met extern template declareren de geleverde instanties; ze leveren de memberbodies niet. Bij deze instantiatie op classniveau moet de definitie van de class-template bereikbaar blijven. C++-werkdocument: expliciete instantiatie
Definieer de bodies en lever de instanties
Sla deze volledige implementatie op als Offset.cpp. Plaats de expliciete instantiatiedefinities na de memberdefinities.
#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
De volgorde is van belang: een expliciete instantiatiedefinitie op classniveau omvat alleen leden die op dat moment zijn gedefinieerd. Ook instantieert die niet automatisch membertemplates. Gebruik één bronbestand als eigenaar van elke expliciete instantiatiedefinitie; de taal staat er per template en argumentenset maximaal één toe binnen het programma. Expliciete instantiatie, Template-instantiatie en specialisatie: algemeen
Roep beide ondersteunde typen aan
Sla deze volledige aanroeper op als 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;
}
Het programma controleert de resultaten zowel aan de hand van de afsluitstatus als door ze af te drukken. Dit voorbeeld is op consistentie gecontroleerd, maar tijdens de voorbereiding niet gecompileerd: de onderzoeksomgeving bevatte geen C++-compiler of macOS SDK.
Compileer beide bronbestanden en link daarna beide objectbestanden
Ga in Terminal naar de map met de drie bestanden. Voer deze opdrachten in volgorde uit en ga alleen verder als de vorige opdracht slaagt:
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
Hier maakt -c een objectbestand zonder te linken. De laatste aanroep van de compiler laat -c weg en combineert beide objecten tot een uitvoerbaar bestand. De opdrachten kiezen expliciet voor C++17 en libc++ in plaats van op standaardinstellingen te vertrouwen. Handleiding voor Clang-compileropdrachten
De verwachte uitvoer, afgeleid van de synthetische invoerwaarden, is:
7
2.5
De essentiële afhankelijkheid is zichtbaar in de laatste bouwopdracht: main.o vraagt de functies op en Offset.o levert hun instanties. Zorg er in een groter project voor dat het doel met Offset.cpp wordt gebouwd en dat het objectbestand of de bibliotheek bij de linkstap van het uitvoerbare bestand wordt betrokken. Alleen dit bestand succesvol compileren is niet genoeg om het linken af te ronden.
Voeg niet #include "Offset.cpp" toe aan de aanroeper terwijl je het bestand ook afzonderlijk compileert. Neem voor deze indeling de publieke header op en compileer de implementatie als een eigen translation unit. Als aanroepers de bodies nodig hebben, kies dan bewust voor de eerder genoemde indeling met een opgenomen implementatieheader.
Spoor ontbrekende instanties op in een handige volgorde
Identificeer eerst de exacte onverwezen functie, inclusief de templateargumenten en qualifiers. Volg daarna deze stappen. De controles passen het gedocumenteerde onderscheid toe tussen objectbestanden genereren en linken, samen met de vereisten voor expliciete instantiatie. Clang-compilatiestappen, Regels voor expliciete C++-instantiatie
| Wat je aantreft | Wat je moet controleren of aanpassen |
|---|---|
| Leden van beide ondersteunde typen zijn onverwezen | Controleer of Offset.o voorkomt in de uiteindelijke linkopdracht |
| Een nieuw gebruikt type geeft een fout | Voeg de bijbehorende instantiatie toe aan het bronbestand dat de eigenaar is en de declaratie aan de header |
| Eén methode ontbreekt | Controleer of de body voorafgaat aan de class-instantiatie en of de declaratie overeenkomt met de definitie |
| Een membertemplate ontbreekt | Voeg de vereiste membertemplate-instantiatie afzonderlijk toe, met de definitie beschikbaar |
| Er verschijnt een melding over dubbele definities | Controleer of instantiatiedefinities zijn herhaald of een implementatiebestand zowel is opgenomen als gecompileerd |
| De melding noemt een incompatibele architectuur | Controleer de architectuur van elk gelinkt object en elke bibliotheek |
Als je bijvoorbeeld de aanroeper wijzigt zodat die demo::Offset<float> gebruikt, introduceer je een type dat deze implementatie niet levert. Voeg om het te ondersteunen extern template class Offset<float>; toe binnen de namespace van de header en template class Offset<float>; na de bodies in Offset.cpp. Bouw daarna beide objecten opnieuw. Alleen de declaratie toevoegen genereert de ontbrekende functies niet.
Als LLVM's afzonderlijke uitvoerbare bestand llvm-nm is geïnstalleerd en via je opdrachtpad beschikbaar is, inspecteren deze optionele opdrachten de objecten uit het voorbeeld:
llvm-nm --demangle --undefined-only main.o
llvm-nm --demangle --defined-only Offset.o
Zoek naar overeenkomende lidnamen van demo::Offset in de onverwezen symbolen van de aanroeper en de gedefinieerde symbolen van de implementatie. Andere vermeldingen zijn te verwachten, waaronder verwijzingen naar de standaardbibliotheek. LLVM documenteert beide filters en het omzetten van namen naar leesbare vorm; deze opdrachten vereisen specifiek llvm-nm. Ga er niet van uit dat dit beschikbaar is bij een installatie van Apple's tools. Handleiding voor de opdracht llvm-nm
Behoud compatibiliteit van macOS-architectuur en ABI
Expliciete instantiatie levert concrete templatecode; daardoor wordt één objectbestand niet bruikbaar voor elke architectuur. Apples workflow voor universele binaries compileert en linkt architectuurspecifieke versies voordat die worden gecombineerd. Als de app zowel arm64 als x86_64 ondersteunt, neem de instantiatiebron dan in beide builds op. Een universele macOS-binary bouwen
Gebruik voor een universele build van het voorbeeld de volgende opdracht om het resulterende uitvoerbare bestand te inspecteren. Controleer of de gemelde architecturen overeenkomen met de gewenste build; de eerdere gewone bouwopdrachten vragen niet om een universele binary.
xcrun lipo -archs ./template-demo
Doordat de methodebodies gescheiden blijven, staat de classlayout nog steeds in de header. Behandel wijzigingen aan die layout als wijzigingen aan de interface, waarvoor afhankelijke code opnieuw moet worden gebouwd. Leid niet af dat er een stabiele binaire interface is alleen omdat er expliciete instanties bestaan. libc++ documenteert specifieke compatibiliteitsgaranties en configuratiebeperkingen en maakt expliciet onderscheid tussen die garanties en garanties voor door gebruikers gedefinieerde functies. ABI-garanties van libc++
Wat verandert er wanneer een Swift-app de C++-code aanroept?
Houd twee vereisten uit elkaar: Swift moet een ondersteunde C++-declaratie kunnen importeren en het uiteindelijke uitvoerbare bestand moet de implementatie linken. Volgens de Swift-documentatie zijn concrete class-template-specialisaties via C++-typealiassen beschikbaar; de niet-geïnstantieerde template zelf is niet rechtstreeks beschikbaar als Swift-type. Swift en C++ combineren: class-templates gebruiken
Voor de voorbeeldinterface is het volgende optionele headerfragment bedoeld om binnen namespace demo, na de classdefinitie, in te voegen. Het geeft het concrete type een naam die kan worden geïmporteerd:
using IntOffset = Offset<int>;
Die alias benoemt het type. De expliciete instantiatie in Offset.cpp levert nog steeds de out-of-line constructor en methode. Swift importeert C++-headers met Clang-modules en kan momenteel geen C++20-modules importeren. Volg voor het project de interoperabiliteitsconfiguratie op basis van headers. Swift en C++ combineren
Schakel in Xcode voor het Swift-doel C++ and Objective-C interoperability in met de waarde C++ / Objective-C++. Neem voor een appdoel dat C++ uit datzelfde doel gebruikt de C++-header op in de bridging header. Frameworkdoelen maken ondersteunde declaraties beschikbaar via publieke headers. Swift- en C++-projecten met gemengde talen instellen
Gemengde Swift- en C++-code moet dezelfde C++-standaardbibliotheek gebruiken; Apple-platforms gebruiken libc++. Voor interoperabiliteit is C++14 of nieuwer vereist. Laat de taalmodus van de geïmporteerde headers overeenkomen met de C++-build en controleer de lijst met ondersteunde functies voordat je ingewikkeldere templates beschikbaar maakt. Expliciete instantiatie breidt de door Swift ondersteunde taalconstructies niet uit. Ondersteunde functies en beperkingen van C++-interoperabiliteit
Houd bij een vaste verzameling typen de publieke declaraties, de instantiatielijst en de uiteindelijke linkinvoer bij elkaar. Wanneer een nieuwe aanroeper een ander type nodig heeft, voeg je dat type bewust toe of maak je de implementatie beschikbaar voor instantiatie aan de kant van de aanroeper. Zo blijft de scheiding tussen bronbestanden begrijpelijk naarmate het desktopproject groeit.
Bronnen
- Impliciete instantiatie — C++-werkdocument
- Expliciete instantiatie — C++-werkdocument
- Template-instantiatie en specialisatie: algemeen — C++-werkdocument
- De opdrachtregeltools installeren — Apple
- clang — de Clang-compiler voor C, C++ en Objective-C — LLVM Project
- llvm-nm — de symbolentabel van LLVM-bitcode en objectbestanden weergeven — LLVM Project
- Een universele macOS-binary bouwen — Apple
- ABI-garanties van libc++ — LLVM Project
- Swift en C++ combineren — Swift.org
- Swift- en C++-projecten met gemengde talen instellen — Swift.org
- Ondersteunde functies en beperkingen van C++-interoperabiliteit — Swift.org