Per creare un eseguibile nativo per Mac a partire da più file C++, passa a clang++ ogni file di implementazione necessario oppure compilali in file oggetto e collegali tra loro. Questa guida crea un piccolo programma da Terminale e spiega come diagnosticare le definizioni mancanti.

Preparare gli strumenti di compilazione per Mac

Usa questa procedura per un piccolo programma C++ che vuoi eseguire localmente da Terminale. Apple fornisce il compilatore e l’SDK di macOS tramite Xcode o il pacchetto autonomo Command Line Tools. Se hai già installato Xcode, non ti serve il pacchetto separato. Altrimenti, esegui il comando seguente e completa la procedura di installazione. Consulta Installing the command-line tools di Apple.

xcode-select --install

Al termine dell’installazione, verifica che il compilatore sia disponibile:

clang++ --version

Usa clang++ per il collegamento finale, anche quando gli input sono già file oggetto compilati. La documentazione di Clang consiglia esplicitamente questo driver per i programmi C++, perché la modalità del linguaggio determina quali librerie di runtime vengono collegate automaticamente. Consulta Assembling a Complete Toolchain.

Creare un programma C++ completo composto da tre file

L’esempio fittizio conta gli oggetti su tre vassoi da scrivania e formatta il risultato. Contiene un punto di ingresso e due file di implementazione di supporto. Crea una nuova directory di lavoro vuota e salva al suo interno questi quattro file:

desk-demo/
  desk.hpp
  main.cpp
  label.cpp
  totals.cpp

Salva le dichiarazioni condivise in desk.hpp:

#ifndef DESK_DEMO_DESK_HPP
#define DESK_DEMO_DESK_HPP

#include <string>

std::string make_label(int count);
int total_items(int trays, int per_tray);

#endif

Salva il punto di ingresso in main.cpp:

#include "desk.hpp"
#include <iostream>

int main() {
    const int count = total_items(3, 4);
    std::cout << make_label(count) << '\n';
    return 0;
}

Salva l’implementazione della formattazione in label.cpp:

#include "desk.hpp"

std::string make_label(int count) {
    return "Desk items: " + std::to_string(count);
}

Salva l’implementazione del conteggio in totals.cpp:

#include "desk.hpp"

int total_items(int trays, int per_tray) {
    return trays * per_tray;
}

Ogni file di implementazione include lo stesso header, così il compilatore può verificare che la definizione della funzione corrisponda alla dichiarazione condivisa. La protezione dell’header evita inclusioni ripetute all’interno di una stessa unità di traduzione. Mantieni i corpi di queste normali funzioni non inline nei rispettivi file di implementazione; il C++ impone requisiti sulle definizioni nell’intero programma tramite la sua regola della definizione unica.

Compilare e collegare con un solo comando

Apri Terminale nella directory che contiene i quattro file ed esegui:

clang++ -std=c++17 main.cpp label.cpp totals.cpp -o desk-demo && ./desk-demo

L’output atteso è:

Desk items: 12

Il comando specifica tutti e tre i file di implementazione e imposta il nome del file di output con -o. Poiché non contiene opzioni che interrompono le fasi, Clang compila gli input e avvia il linker. L’opzione esplicita -std=c++17 seleziona la versione del linguaggio invece di affidarsi a quella predefinita del compilatore. Queste opzioni sono documentate nella guida ai comandi del compilatore Clang.

La shell esegue il programma solo se la compilazione riesce, perché i comandi sono uniti con &&. Questo è importante quando ricompili: se la compilazione fallisce, non vuoi eseguire per sbaglio un vecchio eseguibile e scambiare il suo output per il nuovo risultato.

Quando è appropriato usare un carattere jolly?

Per questa specifica directory, il comando seguente è un’alternativa all’elenco esplicito dei sorgenti:

