The C++ logo, by Jeremy Kratz, licensed under CC0 1.0 Universal

Linker error with static member variable


6 - 7 minutes read, 1454 words
Categories: c++
Keywords: c++ constexpr conversion inline namespace temporary object

It’s interesting to see how small snippets of code can cause so much headache.

Consider this minimal example.

struct c{
    static const int i = 42;
};
int foo(const int& i) { return i; }

int main(){
    return foo(c::i);
}

It won’t compile with gcc or clang, unless compiling with optimization:

> g++ --std=c++11 main.cpp
/usr/bin/ld: /tmp/ccYtUx0X.o: warning: relocation against '_ZN1c1iE' in read-only section '.text'
/usr/bin/ld: /tmp/ccYtUx0X.o: in function 'main':
main.cpp:(.text+0x12): undefined reference to 'c::i'
/usr/bin/ld: warning: creating DT_TEXTREL in a PIE
collect2: error: ld returned 1 exit status
> g++ --std=c++11 -O1 main.cpp
# compiles without warnings

The error is cryptic for multiple reasons.

If foo takes its argument by value and not const-ref, then the code also compiles

struct c{
    static const int i = 42;
};
int foo(int i) { return i; }

int main(){
    return foo(c::i);
}

If we use a float instead of an int, we will get the following compiler error with GCC: error: ‘constexpr’ needed for in-class initialization of static data member ‘const float c::i’ of non-integral type [-fpermissive]. If we also add constexpr, the code compiles

struct c{
    static constexpr const float i = 42;
};
int foo(const int& i) { return i; }

int main(){
    return foo(c::i);
}

But if I change the signature of int foo(const int&) to int foo(const float&), then the issue is there again

struct c{
    static const constexpr float i = 42;
};
int foo(const float& i) { return i; }

int main(){
    return foo(c::i);
}

So it seems that if there is an implicit conversion between different types, then the code compiles.

The issue also disappears when using a "normal" global variable, ie when using a namespace instead of a struct for scoping symbols. The static keyword, in this case, does not make any difference:

namespace c{
    const int i = 42;
}
int foo(const int& i) { return i; }

int main(){
    return foo(c::i);
}

Is there anything special happening with integral and fundamental types?

Apparently not, the same issue appears when using a user-defined type

struct wrap{int i;};
struct c{
    static constexpr const wrap i{42};
};
void foo(const wrap&) { }

int main(){
    foo(c::i);
}

Then notice that the following snippet compiles without issues

struct c{
    static const int i;
};
const int c::i = 42;
void foo(const int&) { }

int main(){
    foo(c::i);
}

and that it is not possible to rewrite this code with non-integral types, as constexpr requires an initializer where the value is declared.

I mainly analyzed the issue with a C++11 compiler (as it was the scope of the project), but then I decided to see if using a more recent C++ standard would change anything.

Indeed, in C++17, when using constexpr, the issue disappears, thus

struct wrap{int i;};
struct c{
    static constexpr const TYPE i{42};
};
void foo(const TYPE&) { }

int main(){
    foo(c::i);
}

compiles both with #define TYPE int and #define TYPE wrap with g++ --std=c++17 main.cpp.

But without constexpr it does compile only with #define TYPE int (as in C++11) and with optimizations.

Rule of thumb

I’ve already written down some notes about namespace and classes, but it did not occur to me that in this case, it would make a difference.

This is another reason for using the tool designed to fulfill a particular job instead of using paradigms of other languages.

Also, for defining compile-time constants, use constexpr. It helps to avoid errors, see the issue with globals, avoid unnecessary overhead, and in this case, when targeting at least C++17, linker errors too.

But it does not explain why the code behaves differently when using struct, and it also does not offer a solution in those cases we should prefer it over a namespace.

Putting it all together

The issue does not appear if

  • initialization is not inline

  • there is a conversion

  • using constexpr in C++17 (and inline initialization)

  • there is no foo

  • an implicit conversion between the global and parameter required by foo happens

  • foo takes a parameter by value

Also note that for initializing inline, int suffices to be const, but other types (float, user-declared types, …​) need to be declared constexpr.

It seems that static member variables without out-of-class definitions are not real objects contrary to global variables.

I’m unsure where it is exactly stated in the standard and why this difference exists, but GCC and clang act accordingly.

The most explicit reference to this property I could find is the note

Once the static data member has been defined, it exists even if no objects of its class have been created. […​]

This also explains why the code compiles if the constant is an int and foo accepts a float. If a conversion takes place, then "a temporary object is created".

The fact that the code does compile with optimization is probably a side effect. The compiler might inline the function and completely optimize it away, or just replace the call with the literal value after it sees that the address is not taken.

In the case of function taking the parameter by value, the fact that the object does not exist is not an issue, as it is not possible to take the address of the global.

Having said that, following snippets compiles with both GCC and clang, which seems to contradict what I’ve just said

struct c{
	static const int i = 42;
};
int main(){
	&c::i;
}

But if the value is just stored somewhere

struct c{
	static const int i = 42;
};
int main(){
	auto p = &c::i;
}

then it does not compile with undefined reference to 'c::i', even if the value is used as much as before.

But when adding constexpr, why does the code compile since C++17?

inline variables

Since C++17 there are inline variables. Just as with inline functions, the inline keyword is used to avoid ODR-violations.

In this case, the object exists and thus there are no issues.

constexpr variables are, since C++17 inline, so I double-checked and

struct wrap{int i;};
struct c{
	static inline const TYPE i{42};
};
void foo(const TYPE& i) { }
int main(){
	foo(c::i);
}

compiles without constexpr and both with #define TYPE wrap and #define TYPE int.

Why is constexpr not necessary for int?

There is one question open, why don’t we need constexpr for int in the first example?

This is for historic reasons, as constexpr did not exist in the first revisions of the C++ language. Without special-casing int, writing

const int size = 3

void foo(){
	int arr[size]{};
}

would not have been possible.

For the record, in C the code is valid and uses a variable-length array (VLA), an array which stack size is determined at runtime. (making sizeof a runtime operation!).

VLA have multiple disadvantages, they do not perform well, have a complicated implementation, are a recipe for stack overflows (and thus security hazard), might leak in case of longjmp, MSVC does not support them, and are deprecated since C17.

In C++ the code is equivalent to

void foo(){
	int arr[3]{};
}

just as if size would have been an integral, and not a variable!

In C it is possible to mimic such feature with an enum or macro

enum{ size = 3 };
// or: #define size 3

void foo(){
	int arr[size]{};
}

And note that in both of those cases, there is no object size as it is not possible to get an address from it.

How to fix the linker error

There are multiple solutions, that only depend loosely on each other. Depending on the context some might not be applicable, and for each possible solution, one has to consider the pro and contra.

The best would be to use a never standard, at least C++17, and define the variable as constexpr. If the variable cannot be made constexpr

  • use C++17 and mark the variable constexpr

  • define the variable as inline

  • use a namespace instead of a struct/class

  • if the object is small, change foo to accept it by value

  • create a temporary object when the value is passed to foo. This can be done by creating a local variable, or with an apparently redundant static_cast. In the case of integral types, it can also be done by doing some otherwise unnecessary operations, like adding 0 or use the unary + operator on an integral type.

  • use an enum or define

  • leave the declaration as-is, and add a definition in the corresponding .cpp file. The initialization can be moved to the implementation file, but must be left in the header in case of constexpr.


Do you want to share your opinion? Or is there an error, same parts that are not clear enough?

You can contact me here.