Para manter as implementações de templates C++ em um arquivo-fonte separado no macOS, instancie explicitamente os tipos compatíveis onde as definições estiverem visíveis e, em seguida, vincule esse arquivo-objeto ao executável. Este passo a passo cria um pequeno exemplo local com as ferramentas C++ da Apple.

A tarefa é específica: compilar um programa C++ cujos corpos dos métodos do template ficam em um arquivo .cpp. O exemplo dá suporte a dois tipos concretos. Uma seção posterior explica os requisitos adicionais quando um app de desktop Swift usa a mesma interface C++.

Por que mover o código de template para um arquivo-fonte causa erros de vinculação?

Um template descreve uma família de tipos ou funções. Compilar seu arquivo de implementação não produz automaticamente todas as especializações possíveis. A instanciação de uma classe também não instancia automaticamente o corpo de cada função membro; em geral, os membros são instanciados quando necessário. Essas distinções explicam por que um chamador pode compilar com êxito e ainda assim deixar referências sem resolução na etapa de vinculação. Rascunho de trabalho do C++: instanciação implícita

Considere um cabeçalho que define o layout de um template de classe e declara seu construtor e seus métodos. Um chamador consegue entender essas declarações. Porém, se os corpos dos métodos existirem apenas em outro arquivo-fonte, o chamador não poderá usar esses corpos para gerar as funções necessárias. O arquivo de implementação precisa fornecer as instanciações concretas de que o chamador necessita.

Uma definição de instanciação explícita solicita essas instanciações. Uma declaração extern template identifica uma instanciação fornecida em outro lugar. As definições da implementação precisam estar disponíveis na unidade de tradução que fornece a definição da instanciação explícita. Rascunho de trabalho do C++: instanciação explícita

Escolha a organização de acordo com quem controla os argumentos de tipo:

Requisito de quem usa Organização adequada Consequência para a manutenção
Os chamadores podem fornecer tipos adequados arbitrários Coloque as definições no cabeçalho ou em um cabeçalho de implementação incluído por ele Os chamadores podem instanciar novos tipos
A biblioteca dá suporte a um conjunto conhecido de tipos Mantenha os corpos em um arquivo-fonte e instancie explicitamente esse conjunto Adicione uma instanciação ao incluir um tipo compatível
Um app de desktop precisa apenas de uma operação concreta Exponha um wrapper C++ comum em torno de uma especialização selecionada Mantenha as escolhas de template dentro da implementação C++

Este guia de decisão decorre das regras de instanciação acima. Neste passo a passo, o conjunto compatível é intencionalmente limitado a int e double.

Crie um exemplo completo com três arquivos

Use uma pasta nova e vazia e salve os três arquivos a seguir nela. O programa fictício soma um deslocamento a um valor; as entradas pequenas facilitam a conferência dos resultados esperados.

Prepare as ferramentas de linha de comando da Apple

A Apple fornece o compilador e o SDK do macOS pelo Xcode ou pelo pacote independente Command Line Tools. Se nenhum deles estiver instalado, abra o Terminal e execute o comando a seguir; depois, conclua o diálogo do instalador. Não é necessário instalar as ferramentas separadamente se o Xcode já estiver instalado. Instalar as ferramentas de linha de comando

xcode-select --install

Defina a interface pública

Salve este cabeçalho completo como Offset.hpp. Mantenha a definição da classe, incluindo seu membro de dados, visível para os chamadores. Somente os corpos do construtor e dos métodos são movidos para o arquivo-fonte.

#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

As duas linhas extern template declaram as instanciações fornecidas; elas não fornecem os corpos dos membros. A definição do template de classe precisa continuar acessível para este arranjo de instanciação explícita no nível da classe. Rascunho de trabalho do C++: instanciação explícita

Defina os corpos e forneça as instanciações

Salve esta implementação completa como Offset.cpp. Coloque as definições de instanciação explícita após as definições dos membros.

#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

A ordem importa: uma definição de instanciação explícita no nível da classe abrange apenas os membros definidos até aquele ponto. Ela também não instancia automaticamente templates de membros. Mantenha um único arquivo-fonte responsável por cada definição de instanciação explícita; a linguagem permite no máximo uma por template e conjunto de argumentos no programa. Instanciação explícita, Instanciação e especialização de templates: geral

Chame os dois tipos compatíveis

Salve este chamador 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;
}

O programa verifica os resultados pelo código de saída e também os imprime. Este exemplo foi inspecionado quanto à consistência, mas não foi compilado durante a preparação: o ambiente de pesquisa não tinha compilador C++ nem SDK do macOS.

Compile os dois arquivos-fonte e, depois, vincule os dois arquivos-objeto

No Terminal, vá até a pasta que contém os três arquivos. Execute estes comandos na ordem indicada e prossiga apenas se cada comando anterior for concluído com êxito:

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

Aqui, -c produz um arquivo-objeto sem fazer a vinculação. A invocação final do compilador omite -c e combina os dois objetos em um executável. Os comandos selecionam explicitamente C++17 e libc++, em vez de depender dos valores padrão. Guia de comandos do compilador Clang

A saída esperada, derivada das entradas fictícias, é:

7
2.5

A dependência essencial aparece no último comando de compilação: main.o solicita as funções, e Offset.o fornece suas instanciações. Em um projeto maior, garanta que o destino que contém Offset.cpp seja compilado e que seu objeto ou sua biblioteca chegue à etapa de vinculação do executável. Compilar esse arquivo isoladamente não conclui a vinculação.

