Class template static field initialization different results on GCC and Clang
Asked Answered
I

1

7

Consider the following C++ code example:

#include<iostream>

class B {
public:
    int val;

    B(int v):val(v){
        std::cout<< val << "\n";
    }
};

template<typename T>
class A {
public:
    static B b;
};

template<typename T>
B A<T>::b = B(1);

int main() {
    A<int>::b.val;
    return 0;
}

On GCC 11.4.0, build with g++ main.cc -g got output 1.

On Clang 14.0.0, build with clang++ main.cc -g got segfault.

Ubuntu 22.04.4 LTS

Cannot understand the reason of such behavior, would be very grateful for any help.

Indecision answered 23/6 at 10:25 Comment(3)
Looks like a bug in clang till Clang-16. From Clang-17 it prints 1, like GCC. godbolt.org/z/4E8e4WT4aOverscrupulous
It seems like it hasn't properly instantiated the standard streams at the time std::cout is used.Debbydebee
That's an example of infamous static object initialization order FIASCO. Most implementations use the nifty counter trick to overcome the special case of cout and cin; you just need to #include <iostream> as the very first line in the TU. It seems like CLANG has not used that(nifty counter) pattern; I don't know if it is a platform bug or not. But the recommended way of defining static variables is Meyers' singleton.Bribery
C
6

The program has undefined behavior because you use std::cout without having any guarantee that it is initialized.

The standard streams like std::cout are not automatically initialized and usable. Instead the header <iostream> behaves as if it declared a global static storage duration variable of type std::ios_base::Init. When a variable of this type is initialized, it will initialize the standard streams and make them usable.

The initialization of the static storage duration variable A<int>::b is dynamic initialization, because the constructor of B is not constexpr and even if it was constexpr still, because it calls a non-constexpr operator<<. And because it is a non-local variable instantiated from a template, it has unordered dynamic initialization, meaning that its initialization is unordered with any other dynamic initialization of non-local variables.

Because the initialization of A<int>::b is unordered with initialization of <iostream>'s std::ios_base::Init instance, it may happen prior to the initialization of std::cout.

To assure that the streams are initialized when you need to use them before main is entered, you need to initialize a std::ios_base::Init instance yourself:

B(int v):val(v){
    std::ios_base::Init _;
    std::cout<< val << "\n";
}

Or better, avoid global variables with dynamic initialization before main and instead use local static storage duration variables which are initialized at their first point of use from main. See e.g. the Meyers' singleton pattern.

Cephalization answered 23/6 at 11:12 Comment(2)
I'm not following how the "dynamic initialization" part applies here. There are 2 variables of static storage duration in the same TU, so they should be initialized from top to bottom I think.Overscrupulous
@Overscrupulous "so they should be initialized from top to bottom I think": That's only true if they both have ordered dynamic initialization or sometimes if they have partially ordered dynamic initialization. See timsong-cpp.github.io/cppwp/n4950/basic.start.dynamic#3. OP using a template causes the issue.Cephalization

© 2022 - 2024 — McMap. All rights reserved.