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

Счетчик Nifty/Schwarz, стандартно совместимый?

Сегодня утром у меня была дискуссия с коллегой о статическом порядке инициализации переменных. Он упомянул счетчик Nifty/Schwarz, и я (вроде) озадачен. Я понимаю, как это работает, но я не уверен, стандартно ли это соответствует стандарту.

Предположим, что 3 следующих файла (первые два являются copy-pasta'd из Дополнительные Идиомы С++):


//Stream.hpp
class StreamInitializer;

class Stream {
   friend class StreamInitializer;
 public:
   Stream () {
   // Constructor must be called before use.
   }
};
static class StreamInitializer {
  public:
    StreamInitializer ();
    ~StreamInitializer ();
} initializer; //Note object here in the header.

//Stream.cpp
static int nifty_counter = 0; 
// The counter is initialized at load-time i.e.,
// before any of the static objects are initialized.
StreamInitializer::StreamInitializer ()
{
  if (0 == nifty_counter++)
  {
    // Initialize Stream object static members.
  }
}
StreamInitializer::~StreamInitializer ()
{
  if (0 == --nifty_counter)
  {
    // Clean-up.
  }
}

// Program.cpp
#include "Stream.hpp" // initializer increments "nifty_counter" from 0 to 1.

// Rest of code...
int main ( int, char ** ) { ... }

... и вот проблема! Существуют две статические переменные:

  • "nifty_counter" в Stream.cpp; и
  • "инициализатор" в Program.cpp.

Поскольку две переменные находятся в двух разных единицах компиляции, официальная гарантия (AFAIK) отсутствует, если nifty_counter инициализируется до 0 до вызова конструктора initializer.

Я могу думать о двух быстрых решениях как о двух, почему это "работает":

  • современные компиляторы достаточно умны, чтобы разрешить зависимость между двумя переменными и поместить код в соответствующем порядке в исполняемый файл (крайне маловероятно);
  • nifty_counter на самом деле инициализируется в режиме "load-time", как говорится в статье, и его значение уже помещено в "сегмент данных" в исполняемом файле, поэтому оно всегда инициализируется "перед запуском любого кода" (высоко вероятно).

Оба они кажутся мне такими, как они зависят от некоторой неофициальной, но возможной реализации. Является ли этот стандарт совместимым или это просто "настолько вероятно работает", что мы не должны беспокоиться об этом?

4b9b3361

Ответ 1

Я считаю, что это гарантировано. Согласно стандарту ($ 3.6.2/1): "Объекты со статической продолжительностью хранения (3.7.1) должны быть инициализированы нулями (8.5) до любой другой инициализации".

Поскольку nifty_counter имеет статическую продолжительность хранения, он инициализируется до создания initializer независимо от распределения между единицами перевода.

Изменить: перечитав рассматриваемый раздел и рассмотрев ввод комментария @Tadeusz Kopec, я менее уверен в том, насколько он четко определен, поскольку он стоит прямо сейчас, но довольно тривиально обеспечить, чтобы он был четко определен: удалить инициализацию из определения nifty_counter, чтобы она выглядела следующим образом:

static int nifty_counter;

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

Ответ 2

Я думаю, что отсутствующий в этом примере способ избежать строительства Stream, это часто не переносится. Помимо отличного счетчика роль инициализаторов состоит в том, чтобы построить что-то вроде:

extern Stream in;

Если один блок компиляции имеет память, связанную с этим объектом, существует ли какой-либо специальный конструктор перед использованием нового оператора new, или в тех случаях, когда я видел, что память выделяется другим способом, чтобы избежать конфликтов, Мне кажется, что в этом потоке есть конструктор no-op, тогда не определяется порядок инициализации или первый конструктор no-op.

Выделение области байтов часто не переносимо, например, для gnu iostream пространство для cin определяется как:

typedef char fake_istream[sizeof(istream)] __attribute__ ((aligned(__alignof__(istream))))
...
fake_istream cin;

llvm использует:

_ALIGNAS_TYPE (__stdinbuf<char> ) static char __cin [sizeof(__stdinbuf <char>)];

Оба делают определенное предположение о пространстве, необходимом для объекта. Где Шварц-счетчик инициализируется с новым размещением:

new (&cin) istream(&buf)

Практически это не выглядит портативным.

Я заметил, что некоторые компиляторы, такие как gnu, microsoft и AIX, имеют расширения для компилятора, чтобы влиять на статический порядок инициализации:

  • Для Gnu это: Включить init-priority с флагом -f и использовать __attribute__ ((init_priority (n))).
  • В окнах с компилятором microsoft есть #pragma (http://support.microsoft.com/kb/104248)