Declare the global with extern in a header, define it in exactly one .cpp file, and link that file’s object with every caller. This guide shows the complete macOS build, an address check, and when a C++17 inline variable is appropriate.

Why does a global in a header cause duplicate definitions?

For a small native Mac program, the task is to let several separately compiled source files read and modify the same object. Start by separating a declaration from a definition: extern int itemCount; declares an object, while int itemCount = 0; defines it. Adding an initializer to extern, as in extern int itemCount = 0;, also makes it a definition. The C++ working draft documents this distinction in Declarations and definitions.

A header containing an ordinary namespace-scope definition can introduce that definition into every source file that includes it. For a non-inline, non-template variable with external linkage, multiple definitions across translation units violate the one-definition rule. A linker may report duplicate symbols, but the language does not require every such violation to be diagnosed. See the working draft’s One-definition rule.

An include guard prevents repeated inclusion within one translation unit. It does not coordinate separately compiled source files. Thus, a guarded header can still cause multiple definitions across object files. Clang’s compiler command guide describes header expansion during preprocessing and the later linking of object files.

Build a complete shared-global example on your Mac

Use either Xcode or Apple’s Command Line Tools for Xcode package. Apple documents the standalone package as an alternative for development outside Xcode; a full Xcode installation already includes the tools. If neither is installed, run the following command and finish the installation dialog. See Installing the command-line tools.

xcode-select --install

Check the selected developer directory and compiler before building:

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

Apple’s archived Building from the Command Line with Xcode FAQ documents both inspecting the selected developer directory and using xcrun to locate or run developer tools. Keep the same selected toolchain throughout this example.

Create a new empty folder named shared-global-demo, open it in your editor, and save the following four files there. This is a synthetic, single-threaded C++ program: one file owns the global, another modifies it, and the entry point checks both its value and its identity.

Declare the object in the shared header

Save this as shared_state.hpp:

#ifndef SHARED_STATE_HPP
#define SHARED_STATE_HPP

namespace demo {
extern int itemCount;

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

#endif

The header supplies the declaration used by every source file. Keeping the namespace and type in one shared header also avoids maintaining separate handwritten declarations that can drift apart.

Give the object one definition

Save this as shared_state.cpp:

#include "shared_state.hpp"

namespace demo {
int itemCount = 0;
}

This is the sole definition of demo::itemCount. Including the header in its defining source file lets the compiler check that the definition agrees with the declaration. The explicit initializer makes the initial value clear.

Modify it from another translation unit

Save this as worker.cpp:

#include "shared_state.hpp"

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

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

Save this as 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;
}

The address comparison supplies a useful check beyond successful linking: worker.cpp returns the address it sees, and main.cpp compares it with its own. The exit status also checks the modified value. Neither check depends on assertions being enabled.

Compile separately, then link all three object files

In Terminal, change to the folder containing those four files. Run this complete build block:

