漂亮的计数器习语的错误或格式错误的静态订单惨败?

A bug with the nifty-counter idiom or an ill-formed static order fiasco?

以下代码因 clang(x86_64-pc-linux-gnu 上的版本 5.0.0-3~16.04.1)而崩溃,但在 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;    
}

clang 崩溃,根据地址清理程序,由于:

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)

这是 clang 中 nifty-counter 习惯用法的错误,还是格式错误的静态初始化顺序失败的示例?


编辑

根据接受的答案,问题可以改写为:

为避免剧透,我只是暗示答案是。这可能会发生,但不要担心,有解决方法。要查看更多信息,请关注 .

同样按照接受的答案,所讨论的案例可以缩小到更基本的场景:

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(而且看起来,理所当然!)。

不幸的是,该代码具有未指定的行为。原因类似于,如果不是通常的定义,静态初始化顺序 Fiasco。

对象 std::cout 和在 <iostream> 中声明的其他类似对象在类型 std::ios_base::Init 的第一个对象初始化之前不能使用。包含 <iostream> 定义(或表现得好像它定义了)具有静态存储持续时间 ([iostream.objects.overview]/3) 的该类型的非本地对象。这在大多数情况下都能满足要求,即使在动态初始化期间使用 std::cout 和朋友,因为 Init 定义通常在翻译单元中比任何其他非本地静态存储对象更早定义。

然而,[basic.start.dynamic]/1 表示

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

因此,尽管 <iostream> 中定义的 std::ios_base::Init 对象(有效地)的初始化是有序的,但 A<int>::i 的初始化是无序的,因此这两个初始化的顺序不确定。所以我们不能指望这段代码能正常工作。

正如@walnut 在评论中提到的,可以通过在使用 std::cout 之前在 A<int>::i 的动态初始化期间强制初始化另一个 std::ios_base::Init 对象来更正代码:

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