Aby umieścić implementacje szablonów C++ w osobnym pliku źródłowym w systemie macOS, jawnie instancjuj obsługiwane typy tam, gdzie widoczne są ich definicje, a następnie dołącz ten plik obiektowy do pliku wykonywalnego. W tym przewodniku zbudujemy niewielki lokalny przykład za pomocą narzędzi C++ firmy Apple.

Zadanie jest konkretne: skompilować program C++, którego definicje metod szablonu znajdują się w pliku .cpp. Przykład obsługuje dwa konkretne typy. W dalszej części wyjaśniono dodatkowe wymagania, gdy aplikacja desktopowa w języku Swift korzysta z tego samego interfejsu C++.

Dlaczego przeniesienie kodu szablonu do pliku źródłowego powoduje błędy linkowania?

Szablon opisuje rodzinę typów lub funkcji. Skompilowanie jego pliku implementacyjnego nie tworzy automatycznie wszystkich możliwych specjalizacji. Instancjowanie klasy również nie powoduje automatycznie instancjowania ciał wszystkich jej funkcji składowych; funkcje składowe są zazwyczaj instancjowane w razie potrzeby. Te różnice wyjaśniają, dlaczego kod wywołujący może skompilować się poprawnie, a mimo to pozostawić nierozwiązane odwołania na etapie linkowania. Projekt roboczy C++: instancjowanie niejawne

Rozważ nagłówek definiujący układ szablonu klasy i deklarujący jego konstruktor oraz metody. Kod wywołujący może korzystać z tych deklaracji. Jeśli jednak ciała metod znajdują się tylko w innym pliku źródłowym, kod wywołujący nie może użyć tych ciał do wygenerowania potrzebnych funkcji. Plik implementacyjny musi dostarczyć konkretne instancje wymagane przez kod wywołujący.

Definicja jawnego instancjowania żąda utworzenia tych instancji. Deklaracja extern template wskazuje instancję dostarczaną w innym miejscu. Definicje implementacji muszą być dostępne w jednostce translacji zawierającej definicję jawnego instancjowania. Projekt roboczy C++: jawne instancjowanie

Wybierz układ odpowiednio do tego, kto kontroluje argumenty typów:

Wymaganie użytkownika Odpowiedni układ Konsekwencja utrzymaniowa
Kod wywołujący może podawać dowolne odpowiednie typy Umieść definicje w nagłówku albo w nagłówku implementacji dołączanym z niego Kod wywołujący może instancjować nowe typy
Biblioteka obsługuje znany zestaw typów Umieść ciała w pliku źródłowym i jawnie instancjuj ten zestaw Przy dodawaniu obsługiwanego typu dodaj jego instancję
Aplikacja desktopowa potrzebuje tylko konkretnej operacji Udostępnij zwykły wrapper C++ wokół wybranej specjalizacji Wybory szablonu pozostają wewnątrz implementacji C++

Ta wskazówka wynika z opisanych wyżej reguł instancjowania. W tym przewodniku obsługiwany zestaw jest celowo ograniczony do int i double.

Zbuduj kompletny przykład składający się z trzech plików

Utwórz nowy, pusty folder i zapisz w nim razem poniższe trzy pliki. Syntetyczny program dodaje przesunięcie do wartości; niewielkie dane wejściowe ułatwiają sprawdzenie oczekiwanych wyników.

Przygotuj narzędzia wiersza poleceń firmy Apple

Firma Apple udostępnia kompilator i SDK systemu macOS za pośrednictwem Xcode lub samodzielnego pakietu Command Line Tools. Jeśli nie masz zainstalowanego żadnego z nich, otwórz Terminal, uruchom poniższe polecenie i dokończ instalację w oknie instalatora. Jeśli Xcode jest już zainstalowany, nie trzeba instalować osobnego pakietu narzędzi. Instalowanie narzędzi wiersza poleceń

xcode-select --install

Zdefiniuj interfejs publiczny

Zapisz ten kompletny nagłówek jako Offset.hpp. Definicja klasy, w tym jej składowa danych, powinna być widoczna dla kodu wywołującego. Do pliku źródłowego przenosimy tylko ciała konstruktora i metod.

#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

Dwie linie extern template deklarują dostarczane instancje; nie zawierają ciał metod. Definicja szablonu klasy musi pozostać dostępna przy takim układzie jawnego instancjowania na poziomie klasy. Projekt roboczy C++: jawne instancjowanie

Zdefiniuj ciała i dostarcz instancje

Zapisz tę kompletną implementację jako Offset.cpp. Umieść definicje jawnego instancjowania po definicjach elementów klasy.

#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

