Что означает идиома С++, эквивалентная статическому блоку Java?

У меня есть класс с некоторыми статическими членами, и я хочу запустить некоторый код для их инициализации (предположим, что этот код не может быть преобразован в простое выражение). В Java я просто делал

class MyClass {
    static int myDatum;

    static {
        /* do some computation which sets myDatum */
    }
}

Если я ошибаюсь, С++ не допускает таких статических блоков кода, правильно? Что я должен делать вместо этого?

Мне нужно решение для обоих следующих параметров:

  • Инициализация происходит при загрузке процесса (или когда DLL с этим классом загружается).
  • Инициализация происходит, когда сначала создается экземпляр класса.

Для второго варианта я думал:

class StaticInitialized {
    static bool staticsInitialized = false;

    virtual void initializeStatics();

    StaticInitialized() {
        if (!staticsInitialized) {
            initializeStatics();
            staticsInitialized = true;
        }
    }
};

class MyClass : private StaticInitialized {
    static int myDatum;

    void initializeStatics() {
        /* computation which sets myDatum */
    }
};

но это невозможно, так как С++ (на данный момент?) не позволяет инициализировать непостоянные статические члены. Но, по крайней мере, это уменьшает проблему статического блока до статической инициализации с помощью выражения...

Ответ 1

Вы также можете иметь статические блоки в C++ - вне классов.

Оказывается, мы можем реализовать статический блок в стиле Java, хотя и вне класса, а не внутри него, то есть в области видимости блока перевода. Реализация немного уродливая под капотом, но при использовании она довольно элегантна!

Загружаемая версия

Теперь есть GitHub-репозиторий для решения, содержащий единственный заголовочный файл: static_block.hpp.

использование

Если вы напишите:

static_block {
    std::cout << "Hello static block world!\n";
}

этот код будет выполняться перед вашим main(). И вы можете инициализировать статические переменные или делать все что угодно. Таким образом, вы можете поместить такой блок в файл реализации вашего класса .cpp.

Заметки:

  • Вы должны окружить свой статический блочный код фигурными скобками.
  • Относительный порядок выполнения статического кода не гарантируется в C++.

Реализация

В реализации статического блока используется фиктивная переменная, которая статически инициализируется с помощью функции. Ваш статический блок на самом деле является телом этой функции. Чтобы гарантировать, что мы не столкнемся с какой-либо другой фиктивной переменной (например, из другого статического блока - или где-либо еще), нам нужно немного макро-машин.

#define CONCATENATE(s1, s2) s1##s2
#define EXPAND_THEN_CONCATENATE(s1, s2) CONCATENATE(s1, s2)
#ifdef __COUNTER__
#define UNIQUE_IDENTIFIER(prefix) EXPAND_THEN_CONCATENATE(prefix, __COUNTER__)
#else
#define UNIQUE_IDENTIFIER(prefix) EXPAND_THEN_CONCATENATE(prefix, __LINE__)
#endif // __COUNTER__

и вот макрос работы по объединению вещей:

#define static_block STATIC_BLOCK_IMPL1(UNIQUE_IDENTIFIER(_static_block_))

#define STATIC_BLOCK_IMPL1(prefix) \
    STATIC_BLOCK_IMPL2(CONCATENATE(prefix,_fn),CONCATENATE(prefix,_var))

#define STATIC_BLOCK_IMPL2(function_name,var_name) \
static void function_name(); \
static int var_name __attribute((unused)) = (function_name(), 0) ; \
static void function_name()

Заметки:

  • Некоторые компиляторы не поддерживают __COUNTER__ - это не является частью стандарта C++; в этих случаях код выше использует __LINE__, который тоже работает. GCC и Clang поддерживают __COUNTER__.
  • Это C++ 98; вам не нужны никакие конструкции C++ от 14.11.17. Тем не менее, это не допустимый C, несмотря на то, что он не использует никаких классов или методов
  • __attribute ((unused)) можно удалить или заменить на [[unused]] если у вас есть компилятор C++ 11, которому не нравится неиспользуемое расширение в стиле GCC.
  • Это не предотвращает и не помогает с фиаско статического порядка инициализации, поскольку, хотя вы знаете, что ваш статический блок будет выполняться до main(), вы не гарантированы, когда именно это произойдет относительно других статических инициализаций.

Live Demo

Ответ 2

Для # 1, если вам действительно нужно инициализировать загрузку процесса/библиотеку, вам придется использовать что-то специфичное для платформы (например, DllMain в Windows).

Однако, если вам достаточно запустить инициализацию перед любым кодом из того же .cpp файла, что и при выполнении статики, должно работать следующее:

// Header:
class MyClass
{
  static int myDatum;

  static int initDatum();
};

 

// .cpp file:
int MyClass::myDatum = MyClass::initDatum();

Таким образом, initDatum() должен быть вызван до того, как будет выполнен любой код из этого файла .cpp.

Если вы не хотите загрязнять определение класса, вы также можете использовать Lambda (С++ 11):

// Header:
class MyClass
{
  static int myDatum;
};

 

// .cpp file:
int MyClass::myDatum = []() -> int { /*any code here*/ return /*something*/; }();

