Подтвердить что ты не робот

Порядок статических деструкторов

Если класс Foo имеет статическую переменную-член Bar, я ожидал бы, что деструктор Bar будет запущен только после запуска последнего экземпляра Foo destructor. Это не происходит с фрагментом кода ниже (gcc 6.3, clang 3.8):

#include <memory>
#include <iostream>

class Foo;
static std::unique_ptr<Foo> foo;

struct Bar {
    Bar() {
        std::cout << "Bar()" << std::endl;
    }

    ~Bar() {
        std::cout << "~Bar()" << std::endl;
    }
};

struct Foo {
    Foo() {
        std::cout << "Foo()" << std::endl;
    }

    ~Foo() {
        std::cout << "~Foo()" << std::endl;
    }

    static Bar bar;
};
Bar Foo::bar;

int main(int argc, char **argv) {
    foo = std::make_unique<Foo>();
}

Выходы:

Bar()
Foo()
~Bar()
~Foo()

Почему порядок разрушения здесь не наоборот? Если ~Foo() использует Foo::bar, это будет полезно после удаления.

4b9b3361

Ответ 1

В С++ объекты строятся в порядке их возникновения и уничтожаются в обратном порядке. Сначала идет конструкция foo, затем bar конструкция, затем выполняется main, тогда bar разрушается, а затем foo. Это поведение, которое вы видите. Переключатель появляется, потому что конструктор foo не создает a foo, он создает пустой unique_ptr, поэтому вы не видите Foo() в выводе. Затем bar строится с выходом, а в main вы создаете фактический foo после того, как foo уже долго строится.

Ответ 2

Я ожидаю, что деструктор Bar будет запущен только после того, как будет запущен последний экземпляр Foo destructor.

Нет, как элемент данных static, Foo::bar не зависит от всех экземпляров Foo.

И для кода, который вы показали,

static std::unique_ptr<Foo> foo; // no Foo created here

Bar Foo::bar; // Foo::bar is initialized before main(), => "Bar()"

int main(int argc, char **argv) {
    foo = std::make_unique<Foo>(); // an instance of Foo is created, => "Foo()"
} 

// objects are destroyed in the reverse order how they're declared
// Foo::bar is defined after foo, so it destroyed at first => "~Bar()"
// foo is destroyed; the instance of Foo managed by it is destroyed too => "~Foo()"

Ответ 3

Усложнение заключается в том, что код не обрабатывает конструктор foo. Случается, что foo создается сначала, чем Foo::bar создается. Вызов make…unique вызывает объект foo. Затем main завершается, и два статических объекта уничтожаются в обратном порядке их построения: Foo::bar уничтожается, затем foo. Деструктор для foo уничтожает объект foo, на который он указывает, который создан в main.

Ответ 4

Время жизни статических объектов основано исключительно на порядке их определения. Компилятор не "знает достаточно", когда вызывать Bar::Bar() столько же, сколько вызывать Bar::~Bar().

Чтобы лучше проиллюстрировать проблему, рассмотрите этот

class Foo;

struct Bar {
    Bar() {
        std::cout << "Bar()" << std::endl;
    }

    ~Bar() {
        std::cout << "~Bar()" << std::endl;
    }

    void baz() {}
};

struct Foo {
    Foo() {
        bar.baz();
        std::cout << "Foo()" << std::endl;
    }

    ~Foo() {
        std::cout << "~Foo()" << std::endl;
    }

    static Bar bar;
};

Foo foo;
Bar Foo::bar;

int main() {}

Печать

Foo()
Bar()
~Bar()
~Foo()

Добавление std::unique_ptr откладывает Foo::Foo() после его построения в основном, создавая иллюзию компилятора, "зная", когда вызывать Bar::Bar().

TL;DR Статические объекты должны быть определены позже, чем их зависимости. Прежде чем определить bar, это так же сложно определить a std::unique_ptr<Foo> и определить a Foo

Ответ 5

В общем, вы не должны писать код, который зависит от порядка построения или уничтожения статических (или глобальных) данных. Это делает нечитаемый и недостижимый код (вы можете предпочесть статические интеллектуальные указатели или явная инициализация или процедуры запуска из main). И AFAIK этот заказ не указан, когда вы связываете несколько единиц перевода.

Обратите внимание, что GCC предоставляет init_priority и constructor (с приоритетом). Я считаю, что вам следует избегать их использования. Тем не менее, __attribute__(constructor) является полезным внутри плагинами для инициализации плагина.

В некоторых случаях вы также можете использовать atexit (3) (по крайней мере, в системах POSIX). Я не знаю, вызываются ли такие зарегистрированные функции до или после деструкторов (и я думаю, что вы не должны заботиться об этом порядке).