Порядок инициализации статических встроенных членов

Хорошо известная проблема в C++ — это фиаско статического порядка инициализации. По-прежнему считается ли проблемой использование статических встроенных членов C++17?

Вот пример, где статический встроенный член используется в двух разных единицах трансляции (a.cpp и b.cpp) в качестве инициализатора для двух невстроенных статических членов:

counter.hh

#pragma once

#include <vector>
#include <fstream>

class Counter
{
    public:
        Counter()  { std::ofstream os("o.txt", std::ofstream::app); os << "Counter created" << std::endl; }
        ~Counter() { std::ofstream os("o.txt", std::ofstream::app); os << "Counter destroyed" << std::endl; }
        void add_instance()    
        { 
            ++m_instances; 
            std::ofstream os("o.txt", std::ofstream::app); os << "Counter increased: " << m_instances << std::endl; 
        }
        void remove_instance() 
        { 
            --m_instances; 
            std::ofstream os("o.txt", std::ofstream::app); os << "Counter decreased: " << m_instances << std::endl; 
        }

    private:
        int m_instances = 0;
};

class Object
{
    public:
        Object(Counter & counter) : m_counter(counter) 
        {
            m_counter.add_instance(); 
            std::ofstream os("o.txt", std::ofstream::app); os << "Object created" << std::endl; 
        }
        ~Object() 
        { 
            m_counter.remove_instance(); 
            std::ofstream os("o.txt", std::ofstream::app); os << "Object destroyed" << std::endl; 
        }

    private:
        Counter & m_counter;
};

struct C
{
    static inline Counter static_counter{};
};

а.хх

#pragma once

#include "counter.hh"

struct A
{
    static Object static_a; //not inline
};

a.cpp

#include "a.hh"

Object A::static_a{C::static_counter};

б.чч

#pragma once

#include "counter.hh"

struct B
{
    static Object static_b; //not inline
};

b.cpp

#include "b.hh"

Object B::static_b{C::static_counter};

main.cpp

#include "a.hh"
#include "b.hh"

int main() { }

вывод (с MSVC 16.1.2)

Counter created
Counter increased: 1
Object created
Counter increased: 2
Object created
Counter decreased: 1
Object destroyed
Counter decreased: 0
Object destroyed
Counter destroyed

Я думаю, что в отношении инициализации эта практика безопасна, поскольку стандарт C++17 гарантирует, что статические встроенные члены: (1) всегда инициализируются перед любым использованием и (2) инициализируются только один раз для нескольких единиц трансляции.

Но я хотел бы знать, есть ли какие-либо скрытые недостатки в этом шаблоне, например, связанные с порядком уничтожения каждой переменной в разных TU. Правильно ли определено, что и static_a, и static_b всегда уничтожаются до static_counter?


person Tarquiscani    schedule 09.06.2019    source источник
comment
Насколько я знаю, порядок разрушения противоположен порядку созидания. В этом случае это зависит от вашего порядка включения. A.hpp, включите counter.hpp, поэтому порядок инициализации: Counter, A, B, И порядок уничтожения: B, A, Counter. Я в этом почти уверен, так что подождем официального ответа.   -  person CoralK    schedule 09.06.2019
comment
Возможно, вы правы в том, что порядок уничтожения всегда противоположен порядку созидания. Но нет никакой гарантии, что порядок будет Counter, A, B. Он также может быть Counter, B, A. Это не зависит от порядка включения, потому что у нас есть три разных единицы трансляции (a.cpp, b.cpp и main.cpp) и как вы можете сказать, что переменные в main.cpp имеют приоритет над переменными в a.cpp? Я думаю, что это определяется реализацией или вообще совершенно случайно.   -  person Tarquiscani    schedule 09.06.2019
comment
Что вы подразумеваете под безопасным? значения a/b не предсказуемы...   -  person Jarod42    schedule 09.06.2019
comment
Я хотел бы понять, гарантированно ли static_counter будет инициализирован до static_a/static_b и гарантированно ли он будет уничтожен после static_a/static_b. Я знаю, что static_a и static_b были бы опасны, если бы использовались для инициализации других статических членов, но меня интересует только связь между static_counter и другими глобальными переменными, которые зависят от них при построении. Может быть, я должен найти более ясный пример   -  person Tarquiscani    schedule 09.06.2019


Ответы (1)


Да, это нормально, так как в каждой единице перевода static_counter определено ранее static_a/static_b. Порядок уничтожения не обязательно будет обратным (учитывая потоки, это в любом случае бессмысленно), но обратная сторона каждой гарантии выполняется, так что это тоже работает.

person Davis Herring    schedule 09.06.2019
comment
Спасибо, я думаю, вы имеете в виду параграфы Динамическая инициализация - 2) на первой странице и 1) - b) на второй странице. - person Tarquiscani; 10.06.2019
comment
@Tarquiscani: Верно (это 1a перед C++11 для деструкторов, если кому интересно). - person Davis Herring; 10.06.2019