Search code examples
c++templateslinkerc++14undefined-reference

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


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).


Solution

  • 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;
    }