5

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?

Community
  • 1
  • 1
Seleznev Anton
  • 641
  • 5
  • 14
  • 2
    Yes, it is normal. This is undefined behavior. The compiler can do anything. – Sam Varshavchik Apr 03 '17 at 20:41
  • *"Is it normal that compilers differ with turned off optimizations"* -- Yes. If, at higher optimizations, the compiler/linker is able to eliminate all references to a symbol, you can get away with not defining that symbol. (Though, to make sure the program is well-formed, you should define it regardless.) – cdhowie Apr 03 '17 at 20:52
  • Does that really an undefined behaviour? Can you give a link to a description of this case as an undefined behaviour, please? – Seleznev Anton Apr 03 '17 at 20:53
  • @SeleznevAnton I believe the description is "you are using something you have declared but not defined." In this case, `A::bbb`. – cdhowie Apr 03 '17 at 20:53
  • 2
    @SeleznevAnton see [basic.def.odr] of C++14 "Every program shall contain exactly one definition of every non-inline function or variable that is odr-used in that program; no diagnostic required". The previous paragraph defines *odr-used*, which does include the usages in your code. "No Diagnostic Required" means undefined behaviour ([intro.compliance]/2.3) – M.M Apr 03 '17 at 21:07
  • 1
    @Barry agree, but I upvote Sam's for explaining and would downvote yours for being non-constructive! – M.M Apr 03 '17 at 21:11
  • Thanks a lot for the desctiption. – Seleznev Anton Apr 03 '17 at 21:13
  • @M.M It's not an explanation of anything. "This is undefined behavior." What is "this"? Why is it undefined? The fact that a specific part of OP's program is UB for a specific reason does not make the comment remotely constructive or helpful. Add on that the meta-effect of this is what he does, all the time. – Barry Apr 03 '17 at 21:21
  • @Barry OP wrote "I understand why this error occurs." which I took to mean that he understands the problem with the code, but didn't understand that it was NDR (which = UB in this case). Although you could argue it should be posted as an answer rather than a comment. – M.M Apr 03 '17 at 21:25

1 Answers1

11

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.

Barry
  • 286,269
  • 29
  • 621
  • 977