Não adicione #include "Offset.cpp" ao chamador se também estiver compilando esse arquivo separadamente. Neste arranjo, inclua o cabeçalho público e compile a implementação como uma unidade de tradução própria. Se os chamadores precisarem dos corpos, escolha deliberadamente o arranjo com cabeçalho de implementação incluído, apresentado na tabela anterior.

Diagnostique instanciações ausentes em uma ordem útil

Primeiro, identifique a função exata sem resolução, incluindo seus argumentos de template e qualificadores. Em seguida, siga esta sequência. As verificações aplicam a distinção documentada entre gerar arquivos-objeto e vinculá-los, juntamente com os requisitos de instanciação explícita. Etapas de compilação do Clang, Regras de instanciação explícita do C++

O que você encontra O que verificar ou alterar
Há membros sem resolução para os dois tipos compatíveis Confirme que Offset.o aparece no comando final de vinculação
O uso de um tipo novo falha Adicione a instanciação correspondente ao arquivo de implementação responsável e a declaração ao cabeçalho
Falta um método Confirme que seu corpo precede a instanciação da classe e que sua declaração corresponde à definição
Falta um template de membro Forneça separadamente a instanciação necessária do template de membro, com sua definição visível
Aparece um diagnóstico de definição duplicada Verifique se há definições de instanciação repetidas ou se um arquivo de implementação foi compilado e incluído
O diagnóstico menciona uma arquitetura incompatível Verifique a arquitetura de cada objeto e biblioteca vinculados

Por exemplo, alterar o chamador para usar demo::Offset<float> introduz um tipo que esta implementação não fornece. Para dar suporte a ele, adicione extern template class Offset<float>; dentro do namespace do cabeçalho e template class Offset<float>; após os corpos em Offset.cpp; depois, recompile os dois objetos. Apenas adicionar a declaração não gera as funções ausentes.

Se o executável separado llvm-nm do LLVM estiver instalado e disponível no caminho de comandos, estes comandos opcionais inspecionam os objetos do exemplo:

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

Procure nomes de membros demo::Offset correspondentes nos símbolos sem resolução do chamador e nos símbolos definidos pela implementação. Outras entradas são esperadas, inclusive referências à biblioteca padrão. O LLVM documenta os dois filtros e a desmanglagem de nomes; estes comandos exigem especificamente llvm-nm, cuja disponibilidade não deve ser presumida a partir de uma instalação das ferramentas da Apple. Guia de comandos do llvm-nm

Preserve a compatibilidade de arquitetura e ABI do macOS

A instanciação explícita fornece código de template concreto; ela não torna um arquivo-objeto utilizável em todas as arquiteturas. O fluxo de trabalho da Apple para binários universais compila e vincula versões específicas de cada arquitetura antes de combiná-las. Se o app for compatível com arm64 e x86_64, inclua o arquivo de instanciação em ambas as compilações. Criar um binário universal do macOS

Para uma compilação universal do exemplo, inspecione o executável resultante com o comando a seguir. Confira se as arquiteturas informadas correspondem à compilação pretendida; os comandos de compilação comuns anteriores não solicitam um binário universal.

xcrun lipo -archs ./template-demo

Manter os corpos dos métodos separados também deixa o layout da classe visível no cabeçalho. Trate alterações nesse layout como mudanças de interface que exigem recompilar o código dependente. Não deduza que há uma interface binária estável apenas pela presença de instanciações explícitas. A libc++ documenta garantias específicas e limites de configuração, e distingue explicitamente essas garantias das garantias sobre funções definidas pelo usuário. Garantias de ABI da libc++

O que muda quando um app Swift chama o código C++?

Mantenha separados dois requisitos: o Swift precisa importar uma declaração C++ compatível, e o executável final precisa vincular sua implementação. A documentação do Swift expõe especializações concretas de templates de classe por meio de aliases de tipo C++; o próprio template não instanciado não está diretamente disponível como tipo Swift. Misturando Swift e C++: usando templates de classe

Para a interface de exemplo, o trecho a seguir é um fragmento opcional de cabeçalho a ser inserido dentro de namespace demo, após a definição da classe. Ele dá um nome ao tipo concreto para que possa ser importado:

using IntOffset = Offset<int>;

Esse alias nomeia o tipo. A instanciação explícita em Offset.cpp ainda fornece o construtor e o método definidos fora da classe. O Swift importa cabeçalhos C++ usando módulos Clang e atualmente não consegue importar módulos C++20. Siga a configuração de interoperabilidade baseada em cabeçalhos para o projeto. Misturando Swift e C++

No Xcode, habilite C++ and Objective-C interoperability com o valor C++ / Objective-C++ para o destino Swift. Em um destino de app que usa C++ a partir desse mesmo destino, inclua o cabeçalho C++ no cabeçalho de bridging. Destinos de framework expõem declarações compatíveis por meio de cabeçalhos públicos. Configurando projetos Swift e C++ com linguagens mistas

O código misto Swift e C++ precisa usar a mesma biblioteca padrão C++; nas plataformas Apple, usa-se libc++. A interoperabilidade exige C++14 ou posterior. Ajuste o modo da linguagem dos cabeçalhos importados para corresponder à compilação C++ e consulte a lista de recursos compatíveis antes de expor templates mais elaborados. A instanciação explícita não amplia os mapeamentos de linguagem compatíveis com Swift. Recursos compatíveis e limitações da interoperabilidade com C++

Para um conjunto fixo de tipos, mantenha juntas as declarações públicas, a lista de instanciações e as entradas da vinculação final. Quando um novo chamador precisar de outro tipo, adicione esse tipo deliberadamente ou disponibilize a implementação para instanciação no lado do chamador. Essa escolha mantém clara a separação entre arquivos-fonte à medida que o projeto de desktop cresce.

Fontes