Не забывайте последнюю пару круглых скобок, которая на самом деле вызывает лямбда.


Что касается № 2, есть одна проблема: вы не можете вызвать виртуальную функцию в конструкторе. Вам лучше сделать это вручную в классе, вместо того, чтобы использовать для него базовый класс:

class MyClass
{
  static int myDatum;

  MyClass() {
    static bool onlyOnce = []() -> bool {
      MyClass::myDatum = /*whatever*/;
      return true;
    }
  }
};

Предполагая, что класс имеет только один конструктор, который будет работать нормально; он является потокобезопасным, поскольку С++ 11 гарантирует такую ​​безопасность для инициализации статических локальных переменных.

Ответ 3

Вы можете инициализировать статические элементы данных в С++:

#include "Bar.h"

Bar make_a_bar();

struct Foo
{
    static Bar bar;
};

Bar Foo::bar = make_a_bar();

Возможно, вам придется подумать о зависимостях между переводами, но об общем подходе.

Ответ 4

Вот хороший способ имитировать блок static, используя С++ 11:

Макрос

#define CONCATE_(X,Y) X##Y
#define CONCATE(X,Y) CONCATE_(X,Y)
#define UNIQUE(NAME) CONCATE(NAME, __LINE__)

struct Static_ 
{
  template<typename T> Static_ (T only_once) { only_once(); }
  ~Static_ () {}  // to counter "warning: unused variable"
};
// `UNIQUE` macro required if we expect multiple `static` blocks in function
#define STATIC static Static_ UNIQUE(block) = [&]() -> void

Использование

void foo ()
{
  std::cout << "foo()\n";
  STATIC
  {
    std::cout << "Executes only once\n";
  };  
}

Демо.

Ответ 5

В C++ такой идиомы нет.

Причина кроется в совершенно другом характере кода, созданного с помощью C++: время выполнения не "управляется". В сгенерированном коде после компиляции больше нет понятия "класс", и нет такой вещи, как объекты кода, загружаемые по требованию "загрузчиком классов".

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

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

Но вы не должны предполагать ничего сверх этого; особенно вы никогда не можете быть уверены, действительно ли и когда эта инициализация выполняется. Это предупреждение действительно. Особенно не предполагайте никаких побочных эффектов такого кода инициализации. Для компилятора совершенно законно заменить такой код на что-то, что считается "эквивалентным" компилятором. Будущие версии компилятора могут считаться все более и более умными в этом отношении. Возможно, ваш код работает, но может работать с разными флагами оптимизации, с другим процессом сборки, с более новой версией компилятора.


практический намек: если вы окажетесь в ситуации, когда у вас есть несколько статических переменных, которые вам нужно правильно инициализировать, то есть вероятность, что вы хотите разложить их на класс. Затем этот класс может иметь обычный конструктор и деструктор для инициализации/очистки. Затем вы можете разместить экземпляр этого вспомогательного класса в статическую переменную (класс). C++ дает очень сильные гарантии согласованности при вызове ctors и dtors классов, для чего-либо доступного по официальным средствам (без приведения, без обмана на низком уровне).

Ответ 6

Вам может быть лучше вообще придерживаться другого подхода. Нужно ли собирать статическую информацию внутри StaticInitialized?

Рассмотрим создание отдельного одноэлементного класса SharedData. Первый клиент, который вызывает SharedData:: Instance(), затем инициирует создание коллекции общих данных, которые будут просто нормальными данными класса, хотя и живут внутри отдельного экземпляра объекта, который распределяется статически:

//SharedData.h

class SharedData
{
public:
    int m_Status;
    bool m_Active;

    static SharedData& instance();
private:
    SharedData();
}

//SharedData.cpp

SharedData::SharedData()
: m_Status( 0 ), m_Active( true )
{}

// static 
SharedData& SharedData::instance()
{
    static SharedData s_Instance;
    return s_Instance;
}

Любой клиент, заинтересованный в совместном сборе данных, теперь должен получить к нему доступ через Singleton SharedData, и первый такой клиент, который будет вызывать SharedData:: instance(), инициирует настройку этих данных в SharedData ctor, что только когда-либо вызывается один раз.

Теперь ваш код предполагает, что разные подклассы могут иметь свои собственные способы инициализации статических данных (через полиморфный характер initializeStatics()). Но это кажется довольно проблематичной идеей. Являются ли множественные производные классы действительно предназначенными для совместного использования одного набора статических данных, но каждый подкласс инициализирует его по-другому? Это просто означало бы, что какой бы класс не был построен, первым был бы тот, кто установил статические данные своим собственным приходским способом, а затем каждый другой класс должен был бы использовать эту настройку. Это действительно то, что вы хотите?

Я также немного смущен, почему вы пытаетесь совместить полиморфизм с частным наследованием. Количество случаев, когда вы действительно хотели бы использовать личное наследование (в отличие от композиции), очень мало. Мне осталось интересно, если вы каким-то образом считаете, что вам нужно, чтобы initializeStatics() был виртуальным, чтобы производный класс мог его называть. (Это не так). Однако вы, кажется, хотите переопределить initializeStatics() в производном классе по причинам, которые мне не понятны (см. Ранее). Что-то кажется пугающим в отношении всей установки.