Чтобы сохранить реализации шаблонов C++ в отдельном исходном файле на macOS, явно инстанцируйте поддерживаемые типы там, где их определения видны, а затем свяжите этот объектный файл с вашим исполняемым файлом. Это руководство создает небольшой локальный пример с инструментами C++ от Apple.
Задача конкретна: скомпилировать программу на C++, тела шаблонных членов которой находятся в файле .cpp. Пример поддерживает два конкретных типа. В более позднем разделе объясняются дополнительные требования, когда настольное приложение Swift использует тот же интерфейс C++.
Почему перемещение кода шаблона в исходный файл нарушает компоновку?
Шаблон описывает семейство типов или функций. Компиляция его файла реализации не автоматически создает каждую возможную специализацию. Инстанциация класса также не автоматически инстанцирует тело каждой функции-члена; члены обычно инстанцируются по мере необходимости. Эти различия объясняют, почему вызывающий код может успешно скомпилироваться, но оставить неразрешенные ссылки во время компоновки. Рабочий черновик C++: Неявная инстанциация
Рассмотрим заголовочный файл, определяющий компоновку шаблона класса и объявляющий его конструктор и методы. Вызывающий код может понять эти объявления. Но если тела методов существуют только в другом исходном файле, вызывающий код не может использовать их для генерации требуемых функций. Файл реализации должен предоставлять конкретные инстанциации, необходимые вызывающему коду.
Определение явной инстанциации запрашивает эти инстанциации. Объявление extern template идентифицирует инстанциацию, предоставляемую в другом месте. Определения реализации должны быть доступны в единице трансляции, предоставляющей определение явной инстанциации. Рабочий черновик C++: Явная инстанциация
Выберите расположение в зависимости от того, кто контролирует аргументы типов:
| Требование читателя | Подходящее расположение | Последствие для обслуживания |
|---|---|---|
| Вызывающие стороны могут предоставлять произвольные подходящие типы | Разместите определения в заголовке или в заголовочном файле реализации, включаемом из него | Вызывающие стороны могут инстанцировать новые типы |
| Библиотека поддерживает известный набор типов | Храните тела в исходном файле и явно инстанцируйте этот набор | Добавьте инстанциацию при добавлении поддерживаемого типа |
| Настольному приложению нужна только конкретная операция | Предоставьте обычную обертку C++ вокруг выбранной специализации | Храните выборы шаблонов внутри реализации C++ |
Это руководство по принятию решений следует из правил инстанциации выше. Для данного руководства поддерживаемый набор намеренно ограничен int и double.
Построение полного примера из трех файлов
Используйте новую пустую папку и сохраните следующие три файла вместе. Синтетическая программа добавляет смещение к значению; небольшие входные данные позволяют легко проверить ожидаемые результаты.
Подготовка инструментов командной строки Apple
Apple поставляет компилятор и macOS SDK через Xcode или отдельный пакет Command Line Tools. Если ни один из них не установлен, откройте Терминал и выполните следующую команду, затем завершите диалог установки. Отдельная установка инструментов не требуется, если 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, построена, и что ее объектный файл или библиотека достигает этапа компоновки исполняемого файла. Успешная компиляция только этого файла не завершает компоновку.
Не добавляйте #include "Offset.cpp" в вызывающий код, одновременно компилируя его отдельно. Для этого расположения включите публичный заголовок и скомпилируйте реализацию как отдельную единицу трансляции. Если вызывающим сторонам нужны тела, намеренно выберите расположение с включенным заголовочным файлом реализации из предыдущей таблицы.
Диагностика отсутствующих инстанциаций в полезном порядке
Сначала определите точную неразрешенную функцию, включая ее шаблонные аргументы и квалификаторы. Затем следуйте этой последовательности. Проверки применяют задокументированное различие между генерацией объектных файлов и их компоновкой, вместе с требованиями явной инстанциации. Этапы компиляции Clang, Правила явной инстанциации C++
| Что вы обнаружили | Что проверить или изменить |
|---|---|
| У обоих поддерживаемых типов есть неразрешенные члены | Убедитесь, что Offset.o присутствует в финальной команде компоновки |
| Только что используемый тип не работает | Добавьте соответствующую инстанциацию в владеющий файл реализации и объявление в заголовок |
| Отсутствует один метод | Убедитесь, что его тело предшествует инстанциации класса, а его объявление соответствует определению |
| Отсутствует шаблон члена | Предоставьте необходимую инстанциацию шаблона члена отдельно, с видимым определением |
| Появляется диагностика дублирования определения | Проверьте наличие повторяющихся определений инстанциации или включенный и скомпилированный файл реализации |
| Диагностика указывает на несовместимую архитектуру | Проверьте архитектуру каждого скомпонованного объекта и библиотеки |
Например, изменение вызывающего кода для использования demo::Offset<float> вводит тип, который эта реализация не предоставляет. Чтобы поддержать его, добавьте extern template class Offset<float>; внутри пространства имен заголовка и template class Offset<float>; после тел в Offset.cpp, затем пересоберите оба объекта. Простое добавление объявления не генерирует отсутствующие функции.
Если отдельный исполняемый файл llvm-nm LLVM установлен и доступен в вашем пути команд, эти необязательные команды проверяют объекты из примера:
llvm-nm --demangle --undefined-only main.o
llvm-nm --demangle --defined-only Offset.o
Ищите совпадающие имена членов demo::Offset в неразрешенных символах вызывающего кода и определенных символах реализации. Ожидаются и другие записи, включая ссылки на стандартную библиотеку. LLVM документирует как фильтры, так и деманглинг имен; эти команды специально требуют llvm-nm, доступность которого не следует предполагать из установки инструментов Apple. Руководство по команде llvm-nm
Сохранение совместимости архитектуры и ABI macOS
Явная инстанциация предоставляет конкретный код шаблона; она не делает один объектный файл пригодным для каждой архитектуры. Рабочий процесс универсальных бинарных файлов Apple компилирует и компонует версии для конкретных архитектур перед их объединением. Если приложение поддерживает и arm64, и x86_64, включите исходный код инстанциации в обе сборки. Создание универсального бинарного файла macOS
Для универсальной сборки примера проверьте полученный исполняемый файл с помощью следующей команды. Убедитесь, что заявленные архитектуры соответствуют предполагаемой сборке; предыдущие обычные команды сборки не запрашивают универсальный бинарный файл.
xcrun lipo -archs ./template-demo
Хранение тел методов отдельно также оставляет компоновку класса видимой в заголовке. Рассматривайте изменения этой компоновки как изменения интерфейса, требующие пересборки зависимого кода. Не делайте вывод о стабильном бинарном интерфейсе из наличия явных инстанциаций. libc++ документирует конкретные гарантии совместимости и пределы конфигурации, и явно отличает их от гарантий для пользовательских функций. Гарантии ABI libc++
Что меняется, когда приложение Swift вызывает код C++?
Разделите два требования: Swift должен импортировать поддерживаемое объявление C++, а итоговый исполняемый файл должен скомпоновать его реализацию. Документация Swift предоставляет конкретные специализации шаблонов классов через псевдонимы типов C++; неинстанцированный шаблон сам по себе не доступен напрямую как тип Swift. Смешивание Swift и C++: Использование шаблонов классов
Для примера интерфейса ниже приведен необязательный фрагмент заголовка для вставки внутри namespace demo после определения класса. Он дает конкретному типу имя для импорта:
using IntOffset = Offset<int>;
Этот псевдоним называет тип. Явная инстанциация в Offset.cpp все еще предоставляет внестрочный конструктор и метод. Swift импортирует заголовки C++, используя модули Clang, и в настоящее время не может импортировать модули C++20. Следуйте настройке взаимодействия на основе заголовков для проекта. Смешивание Swift и C++
В Xcode включите C++ and Objective-C interoperability со значением C++ / Objective-C++ для цели Swift. Для цели приложения, использующего C++ из той же цели, включите заголовок C++ в свой bridging header. Цели фреймворков предоставляют поддерживаемые объявления через публичные заголовки. Настройка проектов со смешанными языками 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
- Гарантии ABI libc++ — Проект LLVM
- Смешивание Swift и C++ — Swift.org
- Настройка проектов со смешанными языками Swift и C++ — Swift.org
- Поддерживаемые функции и ограничения взаимодействия с C++ — Swift.org