Para mantener las implementaciones de plantillas de C++ en un archivo de código fuente separado en macOS, instancia explícitamente los tipos admitidos donde sus definiciones sean visibles y, después, enlaza ese archivo objeto con el ejecutable. Este recorrido crea un pequeño ejemplo local con las herramientas de C++ de Apple.
La tarea es específica: compilar un programa de C++ cuyos cuerpos de los métodos de plantilla se encuentran en un archivo .cpp. El ejemplo admite dos tipos concretos. En una sección posterior se explican los requisitos adicionales cuando una app de escritorio en Swift consume la misma interfaz de C++.
¿Por qué mover el código de plantilla a un archivo de código fuente rompe el enlazado?
Una plantilla describe una familia de tipos o funciones. Compilar su archivo de implementación no genera automáticamente todas las especializaciones posibles. Instanciar una clase tampoco instancia automáticamente todos los cuerpos de sus funciones miembro; por lo general, los miembros se instancian cuando se necesitan. Estas diferencias explican por qué un llamador puede compilar correctamente y, aun así, dejar referencias sin resolver durante el enlazado. Borrador de trabajo de C++: instanciación implícita
Considera un encabezado que define la disposición de una plantilla de clase y declara su constructor y sus métodos. Un llamador puede entender esas declaraciones. Sin embargo, si los cuerpos de los métodos solo existen en otro archivo de código fuente, el llamador no puede usar esos cuerpos para generar las funciones necesarias. El archivo de implementación debe proporcionar las instanciaciones concretas que necesita el llamador.
Una definición de instanciación explícita solicita esas instanciaciones. Una declaración extern template identifica una instanciación proporcionada en otro lugar. Las definiciones de la implementación deben estar disponibles en la unidad de traducción que proporciona la definición de instanciación explícita. Borrador de trabajo de C++: instanciación explícita
Elige la disposición según quién controle los argumentos de tipo:
| Necesidad del usuario del código | Disposición adecuada | Consecuencia para el mantenimiento |
|---|---|---|
| Los llamadores pueden proporcionar cualquier tipo adecuado | Coloca las definiciones en el encabezado o en un encabezado de implementación incluido desde este | Los llamadores pueden instanciar tipos nuevos |
| La biblioteca admite un conjunto conocido de tipos | Mantén los cuerpos en un archivo de código fuente e instancia explícitamente ese conjunto | Añade una instanciación cuando agregues un tipo admitido |
| Una app de escritorio solo necesita una operación concreta | Expón un contenedor ordinario de C++ alrededor de una especialización seleccionada | Mantén las decisiones sobre plantillas dentro de la implementación de C++ |
Esta guía de decisión se deriva de las reglas de instanciación anteriores. En este recorrido, el conjunto admitido se limita deliberadamente a int y double.
Crea un ejemplo completo de tres archivos
Usa una carpeta nueva y vacía y guarda juntos los tres archivos siguientes. El programa de ejemplo suma un desplazamiento a un valor; las entradas pequeñas facilitan la comprobación de los resultados esperados.
Prepara las herramientas de línea de comandos de Apple
Apple proporciona el compilador y el SDK de macOS a través de Xcode o del paquete independiente de herramientas de línea de comandos. Si no tienes ninguno instalado, abre Terminal y ejecuta el siguiente comando; luego completa el cuadro de diálogo del instalador. Si Xcode ya está instalado, no hace falta instalar las herramientas por separado. Instalación de las herramientas de línea de comandos
xcode-select --install
Define la interfaz pública
Guarda este encabezado completo como Offset.hpp. Mantén visible para los llamadores la definición de la clase, incluido su miembro de datos. Solo los cuerpos del constructor y del método se trasladan al archivo de código fuente.
#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
Las dos líneas extern template declaran las instanciaciones proporcionadas; no proporcionan los cuerpos de los miembros. Para esta disposición de instanciación explícita a nivel de clase, la definición de la plantilla de clase debe seguir siendo accesible. Borrador de trabajo de C++: instanciación explícita
Define los cuerpos y proporciona las instanciaciones
Guarda esta implementación completa como Offset.cpp. Coloca las definiciones de instanciación explícita después de las definiciones de los miembros.
#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
El orden importa: una definición de instanciación explícita a nivel de clase solo cubre los miembros definidos hasta ese punto. Tampoco instancia automáticamente las plantillas de miembros. Usa un único archivo de código fuente responsable de cada definición de instanciación explícita; el lenguaje permite como máximo una por plantilla y conjunto de argumentos en el programa. Instanciación explícita, Instanciación y especialización de plantillas: generalidades
Llama a los dos tipos admitidos
Guarda este llamador completo como 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;
}
El programa comprueba los resultados tanto mediante su código de salida como imprimiéndolos. Se revisó la coherencia de este ejemplo, pero no se compiló durante la preparación: el entorno de investigación no disponía de un compilador de C++ ni del SDK de macOS.
Compila ambos archivos de código fuente y luego enlaza los dos archivos objeto
En Terminal, cambia a la carpeta que contiene los tres archivos. Ejecuta estos comandos en orden y continúa solo si cada comando anterior se ejecuta correctamente:
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
Aquí, -c genera un archivo objeto sin enlazar. La invocación final del compilador omite -c y combina ambos objetos en un ejecutable. Los comandos seleccionan explícitamente C++17 y libc++ en lugar de depender de los valores predeterminados. Guía de comandos del compilador Clang
El resultado esperado, derivado de las entradas del ejemplo, es:
7
2.5
La dependencia esencial se ve en el último comando de compilación: main.o solicita las funciones y Offset.o proporciona sus instanciaciones. En un proyecto más grande, asegúrate de que se compile el destino que contiene Offset.cpp y de que su objeto o biblioteca llegue al paso de enlazado del ejecutable. Compilar ese archivo por sí solo no completa el enlazado.
No añadas #include "Offset.cpp" al llamador si también compilas ese archivo por separado. Para esta disposición, incluye el encabezado público y compila la implementación como una unidad de traducción independiente. Si los llamadores necesitan los cuerpos, elige deliberadamente la disposición con un encabezado de implementación incluido que aparece en la tabla anterior.
Diagnostica las instanciaciones ausentes en un orden útil
Primero identifica la función exacta sin resolver, incluidos sus argumentos de plantilla y calificadores. Luego sigue esta secuencia. Estas comprobaciones aplican la diferencia documentada entre generar archivos objeto y enlazarlos, junto con los requisitos de instanciación explícita. Etapas de compilación de Clang, Reglas de instanciación explícita de C++
| Qué encuentras | Qué comprobar o cambiar |
|---|---|
| Hay miembros sin resolver para ambos tipos admitidos | Confirma que Offset.o aparece en el comando de enlazado final |
| Falla un tipo recién usado | Añade la instanciación correspondiente al archivo de implementación responsable y la declaración al encabezado |
| Falta un método | Confirma que su cuerpo aparece antes de la instanciación de la clase y que su declaración coincide con la definición |
| Falta una plantilla de miembro | Proporciona por separado la instanciación necesaria de la plantilla de miembro, con su definición visible |
| Aparece un diagnóstico de definiciones duplicadas | Comprueba si hay definiciones de instanciación repetidas o si se compila e incluye un archivo de implementación |
| El diagnóstico menciona una arquitectura incompatible | Comprueba la arquitectura de todos los objetos y bibliotecas enlazados |
Por ejemplo, si cambias el llamador para usar demo::Offset<float>, introduces un tipo que esta implementación no proporciona. Para admitirlo, añade extern template class Offset<float>; dentro del espacio de nombres del encabezado y template class Offset<float>; después de los cuerpos en Offset.cpp; luego vuelve a compilar ambos objetos. Añadir solo la declaración no genera las funciones que faltan.
Si tienes instalado el ejecutable independiente llvm-nm de LLVM y está disponible en la ruta de comandos, estos comandos opcionales inspeccionan los objetos del ejemplo:
llvm-nm --demangle --undefined-only main.o
llvm-nm --demangle --defined-only Offset.o
Busca nombres coincidentes de miembros de demo::Offset entre los símbolos sin resolver del llamador y los símbolos definidos en la implementación. Es de esperar que aparezcan otras entradas, incluidas referencias a la biblioteca estándar. LLVM documenta ambos filtros y la desinicialización de nombres; estos comandos requieren específicamente llvm-nm, cuya disponibilidad no debe darse por sentada al instalar las herramientas de Apple. Guía de comandos de llvm-nm
Mantén la compatibilidad de arquitectura y ABI de macOS
La instanciación explícita proporciona código concreto para las plantillas; no hace que un archivo objeto sea utilizable con cualquier arquitectura. El flujo de trabajo de Apple para binarios universales compila y enlaza versiones específicas para cada arquitectura antes de combinarlas. Si la app admite tanto arm64 como x86_64, incluye el archivo de instanciación en ambas compilaciones. Creación de un binario universal de macOS
Para crear una versión universal del ejemplo, inspecciona el ejecutable resultante con el siguiente comando. Comprueba que las arquitecturas indicadas coincidan con la compilación prevista; los comandos de compilación normales anteriores no solicitan un binario universal.
xcrun lipo -archs ./template-demo
Mantener separados los cuerpos de los métodos también deja visible la disposición de la clase en el encabezado. Considera los cambios en esa disposición como cambios de interfaz que requieren volver a compilar el código dependiente. No deduzcas que existe una interfaz binaria estable por el hecho de que haya instanciaciones explícitas. libc++ documenta garantías de compatibilidad específicas y límites de configuración, y distingue explícitamente esas garantías de las que se aplican a las funciones definidas por el usuario. Garantías de ABI de libc++
¿Qué cambia cuando una app de Swift llama al código de C++?
Mantén separados dos requisitos: Swift debe poder importar una declaración de C++ compatible y el ejecutable final debe enlazar su implementación. La documentación de Swift indica que las especializaciones concretas de plantillas de clase se exponen mediante alias de tipos de C++; la plantilla sin instanciar no está disponible directamente como tipo de Swift. Combinación de Swift y C++: uso de plantillas de clase
Para la interfaz del ejemplo, el siguiente fragmento de encabezado es opcional y se inserta dentro de namespace demo, después de la definición de la clase. Da un nombre al tipo concreto para poder importarlo:
using IntOffset = Offset<int>;
Ese alias nombra el tipo. La instanciación explícita de Offset.cpp sigue proporcionando el constructor y el método definidos fuera de línea. Swift importa encabezados de C++ mediante módulos de Clang y, actualmente, no puede importar módulos de C++20. Sigue la configuración de interoperabilidad basada en encabezados para el proyecto. Combinación de Swift y C++
En Xcode, activa C++ and Objective-C interoperability con el valor C++ / Objective-C++ para el destino de Swift. Si un destino de app usa C++ desde ese mismo destino, incluye el encabezado de C++ en su encabezado puente. Los destinos de framework exponen las declaraciones compatibles mediante encabezados públicos. Configuración de proyectos de Swift y C++ con lenguajes combinados
El código que combina Swift y C++ debe usar la misma biblioteca estándar de C++; las plataformas de Apple usan libc++. La interoperabilidad requiere C++14 o una versión posterior. Ajusta el modo de lenguaje de los encabezados importados al de la compilación de C++ y comprueba la lista de características compatibles antes de exponer plantillas más complejas. La instanciación explícita no amplía las correspondencias de lenguaje compatibles con Swift. Características compatibles y limitaciones de la interoperabilidad de C++
Para un conjunto fijo de tipos, mantén juntas las declaraciones públicas, la lista de instanciaciones y las entradas del enlazado final. Cuando otro llamador necesite un tipo adicional, añádelo deliberadamente o pon la implementación a disposición para que el llamador pueda instanciarlo. Esa decisión mantiene clara la separación entre archivos de código fuente a medida que crece el proyecto de escritorio.
Fuentes
- Instanciación implícita — borrador de trabajo de C++
- Instanciación explícita — borrador de trabajo de C++
- Instanciación y especialización de plantillas: generalidades — borrador de trabajo de C++
- Instalación de las herramientas de línea de comandos — Apple
- clang — el compilador Clang de C, C++ y Objective-C — Proyecto LLVM
- llvm-nm — enumera la tabla de símbolos de archivos de código de bits y objetos de LLVM — Proyecto LLVM
- Creación de un binario universal de macOS — Apple
- Garantías de ABI de libc++ — Proyecto LLVM
- Combinación de Swift y C++ — Swift.org
- Configuración de proyectos de Swift y C++ con lenguajes combinados — Swift.org
- Características compatibles y limitaciones de la interoperabilidad de C++ — Swift.org