Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Linker error for constexpr static member variable in gcc and clang

Tags:

c++

c++11

c++14

I have a snippet:

enum class EC {a, b};

struct B {
    constexpr B(EC ec): ec_(ec) {}
    EC ec_;
};

struct A_base {
    constexpr A_base(B b): b_(b) { }
    B b_;
};

struct A: A_base {
    static constexpr B bbb = EC::a;
    constexpr A(B bbbb): A_base(bbbb) { }
};

int main()
{
    A a1(A::bbb);    // 1
    A a2{A::bbb};    // 2

    A a3 = A::bbb;   // 3
    A a4 = {A::bbb}; // 4
}

It compiles good by modern compilers with c++17 support. With c++11 and c++14 standard support linker error occurs. This question was already discussed in Linker error (undefined reference) with `static constexpr const char*` and perfect-forwarding , C++ Linker Error With Class static constexpr and some other discussions. I understand why this error occurs.

However some things I don't understand:

  1. With clang I always get linker errors. However when I set -O3 optimization or declare a1, a2, a3, a4 variables as constexpr it inlines A::bbb, and there is no error.
  2. Gcc without optimization needs reference to A::bbb only for a3, and a4. With optimizations and constexpr it behaves exactly like clang.

Is it normal that compilers differ with turned off optimizations and why does gcc treats a1,a2 initializations not equally to a3, a4?

like image 404
Seleznev Anton Avatar asked Dec 23 '22 19:12

Seleznev Anton


1 Answers

Before C++17

All the initializations of As invoke the A::A(B ) constructor, which require copying a B, which uses the compiler-generated B::B(B const&). Binding a variable to reference (A::bbb) is an odr-use of that variable. The specific rule, from [basic.def.odr] is:

A variable x whose name appears as a potentially-evaluated expression ex is odr-used by ex unless applying the lvalue-to-rvalue conversion (4.1) to x yields a constant expression (5.20) that does not invoke any non-trivial functions and, if x is an object, ex is an element of the set of potential results of an expression e, where either the lvalue-to-rvalue conversion (4.1) is applied to e, or e is a discarded-value expression (Clause 5).

The first call to the copy constructor would involve binding A::bbb to a reference, which is neither an lvalue-to-rvalue conversion nor a discarded-value expression, so it's odr-used.

The most important rule is:

Every program shall contain exactly one definition of every non-inline function or variable that is odr-used in that program outside of a discarded statement (6.4.1); no diagnostic required.

A::bbb is odr-used, but lacks a definition, so we're violating that rule - commonly referred to as an odr violation. But since the compiler is not required to issue a diagnostic in this case ("no diagnostic required", or NDR for short), the program is undefined behavior. These kinds of issues can be frustrating at times for a programmer, but they're arbitrarily difficult for the compiler to diagnose - so it's something that we have to live with.

It is likely that on higher optimization levels, the compilers simply elide the copy, so a call to B::B(B const&) for A::bbb isn't necessary... As to why different initializations are treated differently? Probably as a result of different optimization passes. Ultimately, it doesn't change the fact that this is an odr violation - regardless of whether the code compiles and links.


After C++17

As a result of p0386, static constexpr data members are implicitly inline, which means that now A::bbb does have a definition and now there is no odr-violation. C++17 is cool.

like image 112
Barry Avatar answered May 14 '23 20:05

Barry