Статическая инициализация С++ через счетчик Шварца

A Счетчик Schwartz предназначен для обеспечения того, чтобы глобальный объект инициализировался до его использования.

Пожалуйста, обратите внимание на использование счетчика Schwartz, показанного ниже.

Файл Foo.h:

class Foo
{
   Foo::Foo();
};

Файл Foo.cpp:

#include "Foo.h"

// Assume including Mystream.h provides access to myStream and that
// it causes creation of a file-static object that initializes
// myStream (aka a Schwartz counter).
#include "MyStream.h"

Foo::Foo()
{
   myStream << "Hello world\n";
}

Если Foo:: Foo() запускается после запуска main(), использование myStream гарантированно будет безопасным (т.е. myStream будет инициализирован перед использованием) из-за объекта статического инициализатора файла, упомянутого в комментариях.

Однако предположим, что экземпляр Foo создается до начала main(), как это было бы, если бы оно было глобальным. Это показано здесь:

Файл Global.cpp:

#include "Foo.h"

Foo foo;

Обратите внимание, что Global.cpp не получает файл-статический объект инициализации, например Foo.cpp,. В этом случае, как счетчик Schwartz гарантирует, что инициализатор MyStream (и, следовательно, сам объект MyStream) инициализируется до foo? Или может ли счетчик Шварца потерпеть неудачу в этом случае?

Ответ 1

Использование счетчиков Schwartz (так называемый после Джерри Шварца, который разработал основы библиотеки IOStreams, поскольку теперь он находится в стандарте; обратите внимание, что он может нельзя обвинять многих нечетных вариантов, поскольку они были помечены на исходный проект) может привести к доступу к объектам до их создания. Наиболее очевидным сценарием является вызов функции во время построения глобального объекта, который вызывает в другую единицу перевода, используя свой собственный глобальный объект, построенный с помощью счетчика Шварца (я использую std::cout как глобальный, защищенный счетчиком Шварца, чтобы сохранить пример короткий):

// file a.h
void a();

// file a.cpp
#include <iostream>
void a() { std::cout << "a()\n"; }

// file b.cpp
#include <a.h>
struct b { b() { a(); } } bobject;

Если глобальные объекты в файле b.cpp создаются до тех, что находятся в файле a.cpp, и если std::cout создается с помощью счетчика Шварца с a.cpp, который является первым экземпляром, этот код не будет выполнен. Есть, по крайней мере, две другие причины, по которым счетчики Шварца не работают особенно хорошо:

  • При использовании для этого глобального объекта эти объекты заканчиваются дважды. Хотя это работает на практике, когда все делается правильно, я думаю, что это уродливо. Обход для этого состоит в том, чтобы использовать буфер char соответствующего размера для фактического определения объекта (обычно они искажаются именем как объект правильного типа). Однако в обоих случаях все беспорядочно.
  • Когда глобальные объекты, защищенные счетчиком Шварца, используются во многих единицах перевода (как это имеет место для std::cout), это может вызвать значительную задержку запуска: хорошо написанный код обычно не использует глобальную инициализацию но счетчик Schwartz должен запускать кусок кода для каждого из объектных файлов, которые необходимо загрузить.

Лично я пришел к выводу, что этот метод - отличная идея, но на практике это не работает. Вместо этого я использую три подхода:

  • Не используйте глобальные объекты. Это делает эту дискуссию устаревшей и лучше всего работает в параллельном коде. Если глобальный ресурс абсолютно необходим, статический объект функции, возвращенный ссылкой и инициализированный с использованием std::call_once(), является гораздо лучшей альтернативой.
  • Размещение глобального объекта в соответствующем месте при связывании исполняемого файла (например, последний) заставляет его сначала инициализировать. Я экспериментировал с этим в прошлом и обратно, тогда я обнаружил, что могу разместить объектные файлы надлежащим образом во всех системах, о которых я заботился. Основной недостаток здесь заключается в том, что нет никаких гарантий, и все может измениться при переключении между версиями компилятора. Однако для стандартной библиотеки С++ это приемлемо (и я только заботился о глобальных объектах потока, когда я это делал).
  • Поместите глобальные объекты в выделенную общую библиотеку: при загрузке разделяемой библиотеки выполняется ее код инициализации. Объекты в общей библиотеке становятся доступными только после завершения инициализации. Я обнаружил, что это работает надежно, но требует дополнительной библиотеки.