Как инициализация статической переменной реализована компилятором?

Мне любопытно о базовой реализации статических переменных внутри функции.

Если я объявляю статическую переменную фундаментального типа (char, int, double и т.д.) и даю ему начальное значение, я полагаю, что компилятор просто устанавливает значение этой переменной в самом начале программа до main() вызывается:

void SomeFunction();

int main(int argCount, char ** argList)
{
    // at this point, the memory reserved for 'answer'
    // already contains the value of 42
    SomeFunction();
}

void SomeFunction()
{
    static int answer = 42;
}

Однако, если статическая переменная является экземпляром класса:

class MyClass
{
    //...
};

void SomeFunction();

int main(int argCount, char ** argList)
{
    SomeFunction();
}

void SomeFunction()
{
    static MyClass myVar;
}

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

static bool initialized = 0;
if (!initialized)
{
    // construct myVar
    initialized = 1;
}

Ответ 1

В выводе компилятора, который я видел, функция локальных статических переменных инициализируется точно так, как вы себе представляете.

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

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

Ответ 2

Этот вопрос охватывал аналогичную почву, но безопасность потоков не упоминалась. Для чего это стоит, С++ 0x сделает поток статической инициализации функции безопасным.

(см. С++ 0x FCD, 6.7/4 по статистике функции:" Если элемент управления входит в объявление одновременно, а переменная инициализируется, одновременное выполнение завершение инициализации. ")

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

Ответ 3

Вы правы во всем, включая инициализированный флаг как общую реализацию. Это связано с тем, что инициализация статических локалей не является потокобезопасной и почему существует pthread_once.

Одно небольшое предостережение: компилятор должен испускать код, который "ведет себя так, как будто" статическая локальная переменная строится при первом использовании. Поскольку целочисленная инициализация не имеет побочных эффектов (и не вызывает никакого кода пользователя), она до компилятора, когда она инициализирует int. Пользовательский код не может "законно" узнать, что он делает.

Очевидно, вы можете посмотреть на код сборки или спровоцировать поведение undefined и сделать выводы из того, что на самом деле происходит. Но стандарт С++ не считает это достаточным основанием утверждать, что поведение не "как будто" оно делало то, что говорит спецификация.

Ответ 4

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

Да, это право: и, FWIW, это не обязательно поточно-безопасное (если функция вызывается "в первый раз" двумя потоками одновременно).

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

Ответ 5

Еще один поворот во встроенном коде, где код run-before-main() (cinit/whatever) может скопировать предварительно инициализированные данные (как статики, так и нестатические) в ram из сегмента данных const, возможно, ПЗУ. Это полезно, когда код может не запускаться из какого-либо хранилища (диска), в котором он может быть перезагружен. Опять же, это не нарушает требования языка, поскольку это делается до main().

Незначительное касание. Хотя я не видел многого (вне Emacs), программа или компилятор могли в основном запускать ваш код в процессе и создавать/инициализировать объекты, а затем замораживать и выгружать процесс. Emacs делает что-то похожее на это, чтобы загрузить большое количество elisp (то есть пережевывать его), а затем сбрасывать текущее состояние как рабочий исполняемый файл, чтобы избежать затрат на разбор при каждом вызове.