Weird undefined reference - CXX 14 vs 17
Lately, I've been working on a project that involves migrating our C++17-based solution to an environment that relies on C++14. Honestly, I'm not entirely sure why a client would want such a downgrade, but the customer is king, right?
So, I went ahead and replaced all the features introduced in C++17, like <variant>
, <filesystem>
, inline variables, lambda capture of *this
, constexpr
lambdas, __has_include
, and, regrettably, one of the most important ones, the [[maybe_unused]]
attribute. Most of these changes went smoothly, but I ran into a peculiar issue: undefined references during linking. I was absolutely certain that everything was correctly declared, defined in the header files, and properly linked.
After several days (and a few sleepless nights) pulling my hair out, I finally pinpointed the root cause. The issue stemmed from constexpr static
data members in the header files. To be more precise, it's due to the nuances of how C++ treats these members:
A constexpr specifier used in an object declaration or non-static member function (until C++14) implies const. A constexpr specifier used in a function or static data member (since C++17) declaration implies inline. If any declaration of a function or function template has a constexpr specifier, then every declaration must contain that specifier.
- from cppreference
If a const non-inline (since C++17) static data member or a constexpr static data member (since C++11)(until C++17) is odr-used, a definition at namespace scope is still required, but it cannot have an initializer.
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)
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)
- from cppreference
This means that I didn't have to re-declare static const
and static constexpr
member variables in corresponding .cpp
files in C++17. But because I'm downgrading the project, I have to do it now.
So, I've solved one problem by re-declaring every static constexpr
variable. But what happens when the class is templated? According to the Standard C++ Foundation, it's not advisable to split a templated class into declaration and definition.
I scoured for a more elegant, intuitive, and scalable solution but ended up removing the static keyword and creating an instance every time I needed to use the class. I wish I could come up with a better solution, but I don't believe this "problem" can be fixed without refactoring the entire legacy project. It's working, and I'm content with that for now.