4

The following code crashes with clang (version 5.0.0-3~16.04.1 on x86_64-pc-linux-gnu) but works fine with gcc (9.2.0).

struct Registry {
    static int registerType(int type) {
        std::cout << "registering: " << type;
        return type;
    }
};

template<typename T>
struct A {
    static int i;
};

template<typename T>
int A<T>::i = Registry::registerType(9);

int main() {
    std::cout << A<int>::i << std::endl;    
}

The clang crash, is according to address sanitizer due to:

ASAN:DEADLYSIGNAL
=================================================================
==31334==ERROR: AddressSanitizer: SEGV on unknown address 0xffffffffffffffe8 (pc 0x7f5cc12b0bb6 bp 0x7ffdca3d1a20 sp 0x7ffdca3d19e0 T0)
==31334==The signal is caused by a READ memory access.
    #0 0x7f5cc12b0bb5 in std::ostream::sentry::sentry(std::ostream&) /root/orig/gcc-9.2.0/x86_64-pc-linux-gnu/libstdc++-v3/include/bits/ostream.tcc:48:31
    #1 0x7f5cc12b11e6 in std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long) /root/orig/gcc-9.2.0/x86_64-pc-linux-gnu/libstdc++-v3/include/bits/ostream_insert.h:82:39
    #2 0x4197a7 in __cxx_global_var_init.1 (/tmp/1576534654.656283/a.out+0x4197a7)
    #3 0x514eac in __libc_csu_init (/tmp/1576534654.656283/a.out+0x514eac)
    #4 0x7f5cc02847be in __libc_start_main /build/glibc-Cl5G7W/glibc-2.23/csu/../csu/libc-start.c:247
    #5 0x419858 in _start (/tmp/1576534654.656283/a.out+0x419858)

Is this a bug with the nifty-counter idiom in clang, or an example of an ill-formed static initialization order fiasco?


Edit

Following the accepted answer, the question can be rephrased to:

  • Can it be that the global ostream object std::cout is not properly initialized?
  • Is there a valid case in which the compiler is allowed not to have std::cout initialized, even though we included iostream and we use std::cout properly?
  • Is there a use case where crashing on an ordinary cout << "foo" is not a compiler bug?

To avoid the spoiler I would just hint that the answer is Yes. This can happen, but don't worry there is a workaround. To see more follow the accepted answer below.

Also following the accepted answer, the case in question can be narrowed to an even more basic scenario:

int foo() {
    std::cout << "foo";
    return 0;
}

template<typename T>
struct A {
    static int i;
};

template<typename T>
int A<T>::i = foo();

int main() {
    (void) A<int>::i;    
}

that crashes on the said clang version (and as it seems, justifiably!).

Amir Kirsh
  • 12,564
  • 41
  • 74
  • 1
    *static initialization order fiasco* olny applies if there is more than one TU. You don't have that so it can't be it. – NathanOliver Dec 16 '19 at 22:29
  • Code works fine here: https://wandbox.org/permlink/SCxM0XQ2GNBkvasm. Looks like it might be your implementation. – NathanOliver Dec 16 '19 at 22:31
  • @NathanOliver-ReinstateMonica this is what I thought as well (ostream is also in the game but should be handled by the nifty-counter idiom). So is it a bug in clang?? – Amir Kirsh Dec 16 '19 at 22:32
  • I can only reproduce this with Clang using libstdc++ (but even up to clang trunk). It works fine with libc++. I assume there is some incompatibility issue between clang and libstdc++. See https://godbolt.org/z/ip_8gL – walnut Dec 16 '19 at 22:33
  • http://coliru.stacked-crooked.com/a/b9d0f4ea4faaa043 – Amir Kirsh Dec 16 '19 at 22:33
  • I'm going to go with a libstdc++ + clang interop bug. – NathanOliver Dec 16 '19 at 22:34
  • Does the bug go away if you remove the `cout` statement from the `registerType` function? – M.M Dec 16 '19 at 22:44
  • @M.M yes it does: http://coliru.stacked-crooked.com/a/83e3d304d6e93c40 but on the other hand removing the `cout` from main doesn't change it: http://coliru.stacked-crooked.com/a/04a10be61bdb3dd1 (i.e. the bug doesn't need the cout in main) – Amir Kirsh Dec 16 '19 at 22:54
  • 1
    @NathanOliver-ReinstateMonica That is not true that more than one TU is needed for the fiasco if templates are involved. See answer by aschepler. – walnut Dec 16 '19 at 23:04

1 Answers1

6

The code unfortunately has unspecified behavior. The reason is similar to, if not the usual definition of, the Static Initialization Order Fiasco.

The object std::cout and other similar objects declared in <iostream> may not be used before the first object of type std::ios_base::Init is initialized. Including <iostream> defines (or acts as though it defines) a non-local object of that type with static storage duration ([iostream.objects.overview]/3). This takes care of the requirement in most cases, even when std::cout and friends are used during dynamic initialization, since that Init definition will normally be earlier in the translation unit than any other non-local static storage object definition.

However, [basic.start.dynamic]/1 says

Dynamic initialization of a non-local variable with static storage duration is unordered if the variable is an implicitly or explicitly instantiated specialization, ....

So although the initialization of the std::ios_base::Init object (effectively) defined in <iostream> is ordered, the initialization of A<int>::i is unordered, and therefore the two initializations are indeterminately sequenced. So we can't count on this code working.

As @walnut mentioned in a comment, the code can be corrected by forcing another std::ios_base::Init object to be initialized during dynamic initialization of A<int>::i before the use of std::cout:

struct Registry {
    static int registerType(int type) {
        static std::ios_base::Init force_init;
        std::cout << "registering: " << type;
        return type;
    }
};
aschepler
  • 70,891
  • 9
  • 107
  • 161
  • Like a magic :-) http://coliru.stacked-crooked.com/a/fd48e7a77b51a8da – Amir Kirsh Dec 16 '19 at 23:08
  • Minor nitpick but I guess this incurs a runtime penalty (testing of thread-safe static guard flag every time `registerType` is entered), probably unmeasurable in practice. Would it also solve the problem to have this at file scope but before the definition of `A::i` ? – M.M Dec 16 '19 at 23:36
  • @M.M No, because `A::i` still has unordered initialization. And in all implementations I've seen, a definition of an `Init` object at namespace scope (with internal or no linkage) is exactly what `` already contains. It might be slightly better to take the `static` off `force_init`, since the `Init` constructor probably also needs its own guard or atomic or something. – aschepler Dec 17 '19 at 03:27