Deklarasikan global dengan extern di dalam header, definisikan di tepat satu file .cpp, lalu tautkan objek file tersebut dengan setiap pemanggil. Panduan ini menunjukkan proses build macOS yang lengkap, pemeriksaan alamat, dan kapan variabel inline C++17 tepat digunakan.

Mengapa global di dalam header menyebabkan definisi ganda?

Untuk program Mac native sederhana, tujuannya adalah agar beberapa file sumber yang dikompilasi secara terpisah dapat membaca dan mengubah objek yang sama. Mulailah dengan membedakan deklarasi dari definisi: extern int itemCount; mendeklarasikan sebuah objek, sedangkan int itemCount = 0; mendefinisikannya. Menambahkan penginisialisasi ke extern, seperti pada extern int itemCount = 0;, juga menjadikannya definisi. Draf kerja C++ menjelaskan perbedaan ini dalam Deklarasi dan definisi.

Header yang berisi definisi biasa pada lingkup namespace dapat memasukkan definisi tersebut ke setiap file sumber yang menyertakannya. Untuk variabel non-inline dan non-template dengan linkage eksternal, beberapa definisi di berbagai unit translasi melanggar aturan satu definisi. Linker mungkin melaporkan simbol duplikat, tetapi bahasa ini tidak mewajibkan setiap pelanggaran semacam itu untuk didiagnosis. Lihat Aturan satu definisi dalam draf kerja.

Include guard mencegah penyertaan berulang dalam satu unit translasi. Namun, include guard tidak mengoordinasikan file sumber yang dikompilasi secara terpisah. Karena itu, header ber-guard tetap dapat menyebabkan definisi ganda di berbagai file objek. Panduan perintah compiler Clang menjelaskan perluasan header selama prapemrosesan dan penautan file objek pada tahap berikutnya.

Buat contoh global bersama yang lengkap di Mac Anda

Gunakan Xcode atau paket Command Line Tools for Xcode dari Apple. Apple mendokumentasikan paket mandiri tersebut sebagai alternatif untuk pengembangan di luar Xcode; instalasi Xcode penuh sudah menyertakan alat-alat tersebut. Jika keduanya belum terpasang, jalankan perintah berikut dan selesaikan dialog instalasi. Lihat Menginstal alat baris perintah.

xcode-select --install

Periksa direktori pengembang dan compiler yang dipilih sebelum melakukan build:

xcode-select --print-path
xcrun clang++ --version

FAQ Membangun dari Baris Perintah dengan Xcode dari arsip Apple mendokumentasikan cara memeriksa direktori pengembang yang dipilih dan menggunakan xcrun untuk menemukan atau menjalankan alat pengembang. Gunakan toolchain terpilih yang sama di seluruh contoh ini.

Buat folder kosong baru bernama shared-global-demo, buka di editor Anda, lalu simpan empat file berikut di sana. Ini adalah program C++ sintetis dengan satu thread: satu file memiliki global, file lain mengubahnya, dan titik masuk memeriksa nilai serta identitasnya.

Deklarasikan objek di header bersama

Simpan sebagai shared_state.hpp:

#ifndef SHARED_STATE_HPP
#define SHARED_STATE_HPP

namespace demo {
extern int itemCount;

void addItem();
int* itemCountAddress();
}

#endif

Header ini menyediakan deklarasi yang digunakan oleh setiap file sumber. Menyimpan namespace dan tipe dalam satu header bersama juga menghindari pemeliharaan deklarasi terpisah yang ditulis secara manual dan dapat berbeda.

Berikan objek satu definisi

Simpan sebagai shared_state.cpp:

#include "shared_state.hpp"

namespace demo {
int itemCount = 0;
}

Inilah satu-satunya definisi demo::itemCount. Menyertakan header di file sumber yang mendefinisikannya memungkinkan compiler memeriksa kesesuaian definisi dengan deklarasi. Penginisialisasi eksplisit memperjelas nilai awalnya.

Ubah dari unit translasi lain

Simpan sebagai worker.cpp:

#include "shared_state.hpp"

namespace demo {
void addItem() {
    ++itemCount;
}

int* itemCountAddress() {
    return &itemCount;
}
}

Simpan sebagai main.cpp:

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

int main() {
    std::cout << "before: " << demo::itemCount << '\n';
    demo::addItem();

    const bool sameObject =
        demo::itemCountAddress() == &demo::itemCount;

    std::cout << "after: " << demo::itemCount << '\n';
    std::cout << std::boolalpha
              << "same object: " << sameObject << '\n';

    return (demo::itemCount == 1 && sameObject) ? 0 : 1;
}