Kolejność ma znaczenie: definicja jawnego instancjowania klasy obejmuje tylko te funkcje składowe, które zostały zdefiniowane w tym miejscu lub wcześniej. Nie instancjuje też automatycznie szablonów funkcji składowych. Dla każdej definicji jawnego instancjowania zachowaj jeden plik źródłowy będący jej właścicielem; język dopuszcza w programie najwyżej jedną taką definicję dla danego szablonu i zestawu argumentów. Jawne instancjowanie, Instancjowanie i specjalizacja szablonów: zagadnienia ogólne

Wywołaj oba obsługiwane typy

Zapisz ten kompletny kod wywołujący jako 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;
}

Program sprawdza wyniki za pomocą kodu wyjścia i jednocześnie je wypisuje. Spójność tego przykładu sprawdzono, ale nie skompilowano go podczas przygotowywania: w środowisku badawczym nie było kompilatora C++ ani SDK systemu macOS.

Skompiluj oba pliki źródłowe, a następnie połącz oba pliki obiektowe

W Terminalu przejdź do folderu zawierającego trzy pliki. Uruchom kolejno poniższe polecenia, kontynuując tylko wtedy, gdy poprzednie zakończy się powodzeniem:

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

Tutaj -c tworzy plik obiektowy bez linkowania. Ostatnie wywołanie kompilatora pomija -c i łączy oba pliki obiektowe w plik wykonywalny. Polecenia jawnie wybierają C++17 i libc++, zamiast polegać na ustawieniach domyślnych. Przewodnik po poleceniach kompilatora Clang

Oczekiwany wynik, wyliczony na podstawie syntetycznych danych wejściowych, to:

7
2.5

Kluczowa zależność jest widoczna w ostatnim poleceniu budowania: main.o żąda funkcji, a Offset.o dostarcza ich instancje. W większym projekcie upewnij się, że cel zawierający Offset.cpp jest budowany, a jego plik obiektowy lub biblioteka trafia na etap linkowania pliku wykonywalnego. Samo pomyślne skompilowanie tego pliku nie kończy linkowania.

Nie dodawaj #include "Offset.cpp" do kodu wywołującego, jeśli jednocześnie kompilujesz ten plik osobno. W tym układzie dołącz publiczny nagłówek, a plik implementacyjny kompiluj jako osobną jednostkę translacji. Jeśli kod wywołujący ma potrzebować ciał, wybierz świadomie opisany wcześniej układ z dołączanym nagłówkiem implementacji.

Diagnozuj brakujące instancje w użytecznej kolejności

Najpierw ustal dokładną nierozwiązaną funkcję, łącznie z jej argumentami szablonu i kwalifikatorami. Następnie wykonaj poniższe kroki. Sprawdzenia te wykorzystują opisaną różnicę między generowaniem plików obiektowych a ich linkowaniem oraz wymagania dotyczące jawnego instancjowania. Etapy kompilacji w Clang, Reguły jawnego instancjowania w C++

Co stwierdzisz Co sprawdzić lub zmienić
Brakuje nierozwiązanych metod dla obu obsługiwanych typów Upewnij się, że Offset.o znajduje się w końcowym poleceniu linkowania
Nie działa nowo użyty typ Dodaj odpowiadającą mu instancję do pliku implementacyjnego będącego jej właścicielem oraz deklarację do nagłówka
Brakuje jednej metody Upewnij się, że jej ciało występuje przed instancją klasy, a deklaracja odpowiada definicji
Brakuje szablonu funkcji składowej Osobno dostarcz wymaganą instancję szablonu funkcji składowej, gdy jej definicja jest widoczna
Pojawia się komunikat o zduplikowanej definicji Sprawdź, czy definicje instancjowania się nie powtarzają lub czy plik implementacyjny nie jest jednocześnie kompilowany i dołączany
Komunikat dotyczy niezgodnej architektury Sprawdź architekturę każdego linkowanego pliku obiektowego i każdej biblioteki

Na przykład zmiana kodu wywołującego tak, by używał demo::Offset<float>, wprowadza typ, którego ta implementacja nie dostarcza. Aby go obsłużyć, dodaj extern template class Offset<float>; wewnątrz przestrzeni nazw nagłówka oraz template class Offset<float>; za ciałami w Offset.cpp, a następnie ponownie zbuduj oba pliki obiektowe. Samo dodanie deklaracji nie wygeneruje brakujących funkcji.

Jeśli osobny plik wykonywalny LLVM llvm-nm jest zainstalowany i dostępny w ścieżce poleceń, poniższe opcjonalne polecenia umożliwiają sprawdzenie plików obiektowych z przykładu:

