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