(
    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 documents that -c produces an object file without linking; omitting a stage-selection option allows the driver to link. The explicit -std=c++17 selects the language mode. See the Clang compiler command guide. The subshell stops if a command fails, preventing a failed build from proceeding to run an older executable.

Expected output, derived from the source rather than captured from a Mac execution, is:

before: 0
after: 1
same object: true

The research environment did not provide a Mac compiler, so this example was inspected rather than executed. The three compilation commands deliberately expose the object-file boundary: shared_state.o supplies the definition, while worker.o and main.o use it.

When should you use a C++17 inline variable?

Choose a C++17 inline variable when the variable’s definition belongs in a shared header. Clang lists inline variables among its C++17 language features. The language specifies that an inline variable with external linkage may be defined in multiple translation units while remaining one entity with one address. See The inline specifier.

For the same example, replace the entire contents of shared_state.hpp with this version:

#ifndef SHARED_STATE_HPP
#define SHARED_STATE_HPP

namespace demo {
inline int itemCount = 0;

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

#endif

Keep worker.cpp and main.cpp unchanged. Remove shared_state.cpp from this variant’s build: its separate definition is no longer needed. Recompile both users and link only their object files:

(
    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
)

This variant has the same expected output. Do not include the old shared_state.o in its link command. Keeping obsolete object files out of the explicit link list makes the change in ownership unambiguous.

The inline definitions must satisfy the one-definition rule’s consistency requirements, including matching token sequences and corresponding name lookup. Avoid macros that produce different initializers in different source files. A successful link alone does not establish compliance. See One-definition rule.

Requirement Suitable approach Key condition
One implementation file owns mutable state Header declaration with extern; one .cpp definition Link the owning object exactly once
The shared definition belongs in a header C++17 inline variable Every consuming translation unit uses a compatible language mode and consistent definition
Each source file intentionally owns separate state Namespace-scope static or an unnamed namespace These create internal linkage, so they do not implement the shared-object task

The internal-linkage distinction follows the working draft’s Program and linkage. For this small multi-file program, the extern approach makes ownership especially easy to inspect.

Diagnose the failure before changing compiler flags

Use this sequence to distinguish an ownership problem from a missing build input. The checks follow the declaration rules and compilation stages used above; diagnostic wording can vary by toolchain.

  1. If compilation fails, check declarations first. Confirm that each source includes shared_state.hpp and uses the same namespace, type, and spelling. Linking cannot repair an undeclared name or conflicting declarations.
  2. If linking reports a duplicate symbol, count definitions and link inputs. Look for an initializer in the header, a second .cpp definition, or the same owning object supplied twice. For the inline variant, confirm that the old non-inline definition is absent.
  3. If linking reports an undefined symbol, find the missing owner. In the extern version, confirm that shared_state.cpp was compiled successfully and that shared_state.o appears in the final link command. A declaration alone does not supply the definition.
  4. If linking succeeds but the identity check fails, inspect linkage. Look for namespace-scope static variables or definitions inside unnamed namespaces. They give each translation unit its own object.
  5. After changing a header or ownership strategy, rebuild all affected objects. Repeat the complete applicable build block so callers and the defining file use the current declarations.

Two shortcuts deserve particular care. First, replacing the global with namespace-scope static int itemCount = 0; in the header creates separate per-translation-unit objects. Second, an ordinary namespace-scope const variable generally has internal linkage unless an exception applies, such as an explicit extern declaration or inline. Use the intended linkage deliberately; the relevant exceptions appear in Program and linkage.

Also preserve language linkage. extern "C" specifies C language linkage; it does not provide the missing definition or permit duplicate definitions. For this all-C++ example, retain the default C++ linkage. The working draft explains language linkage and its implementation-specific naming and calling-convention properties in Linkage specifications.

What changes when the Mac app also contains Swift?

The C++ storage arrangement still belongs in the C++ implementation. Importing a header into Swift supplies an interface; the build must also include the C++ implementation that owns a non-inline global. Swift’s mixed-language project setup guide describes exposing supported C++ APIs through framework public headers or an app target’s bridging header.

For an Xcode target that needs Swift to call C++, the documented setting is C++ and Objective-C interoperability, set to C++ / Objective-C++. Enable it for the consuming target and expose the appropriate header. This is a separate requirement from giving the global one definition. See Setting Up Mixed-Language Swift and C++ Projects.

Preserve the documented compatibility limits: Swift C++ interoperability requires Swift 5.9 or later, mixed Swift and C++ code must use the same C++ standard library, and Apple platforms use libc++. Importing a header containing this article’s inline variable requires selecting C++17 or newer for the importer as well as the C++ sources. Swift documents its language-standard controls and compiler constraints in Supported Features and Constraints of C++ Interoperability.

The examples here cover separately compiled C++ files linked into one native executable. Keep that scope when applying the address check; separately loaded frameworks and libraries require their own export and linking design. For the demonstrated task, identify the owning definition, supply it once to the link, and verify that both callers observe the same address and updated value.

Sources