llvm-nm --demangle --undefined-only main.o
llvm-nm --demangle --defined-only Offset.o

Szukaj pasujących nazw metod demo::Offset w nierozwiązanych symbolach kodu wywołującego oraz w zdefiniowanych symbolach implementacji. Pozostałe wpisy są oczekiwane, w tym odwołania do biblioteki standardowej. LLVM opisuje oba filtry i demangling nazw; te polecenia wymagają konkretnie llvm-nm, którego dostępności nie należy zakładać na podstawie samej instalacji narzędzi firmy Apple. Przewodnik po poleceniu llvm-nm

Zachowaj zgodność architektury i ABI systemu macOS

Jawne instancjowanie dostarcza konkretny kod szablonu; nie sprawia, że jeden plik obiektowy działa na każdej architekturze. W procesie tworzenia pliku uniwersalnego firmy Apple kompiluje się i linkuje wersje specyficzne dla architektur, a następnie łączy je w całość. Jeśli aplikacja obsługuje zarówno arm64, jak i x86_64, uwzględnij plik źródłowy z instancjami w obu kompilacjach. Tworzenie uniwersalnego pliku binarnego systemu macOS

Aby utworzyć uniwersalną wersję przykładu, sprawdź wynikowy plik wykonywalny za pomocą poniższego polecenia. Upewnij się, że zgłoszone architektury odpowiadają zamierzonej kompilacji; wcześniejsze zwykłe polecenia budowania nie żądają utworzenia pliku uniwersalnego.

xcrun lipo -archs ./template-demo

Oddzielenie ciał metod pozostawia układ klasy widoczny w nagłówku. Traktuj zmiany tego układu jako zmiany interfejsu, które wymagają ponownego zbudowania zależnego kodu. Nie zakładaj, że jawne instancjowanie zapewnia stabilny interfejs binarny. libc++ opisuje konkretne gwarancje zgodności oraz ograniczenia konfiguracji, a także wyraźnie odróżnia te gwarancje od gwarancji dotyczących funkcji zdefiniowanych przez użytkownika. Gwarancje ABI libc++

Co się zmienia, gdy aplikacja Swift wywołuje kod C++?

Rozdziel dwa wymagania: Swift musi móc zaimportować obsługiwaną deklarację C++, a końcowy plik wykonywalny musi być połączony z jej implementacją. Dokumentacja Swifta udostępnia konkretne specjalizacje szablonów klas za pomocą aliasów typów C++; sam niezinstancjowany szablon nie jest bezpośrednio dostępny jako typ Swift. Łączenie Swifta z C++: używanie szablonów klas

W przypadku interfejsu z przykładu poniższy fragment nagłówka jest opcjonalny; wstaw go wewnątrz namespace demo, za definicją klasy. Nadaje on konkretnemu typowi nazwę, którą można zaimportować:

using IntOffset = Offset<int>;

Alias nadaje nazwę typowi. Jawne instancjowanie w Offset.cpp nadal dostarcza konstruktor i metodę zdefiniowane poza klasą. Swift importuje nagłówki C++ za pomocą modułów Clanga, a obecnie nie potrafi importować modułów C++20. W projekcie zastosuj konfigurację współdziałania opartą na nagłówkach. Łączenie Swifta z C++

W Xcode włącz opcję C++ and Objective-C interoperability i ustaw jej wartość na C++ / Objective-C++ dla celu Swift. W przypadku celu aplikacji korzystającego z C++ w ramach tego samego celu dołącz nagłówek C++ w nagłówku pomostowym. Cele typu framework udostępniają obsługiwane deklaracje za pośrednictwem nagłówków publicznych. Konfigurowanie projektów łączących Swifta i C++

Kod mieszany w Swift i C++ musi korzystać z tej samej biblioteki standardowej C++; na platformach Apple używana jest libc++. Współdziałanie wymaga C++14 lub nowszego. Dopasuj tryb języka zaimportowanych nagłówków do kompilacji C++ i sprawdź listę obsługiwanych funkcji, zanim udostępnisz bardziej złożone szablony. Jawne instancjowanie nie rozszerza obsługiwanych przez Swift mapowań języka. Obsługiwane funkcje i ograniczenia współdziałania z C++

W przypadku ustalonego zestawu typów należy wspólnie utrzymywać deklaracje publiczne, listę instancji i wejścia końcowego linkowania. Gdy nowy kod wywołujący potrzebuje kolejnego typu, dodaj go świadomie albo udostępnij implementację do instancjowania po stronie kodu wywołującego. Dzięki temu granica wyznaczona przez plik źródłowy pozostanie zrozumiała w miarę rozwoju projektu desktopowego.

Źródła