Perbandingan alamat memberikan pemeriksaan yang berguna selain keberhasilan penautan: worker.cpp mengembalikan alamat yang dilihatnya, dan main.cpp membandingkannya dengan alamat yang dilihatnya sendiri. Status keluarnya juga memeriksa nilai yang telah diubah. Kedua pemeriksaan ini tidak bergantung pada aktifnya assertion.

Kompilasi secara terpisah, lalu tautkan ketiga file objek

Di Terminal, pindah ke folder yang berisi keempat file tersebut. Jalankan blok build lengkap berikut:

(
    set -e
    xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
        -c shared_state.cpp -o shared_state.o
    xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
        -c worker.cpp -o worker.o
    xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
        -c main.cpp -o main.o
    xcrun clang++ shared_state.o worker.o main.o -o shared-demo
    ./shared-demo
)

Clang mendokumentasikan bahwa -c menghasilkan file objek tanpa melakukan penautan; jika opsi pemilihan tahap tidak dicantumkan, driver akan melakukan penautan. Opsi -std=c++17 secara eksplisit memilih mode bahasa. Lihat Panduan perintah compiler Clang. Subshell akan berhenti jika sebuah perintah gagal, sehingga build yang gagal tidak akan dilanjutkan dengan menjalankan executable lama.

Keluaran yang diharapkan, disimpulkan dari kode sumber dan bukan hasil yang direkam dari eksekusi di Mac, adalah:

before: 0
after: 1
same object: true

Lingkungan riset tidak menyediakan compiler Mac, jadi contoh ini diperiksa tanpa dijalankan. Ketiga perintah kompilasi tersebut sengaja memperlihatkan batas antarfile objek: shared_state.o menyediakan definisi, sedangkan worker.o dan main.o menggunakannya.

Kapan sebaiknya menggunakan variabel inline C++17?

Pilih variabel inline C++17 jika definisi variabel tersebut seharusnya berada di header bersama. Clang mencantumkan variabel inline di antara fitur bahasa C++17. Bahasa ini menetapkan bahwa variabel inline dengan linkage eksternal dapat didefinisikan di beberapa unit translasi dan tetap menjadi satu entitas dengan satu alamat. Lihat Specifier inline.

Untuk contoh yang sama, ganti seluruh isi shared_state.hpp dengan versi berikut:

#ifndef SHARED_STATE_HPP
#define SHARED_STATE_HPP

namespace demo {
inline int itemCount = 0;

void addItem();
int* itemCountAddress();
}

#endif

Biarkan worker.cpp dan main.cpp tetap sama. Hapus shared_state.cpp dari build varian ini: definisi terpisahnya tidak lagi diperlukan. Kompilasi ulang kedua pengguna lalu tautkan hanya file objek mereka:

(
    set -e
    xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
        -c worker.cpp -o worker.o
    xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
        -c main.cpp -o main.o
    xcrun clang++ worker.o main.o -o shared-demo
    ./shared-demo
)

Varian ini menghasilkan keluaran yang sama seperti yang diharapkan. Jangan sertakan shared_state.o lama dalam perintah penautannya. Dengan mengeluarkan file objek usang dari daftar taut yang eksplisit, perubahan kepemilikan menjadi jelas.

Definisi inline harus memenuhi persyaratan konsistensi aturan satu definisi, termasuk urutan token yang sama dan pencarian nama yang bersesuaian. Hindari makro yang menghasilkan penginisialisasi berbeda di file sumber yang berbeda. Keberhasilan penautan saja tidak membuktikan kepatuhan. Lihat Aturan satu definisi.

Persyaratan Pendekatan yang sesuai Ketentuan utama
Satu file implementasi memiliki state yang dapat diubah Deklarasi header dengan extern; satu definisi .cpp Tautkan objek pemilik tepat satu kali
Definisi bersama seharusnya berada di header Variabel inline C++17 Setiap unit translasi yang menggunakannya memakai mode bahasa yang kompatibel dan definisi yang konsisten
Setiap file sumber memang harus memiliki state terpisah static pada lingkup namespace atau namespace tanpa nama Keduanya menghasilkan linkage internal, sehingga tidak memenuhi tugas berbagi objek

Perbedaan linkage internal ini mengikuti Program dan linkage dalam draf kerja. Untuk program multi-file kecil ini, pendekatan extern membuat kepemilikan sangat mudah diperiksa.

Diagnosis kegagalan sebelum mengubah flag compiler

