Can't compile with static constexpr under pre-C++17 mode

Viewed 538

Why doesn't the following minimal example compile with c++11 nor c++14, but compiles in c++17 and c++2a?

#include <iostream>
#include <limits>
#include <vector>

// works:
// static constexpr int VALUE_LIMIT_A = std::numeric_limits<int>::max();

class Classy {
    // does not work in c++11 (constexpr introduced) nor c++14:
    // works if c++17 or newer:
    static constexpr int VALUE_LIMIT_A = std::numeric_limits<int>::max();
    int VALUE_LIMIT_B = std::numeric_limits<int>::max();

    public:
        explicit Classy();
        std::vector<int> classy;
};

Classy::Classy() {
    // does not work:
    classy.resize(3, VALUE_LIMIT_A);

    // works:
    // classy.resize(3, std::numeric_limits<int>::max());

    // works:
    // std::cout << VALUE_LIMIT_A;

    // works:
    // classy.resize(3, VALUE_LIMIT_B);
}

// required in c++11 and c++14
// constexpr int Classy::VALUE_LIMIT_A;

int main() {
    Classy classy{};

    for (const auto& elem : classy.classy) {
        std::cout << elem << ",";
    }
    std::cout << "\n";
}

Here is output with c++11:

$ g++ -std=c++11 main.cpp && ./a.out
/tmp/ccon7pPo.o: In function `Classy::Classy()':
main.cpp:(.text+0x31): undefined reference to `Classy::VALUE_LIMIT_A'
collect2: error: ld returned 1 exit status

Here is output with c++17:

$ g++ -std=c++17 main.cpp && ./a.out
2147483647,2147483647,2147483647,
2 Answers

Because since C++17, the definition of the constexpr static data member at namespace scope is not required again.

If a const non-inline (since C++17) static data member or a constexpr static data member (since C++11) is odr-used, a definition at namespace scope is still required, but it cannot have an initializer. This definition is deprecated for constexpr data members (since C++17).

struct X {
    static const int n = 1;
    static constexpr int m = 4;
};
const int *p = &X::n, *q = &X::m; // X::n and X::m are odr-used
const int X::n;             // … so a definition is necessary
constexpr int X::m;         // … (except for X::m in C++17)

If a static data member is declared constexpr, it is implicitly inline and does not need to be redeclared at namespace scope. This redeclaration without an initializer (formerly required as shown above) is still permitted, but is deprecated. (since C++17)

And note that std::vector::resize takes the 2nd parameter by reference; which cause VALUE_LIMIT_A to be odr-used for classy.resize(3, VALUE_LIMIT_A);.

Since C++17, with the introduction of inline variables, static constexpr data members are implicitly inline variables:

[dcl.constexpr]

1 ... A function or static data member declared with the constexpr specifier is implicitly an inline function or variable ([dcl.inline])...

Inline variables, like inline functions, are defined in every translation units they are used in. And the compiler resolves the multiple definitions into a single one. This means, the unlike C++14, there is no need to explicitly provide an out of class definition for a static constexpr variable for the sake of the ODR, the compiler takes care of it.

You can also get away with it in C++14, too. Like the other answer mentions, it is resize that ODR-uses the static data member. You can work around it, though:

classy.resize(3, int(VALUE_LIMIT_A));

While it looks superfluous, it in fact has different behavior to using the constant directly. This creates an integer temporary, with the value of the constant. But it doesn't ODR-use the constant. The temporary is bound to the reference instead, and so the problem is avoided. While it's better to define the constant in pre-C++17 code, you can use this trick to adapt code you have no control over.

Related