Порядок уничтожения статических объектов в разделяемых библиотеках - PullRequest
0 голосов
/ 07 февраля 2019

У меня есть основная программа (main.cpp) и общая библиотека (test.h и test.cpp):

test.h:

#include <stdio.h>

struct A {
    A() { printf("A ctor\n"); }
    ~A() { printf("A dtor\n"); }
};

A& getA();

test.cpp:

#include "test.h"

A& getA() {
    static A a;
    return a;
}

main.cpp:

#include "test.h"

struct B {
    B() { printf("B ctor\n"); }
    ~B() { printf("B dtor\n"); }
};

B& getB() {
    static B b;
    return b;
}

int main() {
    B& b = getB();
    A& a = getA();
    return 0;
}

Вот как я собираю эти источники в Linux:

g++ -shared -fPIC test.cpp -o libtest.so
g++ main.cpp -ltest

Вывод в Linux:

B ctor
A ctor
A dtor
B dtor

Когда я запускаю этот пример в Windows (после некоторых настроек, таких как добавление dllexport), я получаю с MSVS 2015/2017:

B ctor
A ctor
B dtor
A dtor

Мне кажется, что первый вывод соответствует стандарту,Например, см .: http://www.open -std.org / jtc1 / sc22 / wg21 / docs / documents / 2014 / n4296.pdf

Из пункта 3.6.3.1:

Если завершение конструктора или динамическая инициализация объекта со статической продолжительностью хранения упорядочивается раньше, чем у другого, завершение деструктора второго секвенируется перед запуском деструктора первого.

То есть, если сначала создается объект B, он должен быть уничтожен последним - это то, что мы видим в Linux.Но выход Windows отличается.Это ошибка MSVC или я что-то упустил?

Ответы [ 4 ]

0 голосов
/ 15 февраля 2019

Относительный порядок конструкторов и деструкторов определяется только в статически связанном исполняемом файле или (общей) библиотеке.Он определяется правилами области видимости и порядком статических объектов в любое удобное время.Последнее также расплывчато, потому что иногда трудно гарантировать порядок ссылок.

Общие библиотеки (dll) загружаются либо операционной системой в начале выполнения, либо могут быть загружены программой по требованию.Таким образом, нет никакого известного порядка, в котором эти библиотеки были бы загружены.Как следствие, нет никакого известного порядка, в котором они были бы выгружены.В результате порядок конструкторов и деструкторов между библиотеками может различаться.Только их относительный порядок гарантирован в пределах одной библиотеки.

Обычно, когда порядок конструкторов или деструкторов важен для библиотек или для разных файлов, существуют простые методы, которые позволяют вам это делать.Одним из них является использование указателей на объекты.Например, если объект A требует, чтобы объект B был сконструирован до него, можно сделать следующее:

A *aPtr = nullptr;
class B {
public:
    B() {
      if (aPtr == nullptr) 
         aPtr = new A();
      aPtr->doSomething();
    }
 };
 ...
 B *b = new B();

Вышеприведенное гарантирует, что A сконструирован до его использования.При этом вы можете сохранить список выделенных объектов или сохранить указатели, shared_pointers, ... в других объектах для организации упорядоченных разрушений, скажем, перед выходом из main.

Итак, чтобы проиллюстрировать вышеприведенное, я-Определил ваш пример в основной форме.Есть определенно несколько способов справиться с этим.В этом примере список уничтожения строится в соответствии с описанным выше методом, выделенные A и B помещаются в список и уничтожаются в конце в определенном порядке.

test.h

#include <stdio.h>
#include <list>
using namespace std;

// to create a simple list for destructios. 
struct Destructor {
  virtual ~Destructor(){}
};

extern list<Destructor*> *dList;

struct A : public Destructor{
 A() {
  // check existencd of the destruction list.
  if (dList == nullptr)
    dList = new list<Destructor*>();
  dList->push_front(this);

  printf("A ctor\n"); 
 }
 ~A() { printf("A dtor\n"); }
};

A& getA();

test.cpp

#include "test.h"

A& getA() {
    static A *a = new A();;
    return *a;
}

list<Destructor *> *dList = nullptr;

main.cpp

#include "test.h"

struct B : public Destructor {
  B() {
   // check existence of the destruciton list
  if (dList == nullptr)
    dList = new list<Destructor*>();
  dList->push_front(this);

  printf("B ctor\n");
 }
 ~B() { printf("B dtor\n"); }
};

B& getB() {
  static B *b = new B();;
  return *b;
}


int main() {
 B& b = getB();
 A& a = getA();

 // run destructors
 if (dList != nullptr) {
  while (!dList->empty()) {
    Destructor *d = dList->front();
    dList->pop_front();
    delete d;
  }
  delete dList;
 }
 return 0;
}
0 голосов
/ 11 февраля 2019

Я вижу две вещи, которые отсутствуют в вашем анализе.

Программа: Стандарт устанавливает требования к выполнению программы.Ваша программа состоит из (исполняемого) файла, созданного командой g++ main.cpp -ltest, предположительно a.out или a.exe.В частности, ваша программа не содержит общих библиотек, с которыми она связана.Поэтому все, что делается совместно используемой библиотекой, выходит за рамки стандарта.

