undefined symbol when initializing a static constexpr home-made string variable with gcc in debug configuration, C++14

100 Views Asked by At

I'm failing to understand a linker error in C++14 with gcc.
My purpose was to have a template that exposes a name, according to a non-type template parameter (a kind of compile-time mapping between a value and a string).
Lacking support for constexpr string in C++14. I implemented a very basic wrapper around a static plain C string:
In header.h

#ifndef HEADER
#define HEADER
#include <cstddef>
#include <ostream>

namespace nConstStr
{
class constStr
{
public:
    constexpr constStr() noexcept = default;
    constexpr constStr(constStr const& str) noexcept :
        _str(str._str), _sz(str._sz)
    {
    }
    template<std::size_t N>
    constexpr explicit constStr(char const (&str)[N]) noexcept :
        _str(str), _sz(N - 1)
    {
    }
    ~constStr() noexcept = default;
    constexpr char operator[](std::size_t const i) const noexcept
    {
        return _str[i];
    }
    constexpr std::size_t size() const noexcept
    {
        return _sz;
    }

private:
    char const* _str = nullptr;
    std::size_t _sz = 0;
};
std::ostream& operator<<(std::ostream& os, constStr const& str)
{
    for (std::size_t i = 0; i < str.size(); ++i)
    {
        os << str[i];
    }
    return os;
}
}

template<std::size_t I> class dummy final
{
public:
    static constexpr nConstStr::constStr name = nConstStr::constStr{"dummy"};
};

#endif

For simplicity, here the string is always the same. Then I've got a translation unit where I'm using this:
In test.cpp

void test() {
    // injecting my overloaded operator<<
    using nConstStr::operator<<;
    std::cout << dummy<42>::name << '\n';
}

Which I call from main.

When compiling with gcc in C++14 in a debug configuration, I've got the link error:

src.cpp:8: undefined reference to `dummy<42ul>::name'

Live.

On my machine, yet, it compiles and runs fine in -O2.
It also works fine with C++17 or more.

Note, MSVC accepts the code but clang seems consistent with gcc (at least on godbolt, couldn't test locally).

2

There are 2 best solutions below

6
Ted Lyngmo On BEST ANSWER

Prior to C++17 (in which you can declare the static members inline and static constexpr members will be implicitly inline) you need to provide an out-of-class definition too:

template<std::size_t I>
class dummy final {
public:
    static constexpr nConstStr::constStr name = nConstStr::constStr{"dummy"};
};

// Add this:
template<std::size_t I>
constexpr nConstStr::constStr dummy<I>::name;

Note: Since you define the operator<< overload in the header file, it should be declared inline:

inline std::ostream& operator<<(std::ostream& os, constStr const& str) {
^^^^^^
    for(std::size_t i = 0; i < str.size(); ++i) {
        os << str[i];
    }
    return os;
}
0
user12002570 On

The problem is that prior to C++17, we need to provide an out of class definition for the constexpr static data member. From C++17 onwards the out of class definition is optional. That is why, when you try your program with C++17, it starts working without any linker error.

This can be seen from static:

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


(emphasis mine)

This means that to solve your problem in C++14, you need to add the following out of class definition for the constexpr static member in the header file header.h.

header.h

//this is needed prior to C++17 but optional in C++17
template<std::size_t I> constexpr nConstStr::constStr dummy<I>:: name;

Working demo