In modern C++, we often rely on static_assert to validate assumptions at compile time. However, you might encounter a surprising compiler discrepancy when comparing the addresses of two extern const variables of an incomplete type.

While Clang and MSVC happily compile such comparisons, GCC rejects them with an error indicating a non-constant condition. In this article, we will dive deep into the C++ standard to determine whether GCC's behavior is a conforming restriction or simply a compiler bug.

The Code in Question

Consider the following minimal reproducible example:

struct A;

extern const A a1;
extern const A a2;

static_assert(&a1 != &a2);

If you compile this with GCC, you will receive an error similar to:

error: non-constant condition for static assertion
static_assert(&a1 != &a2);
              ~~~~^~~~~~~

However, if you complete the type struct A {}, GCC compiles it without any issues. Why does the completeness of the type affect pointer comparison at compile time?

What the C++ Standard Says

To determine if GCC is correct, we must look at two key parts of the C++ standard: Pointer Equality [expr.eq] and Constant Expressions [expr.const].

1. Pointer Equality ([expr.eq])

According to the C++ standard draft, when comparing two pointers:

  • If two pointers point to different complete objects (or subobjects thereof), they compare unequal.
  • Even if the type is incomplete, a1 and a2 are declared as distinct extern variables. They represent distinct objects in the program.

There is no clause in [expr.eq] stating that pointer comparison of incomplete types yields an unspecified or undefined result. Because a1 and a2 are distinct entities, their addresses &a1 and &a2 must be different. Thus, &a1 != &a2 is well-defined and evaluates to true.

2. Constant Expressions ([expr.const])

An expression is a core constant expression unless its evaluation would trigger one of the excluded cases listed in [expr.const]. The relevant rule here is:

An operator where the result is unspecified.

Since the result of comparing two distinct, named global variables is fully specified (it is guaranteed to be true for inequality), it does not fall under this exclusion.

Furthermore, in C++20 and later, rules regarding "potentially non-unique objects" and "constexpr-unknown representation" were introduced to handle edge cases like empty classes or union members. However, a1 and a2 are distinct, named, non-temporary objects, meaning they do not trigger any of these modern restrictions.

Why Does GCC Reject It?

The failure in GCC is a compiler bug (or a conservative limitation of GCC's constexpr evaluator).

Internally, GCC's constexpr engine tries to resolve the layout and properties of the objects being compared to ensure they do not overlap. When GCC encounters an incomplete type, it conservatively assumes that it cannot guarantee the relationship between the two pointers at compile time, failing to recognize that they are distinct, named global variables whose addresses must be unique regardless of their size or layout.

This issue has been tracked in various GCC bug reports regarding the constexpr evaluation of incomplete types.

How to Work Around This Issue

If you need your code to be portable across all major compilers (including GCC), you have a few workarounds:

1. Complete the Type

The simplest workaround is to define the structure before performing the assertion:

struct A {}; // Type is now complete

extern const A a1;
extern const A a2;

static_assert(&a1 != &a2); // Works on all compilers

2. Use a Helper Type

If you cannot complete A because it is part of a strict API boundary, you can wrap the pointers or use an auxiliary complete type to manage the linkage or identity check.

Conclusion

Comparing pointers to extern const variables of incomplete types at compile time is valid C++. GCC's rejection of this pattern is non-conforming, while Clang and MSVC correctly adhere to the standard. Until GCC resolves this limitation in its constexpr evaluator, completing the type remains the most reliable workaround for cross-platform codebases.