Ну, почти.Поскольку вы написали свою разделяемую библиотеку на C ++, ваш файл libtest.so или test.dll действительно входит в сферу действия стандарта, но делает это сам по себе, независимо от исполняющего файла, который его вызывает.То есть наблюдаемое поведение a.exe, игнорирующее наблюдаемое поведение разделяемых библиотек, должно соответствовать стандарту, а наблюдаемое поведение test.dll, игнорирующее наблюдаемое поведение исполняемого файла, должно соответствовать стандарту.

У вас есть две связанные, но технически независимые программы.Стандарт распространяется на каждого из них в отдельности.Стандарт C ++ не охватывает, как независимые программы взаимодействуют друг с другом.

Если вам нужна ссылка для этого, я хотел бы взглянуть на пункт 9 «Фазы перевода» ([lex.phases] -- раздел 2.2 в той версии стандарта, на которую вы ссылаетесь).Результатом связывания a.out является образ программы, в то время как test.dll является частью среды выполнения.

Упорядочено раньше: Похоже, вы пропустили определениеиз "последовательности перед".Да, выход имеет «B ctor» перед «A ctor».Однако это само по себе не означает, что конструктор b был секвенирован до конструктора a.Стандарт C ++ дает точное значение для «последовательности перед» в [intro.execution] (пункт 13 раздела 1.9 в версии стандарта, на которую вы ссылались).Используя точное значение, можно сделать вывод, что если конструктор b секвенируется перед конструктором a, то вывод должен иметь «B ctor» перед «A ctor».Однако обратное (то, что вы предполагали) не выполняется.

В комментариях вы предположили, что это было незначительное изменение, когда слово «секвенировано до» было заменено на «сильно случается раньше».Это не так, поскольку «более строго происходит раньше» также имеет точное значение в более новой версии стандарта ( пункт 12 раздела 6.8.2.1 [intro.races]).Оказывается, что «сильно случается раньше» означает «последовательность перед» или один из трех дополнительных случаев.Таким образом, изменение формулировки было преднамеренным расширением этой части стандарта и охватывало больше случаев, чем раньше.

0 голосов
/ 14 февраля 2019

Даже в Linux вы можете столкнуться с пересечением вызовов статического конструктора и деструктора, если вы вручную открываете и закрываете DLL с помощью dlopen () и dlclose ():

testa.cpp:

#include <stdio.h>

struct A {
    A() { printf("A ctor\n"); }
    ~A() { printf("A dtor\n"); }
};

A& getA() {
    static A a;
    return a;
}

(testb.cpp является аналоговым, за исключением A, измененного на B и a на b)

main.cpp:

#include <stdio.h>
#include <dlfcn.h>

class A;
class B;

typedef A& getAtype();
typedef B& getBtype();

int main(int argc, char *argv[])
{
    void* liba = dlopen("./libtesta.so", RTLD_NOW);
    printf("dll libtesta.so opened\n");
    void* libb = dlopen("./libtestb.so", RTLD_NOW);
    printf("dll libtestb.so opened\n");
    getAtype* getA = reinterpret_cast<getAtype*>(dlsym(liba, "_Z4getAv"));
    printf("gotten getA\n");
    getBtype* getB = reinterpret_cast<getBtype*>(dlsym(libb, "_Z4getBv"));
    printf("gotten getB\n");
    A& a = (*getA)();
    printf("gotten a\n");
    B& b = (*getB)();
    printf("gotten b\n");

    dlclose(liba);
    printf("dll libtesta.so closed\n");
    dlclose(libb);
    printf("dll libtestb.so closed\n");

    return 0;
}

И вывод:

dll libtesta.so opened
dll libtestb.so opened
gotten getA
gotten getB
A ctor
gotten a
B ctor
gotten b
A dtor
dll libtesta.so closed
B dtor
dll libtestb.so closed

Интересно, что выполнение конструктора a откладывается до того момента, когда на самом деле вызывается getA().То же самое для b.Если статическое объявление a и b перемещено изнутри их функций-геттеров на уровень модуля, тогда конструкторы уже вызываются при загрузке DLL, хотя и автоматически.

Конечноприложение зависнет, если a или b все еще будет использоваться в функции main() после вызова dlclose(liba) или dlclose(libb), соответственно.

Если вы скомпилируете и свяжете свое приложениеобычно вызовы dlopen() и dlclose() будут выполняться кодом в среде выполнения.Кажется, что ваша проверенная версия Windows выполняет те вызовы в порядке, который вы неожиданно для вас.Причина, по которой Microsoft решила сделать это таким образом, заключалась, вероятно, в том, что при выходе из программы все в основном приложении все чаще зависело от чего-либо из DLL, чем наоборот.Таким образом, статические объекты из библиотек, как правило, должны быть уничтожены ПОСЛЕ того, как основное приложение разрушено.

По той же причине порядок инициализации также должен быть обратным: библиотеки DLL должны быть первыми, а приложения - вторыми.Таким образом, Linux ошибается как при инициализации, так и при очистке, а Windows - по крайней мере при очистке.

0 голосов
/ 07 февраля 2019

Вся концепция DLL выходит за рамки стандарта C ++.

В Windows библиотеки DLL могут выгружаться динамически во время выполнения программы.Чтобы помочь в этом, каждая DLL будет обрабатывать уничтожение статических переменных, созданных во время загрузки.В результате статические переменные будут уничтожены в порядке, который зависит от порядка выгрузки DLL (когда они получают уведомление DLL_PROCESS_DETACH). DLL и поведение библиотеки времени выполнения Visual C ++ описывает этот процесс.

...