Gunakan urutan ini untuk membedakan masalah kepemilikan dari input build yang hilang. Pemeriksaan ini mengikuti aturan deklarasi dan tahapan kompilasi yang digunakan di atas; redaksi diagnostik dapat berbeda antar-toolchain.

  1. Jika kompilasi gagal, periksa deklarasi terlebih dahulu. Pastikan setiap file sumber menyertakan shared_state.hpp dan menggunakan namespace, tipe, serta ejaan yang sama. Penautan tidak dapat memperbaiki nama yang tidak dideklarasikan atau deklarasi yang bertentangan.
  2. Jika linker melaporkan simbol duplikat, hitung definisi dan input penautan. Cari penginisialisasi di header, definisi .cpp kedua, atau objek pemilik yang disertakan dua kali. Untuk varian inline, pastikan definisi non-inline yang lama sudah tidak ada.
  3. Jika linker melaporkan simbol yang tidak terdefinisi, cari pemilik yang hilang. Pada versi extern, pastikan shared_state.cpp berhasil dikompilasi dan shared_state.o tercantum dalam perintah penautan akhir. Deklarasi saja tidak menyediakan definisinya.
  4. Jika penautan berhasil tetapi pemeriksaan identitas gagal, periksa linkage. Cari variabel static pada lingkup namespace atau definisi di dalam namespace tanpa nama. Keduanya memberi setiap unit translasi objeknya sendiri.
  5. Setelah mengubah header atau strategi kepemilikan, build ulang semua objek yang terdampak. Ulangi blok build lengkap yang sesuai agar file pemanggil dan file pendefinisi menggunakan deklarasi terkini.

Dua jalan pintas perlu diperhatikan. Pertama, mengganti global dengan static int itemCount = 0; pada lingkup namespace di dalam header akan membuat objek terpisah untuk setiap unit translasi. Kedua, variabel const biasa pada lingkup namespace umumnya memiliki linkage internal, kecuali jika berlaku pengecualian, seperti deklarasi extern eksplisit atau inline. Gunakan linkage yang dimaksud secara sengaja; pengecualian terkait dijelaskan dalam Program dan linkage.

Pertahankan juga linkage bahasa. extern "C" menetapkan linkage bahasa C; ini tidak menyediakan definisi yang hilang atau mengizinkan definisi ganda. Untuk contoh yang seluruhnya menggunakan C++, pertahankan linkage C++ bawaan. Draf kerja menjelaskan linkage bahasa serta properti penamaan dan konvensi pemanggilan yang bergantung pada implementasi dalam Spesifikasi linkage.

Apa yang berubah jika aplikasi Mac juga berisi Swift?

Pengaturan penyimpanan C++ tetap menjadi bagian dari implementasi C++. Mengimpor header ke Swift menyediakan antarmuka; build juga harus menyertakan implementasi C++ yang memiliki global non-inline. Panduan penyiapan proyek bahasa campuran Swift menjelaskan cara mengekspos API C++ yang didukung melalui header publik framework atau bridging header target aplikasi.

Untuk target Xcode yang memerlukan Swift untuk memanggil C++, pengaturan yang didokumentasikan adalah Interoperabilitas C++ dan Objective-C, dengan nilai C++ / Objective-C++. Aktifkan pengaturan ini untuk target yang menggunakan C++ dan ekspos header yang sesuai. Ini merupakan persyaratan terpisah dari pemberian satu definisi pada global. Lihat Menyiapkan Proyek Swift dan C++ dengan Bahasa Campuran.

Pertahankan batas kompatibilitas yang didokumentasikan: interoperabilitas C++ Swift memerlukan Swift 5.9 atau yang lebih baru, kode Swift dan C++ campuran harus menggunakan pustaka standar C++ yang sama, dan platform Apple menggunakan libc++. Mengimpor header yang berisi variabel inline dalam artikel ini mengharuskan importer dan sumber C++ memilih C++17 atau yang lebih baru. Swift mendokumentasikan kontrol standar bahasa dan batasan compilernya dalam Fitur yang Didukung dan Batasan Interoperabilitas C++.

Contoh di sini mencakup file C++ yang dikompilasi secara terpisah lalu ditautkan ke satu executable native. Pertahankan cakupan tersebut saat menerapkan pemeriksaan alamat; framework dan pustaka yang dimuat secara terpisah memerlukan rancangan ekspor dan penautannya sendiri. Untuk tugas yang diperagakan, tentukan definisi pemiliknya, sertakan tepat satu kali dalam penautan, lalu verifikasi bahwa kedua pemanggil melihat alamat dan nilai yang diperbarui yang sama.

Sumber