clang++ -std=c++17 ./*.cpp -o desk-demo && ./desk-demo

La shell espande ./*.cpp nei file corrispondenti nella directory corrente. Non cerca nelle sottocartelle e non include i file che terminano in .cc o .cxx. Se specifichi esplicitamente percorsi che contengono spazi, racchiudili tra virgolette.

Usa il carattere jolly solo finché ogni file corrispondente appartiene a questo eseguibile. Se in seguito aggiungi un programma dimostrativo separato con un proprio main, oppure un’implementazione alternativa specifica per una piattaforma, mantieni un elenco esplicito dei sorgenti. In questo caso, “tutti i file di implementazione” significa tutti i file necessari per un singolo target, con ogni definizione richiesta fornita nel modo appropriato.

Compilare prima i file oggetto e poi collegarli

Separare le fasi facilita l’individuazione del punto in cui si verifica un errore: durante la compilazione di un file sorgente o quando si combinano i componenti del programma. Usando gli stessi quattro file e la stessa directory di lavoro, esegui:

(
    set -e
    mkdir -p build
    clang++ -std=c++17 -c main.cpp -o build/main.o
    clang++ -std=c++17 -c label.cpp -o build/label.o
    clang++ -std=c++17 -c totals.cpp -o build/totals.o
    clang++ build/main.o build/label.o build/totals.o -o build/desk-demo
    ./build/desk-demo
)

L’opzione -c produce un file oggetto senza eseguire il collegamento. L’invocazione finale del compilatore riceve i tre file oggetto e collega l’eseguibile. Questo segue le fasi di compilazione documentate da Clang. set -e nella subshell interrompe questa sequenza lineare se un comando fallisce.

Anche qui usa nomi espliciti per i file oggetto. Collegare tutti gli oggetti presenti in una directory di compilazione riutilizzata può includere per sbaglio un file obsoleto di una versione precedente del programma. Se due file sorgente in directory diverse hanno lo stesso nome base, assegna ai rispettivi file oggetto percorsi distinti invece di scriverli entrambi nello stesso file di output.

Se modifichi solo label.cpp, puoi ricompilare quel file oggetto e ripetere il comando di collegamento. Se modifichi desk.hpp, ricompila tutti i sorgenti che lo includono. Questa procedura manuale richiede di tenere traccia di queste relazioni; quando il progetto cresce, un sistema di build diventa più pratico.

Diagnosticare l’errore prima di modificare il comando di compilazione

La sequenza seguente applica a questo esempio le fasi di compilazione e le regole C++ sulle definizioni. La formulazione esatta dei messaggi diagnostici varia in base alla toolchain installata.

Sintomo Primo controllo Passo successivo appropriato
Viene segnalato che manca un header La sintassi dell’include e la posizione dell’header Ripristina la struttura mostrata oppure configura la directory di ricerca degli header corretta.
make_label o total_items non è definita durante il collegamento Verifica se la sua implementazione è tra gli input Aggiungi al collegamento il file .cpp mancante o il relativo file oggetto compilato.
La funzione risulta ancora indefinita nonostante l’inclusione del suo file Le firme della dichiarazione e della definizione Confronta namespace, tipi dei parametri e qualificatori; includi l’header condiviso nell’implementazione.
Compaiono simboli duplicati Presenza ripetuta di input di implementazione Rimuovi un oggetto ripetuto, un .cpp incluso o una definizione alternativa non intenzionale.
main non è definito oppure è duplicato I file del punto di ingresso Includi esattamente una volta il file main.cpp previsto ed escludi i punti di ingresso di altri programmi.
I simboli della libreria C++ non sono definiti Il driver usato per il collegamento finale Usa clang++ affinché vengano fornite le librerie runtime C++.

Per esempio, compilare solo main.cpp può riuscire perché l’header dichiara entrambe le funzioni. Il collegamento finale ha comunque bisogno delle loro definizioni. Aggiungere un’altra inclusione dell’header non fornisce l’oggetto di implementazione mancante. I requisiti sottostanti sono spiegati dalla regola della definizione unica del C++.

Evita di correggere i simboli mancanti includendo label.cpp dentro main.cpp e compilando contemporaneamente label.cpp in modo indipendente. Così la stessa definizione di una normale funzione viene fornita da due unità di traduzione. L’header condiviso e gli input di implementazione separati forniscono già la struttura prevista.

Per esaminare i comandi degli strumenti senza eseguire la compilazione, usa l’opzione -### documentata da Clang. Racchiudila tra virgolette, così la shell la passa senza modificarla:

clang++ '-###' -std=c++17 main.cpp label.cpp totals.cpp -o desk-demo

Questo mostra le invocazioni del compilatore e del linker selezionate dal driver e aiuta a distinguere il comando che intendevi eseguire da quello che la toolchain lancerebbe. Consulta Assembling a Complete Toolchain.

Mantenere compatibili architettura, piattaforma e ABI

Aggiungere tutti i file sorgente non può correggere input binari incompatibili. Per una compilazione locale, parti da una toolchain Apple installata e ricompila insieme i sorgenti del programma. Quando aggiungi librerie precompilate, controlla le piattaforme e le architetture supportate prima di modificare gli elenchi dei sorgenti.

Per creare facoltativamente un eseguibile universale, Apple documenta la compilazione e il collegamento separati per ogni architettura, seguiti dall’unione degli eseguibili finiti con lipo. Con una toolchain e un SDK che supportano entrambi i target, lo stesso esempio può essere compilato così. I target di distribuzione macOS 11 indicati sotto sono scelte a titolo esemplificativo:

(
    set -e
    mkdir -p build
    clang++ -std=c++17 -target arm64-apple-macos11 \
        main.cpp label.cpp totals.cpp -o build/desk-arm64
    clang++ -std=c++17 -target x86_64-apple-macos11 \
        main.cpp label.cpp totals.cpp -o build/desk-x86_64
    lipo -create build/desk-arm64 build/desk-x86_64 \
        -output build/desk-universal
    lipo -archs build/desk-universal
)

Ogni dipendenza collegata deve fornire un input compatibile per la compilazione della rispettiva architettura. Un binario iOS non diventa un binario macOS solo perché entrambi usano arm64. Apple illustra le sezioni per architettura, le differenze tra piattaforme e l’ispezione degli eseguibili in Building a universal macOS binary.

Anche la versione del linguaggio C++ è distinta dalla compatibilità binaria, o ABI. La sola corrispondenza di -std=c++17 non garantisce che librerie compilate separatamente abbiano configurazioni e layout compatibili. LLVM documenta i flag di configurazione della libreria che influenzano l’ABI e afferma che gli utenti non dovrebbero impostare autonomamente quei flag del fornitore. Per questo compito, la scelta pratica è mantenere la configurazione normale della libreria della toolchain. Consulta libc++’s ABI Guarantees.

Usare lo stesso elenco di sorgenti in Xcode o in un progetto misto Swift

In un target Xcode esistente, seleziona il target e apri Build Phases. In Compile Sources, verifica che siano presenti i file di implementazione necessari per quell’eseguibile. Aggiungi le dipendenze da librerie personalizzate tramite Link Binary with Libraries. Apple documenta queste responsabilità distinte in Customizing the build phases of a target.

Per l’esempio, l’elenco dei sorgenti deve includere main.cpp, label.cpp e totals.cpp. Aggiungere l’header al progetto non sostituisce la compilazione delle implementazioni.

Se l’applicazione circostante usa Swift, i comandi C++ puri qui sopra riguardano solo il programma C++. Il percorso documentato da Swift richiede di abilitare l’interoperabilità C++ per esporre API tra i due linguaggi; solo le funzionalità supportate possono attraversare quel confine. Consulta Mixing Swift and C++.

Il codice misto Swift e C++ deve usare la stessa libreria standard C++; sulle piattaforme Apple, Swift usa libc++. Swift documenta anche i vincoli relativi al compilatore e alla versione del linguaggio, tra cui l’obbligo di usare C++14 o versioni successive per l’interoperabilità. Verifica questi requisiti prima di considerare una libreria esistente una dipendenza compatibile. Consulta Supported Features and Constraints of C++ Interoperability.

Per il lavoro locale in Terminale, inizia con la compilazione esplicita dei tre sorgenti. Se fallisce, individua la fase in cui si verifica l’errore, controlla la definizione mancante o ripetuta e verifica che gli input binari siano compatibili. Una volta che il comando produce il risultato atteso, avrai un elenco concreto dei sorgenti da usare in una compilazione più ampia.

Fonti