пятница, 15 июля 2016 г.

C++: загадки инициализации глобальных объектов

Давайте напишем простую структуру, объекты которой смогут выводить на экран время старта программы. Файл pst.h
#include <ctime>

struct ProgramStartTime
{
    ProgramStartTime();

    char *  operator()( void ) const;
    time_t  now_;
};

extern ProgramStartTime  pst_ref;
Файл pst.cc
#include "pst.h"

namespace
{
    time_t  now( time( NULL ) );
}

ProgramStartTime::ProgramStartTime() : now_( now )
{
}

char *  ProgramStartTime::operator()( void ) const
{
    return ctime( &now_ );
}

ProgramStartTime  pst_ref;
Все просто. Время старта программы будем отсчитывать с момента динамической инициализации глобальной переменной now. Функция operator()( void ) структуры ProgramStartTime будет возвращать время now_, инициализированное значением now в конструкторе ProgramStartTime и преобразованное в удобоваримую форму с помощью функции ctime(). Глобальный объект pst_ref будет опорным объектом типа ProgramStartTime в дальнейшем исследовании. А теперь функция main() с собственным глобальным объектом pst. Файл test.cc
#include <iostream>
#include "pst.h"

ProgramStartTime  pst;

int  main( void )
{
    std::cout.width( 22 );
    std::cout << "Start time: " << pst()
              << "Reference start time: " << pst_ref();
}
Компилируем и запускаем программу test.
g++ -Wall -o test pst.cc test.cc
./test
          Start time: Fri Jul 15 09:40:11 2016
Reference start time: Fri Jul 15 09:40:11 2016
Все хорошо, не так ли? Да ну! А как вам такое?
g++ -Wall -o test test.cc pst.cc
./test
          Start time: Thu Jan  1 03:00:00 1970
Reference start time: Thu Jan  1 03:00:00 1970
Ух ты, семидесятый год, начало эпохи! Видимо глобальный объект now был инициализирован нулем! И что самое интересное, мы ведь не меняли исходный код программы, а просто изменили порядок перечисления исходных файлов в списке аргументов g++. Давайте копнем глубже, распечатаем значения членов now_ в объектах pst и pst_ref.
#include <iostream>
#include "pst.h"

ProgramStartTime  pst;

int  main( void )
{
    std::cout.width( 22 );
    std::cout << "Start time: " << pst()
              << "Reference start time: " << pst_ref();
    std::cout.width( 28 );
    std::cout << "Plain start time: " << pst.now_ << std::endl
              << "Plain reference start time: " << pst_ref.now_ << std::endl;
}
g++ -Wall -o test test.cc pst.cc
./test
          Start time: Thu Jan  1 03:00:00 1970
Reference start time: Thu Jan  1 03:00:00 1970
          Plain start time: 0
Plain reference start time: 1468565544
Ага, в объекте pst_ref хранится хорошее время, а в объекте pstноль, то есть начало эпохи. Отложим разбор этого странного факта на потом, пока нужно хотя бы понять, почему функция ctime() в обоих случаях возвращает начало эпохи. Справочное руководство по ctime() говорит, что эта функция возвращает результат в статическом буфере, поэтому она не является потокобезопасной. Нас в данном случае интересует другое. Возвращаемые значения, будучи помещенными несколько раз в один участок памяти в пределах одного выражения (sequence в смысле потока выполнения), могут оказаться одинаковыми: в конце концов это один и тот же участок памяти! Помните известное правило о непредсказуемости порядка вычисления функций в одном выражении? Так вот, в данном случае с функциями, возвращающими результат в глобальном объекте-указателе, это правило распространяется и на возвращаемый результат. Это и есть мораль номер 1. Функции, возвращающие результат вычисления в глобальных объектах, помимо общеизвестных недостатков, таких как небезопасность в многопоточной среде, обладают рядом менее известных проблем, таких как неопределенное поведение с точки зрения возвращаемого значения в пределах одного выражения. Давайте разобьем единое выражение с вызовами ctime() на два и продолжим выяснять, почему в объектах pst и pst_ref хранятся разные значения now_ и как это зависит от порядка перечисления исходных файлов в списке аргументов g++.
#include <iostream>
#include "pst.h"

ProgramStartTime  pst;

int  main( void )
{
    std::cout.width( 22 );
    std::cout << "Start time: " << pst();
    std::cout << "Reference start time: " << pst_ref();
    std::cout.width( 28 );
    std::cout << "Plain start time: " << pst.now_ << std::endl
              << "Plain reference start time: " << pst_ref.now_ << std::endl;
}
g++ -Wall -o test test.cc pst.cc
./test
          Start time: Thu Jan  1 03:00:00 1970
Reference start time: Fri Jul 15 10:29:32 2016
          Plain start time: 0
Plain reference start time: 1468567772
Теперь все хорошо с точки зрения соответствия между значениями now_ и вызовами ctime( &now_ ). Но что же не так с самим значением now_ в объекте pst? Обратим внимание на то, что глобальные объекты pst и now определены в разных файлах, то бишь единицах трансляции (translation unit). Вот вам и объяснение и мораль номер 2. Корректная динамическая инициализация зависимых глобальных объектов в разных единицах трансляции не гарантируется. В нашем примере объект pst в лице конструктора его типа зависит от глобального объекта now, динамически (то есть во время выполнения программы) инициализируемого значением, возвращаемым функцией time(). Заметим, что pst тоже инициализируется динамически, и в этом вся соль. Порядок такой инициализации между разными единицами трансляции не определен, несмотря на то, что один из объектов зависит от другого: C++ не отслеживает такого рода зависимости. Зато статическая инициализация глобальных объектов нулями (в том числе и тех, которые в дальнейшем будут инициализированы динамически) происходит раньше любой динамической, вот почему член now_ в объекте pst равен нулю, а не какому-нибудь другому произвольному значению. С другой стороны, объект pst_ref всегда инициализируется корректно. Но это и не удивительно: его определение находится после определения now в той же единице трансляции, а в этом случае стандарт гарантирует последовательную динамическую инициализацию глобальных объектов. В том, что изменение порядка перечисления исходных файлов в списке аргументов g++ повлияло на корректность программы, нет никакого правила: этот эффект нужно рассматривать как особенность конкретного линковщика на конкретной платформе. И, наконец, мораль номер 3 (позитивная). Чтобы избежать эффектов динамической инициализации глобальных объектов, пользуйтесь локальными статическими объектами, возвращаемыми из специально подготовленных для этого функций-оберток. Динамическая инициализация локальных статических объектов происходит после первого обращения к функции-обертке, поэтому, при условии, что эта функция-обертка в первый раз вызывается из функции main() или глубже по стеку, все глобальные объекты, от которых зависит возвращаемый статический объект, будут гарантированно инициализированы до его собственной инициализации. Условие динамической инициализации локальных статических объектов после первого вызова функции, в которой они определены, позволяет настроить цепочку инициализаций зависимых статических объектов (или так называемый dependency tracking) при условии, что все они размещены как локальные статические объекты в собственных функциях-обертках. Я покажу, как в нашем примере обернуть глобальный объект pst (я не стал оборачивать объект now ради формирования цепочки инициализаций, поскольку в этом случае время его инициализации будет совпадать со временем первого вызова функции-обертки и, в принципе, оно может сильно не совпадать со временем старта программы).
#include <iostream>
#include "pst.h"

ProgramStartTime *  getPst( void )
{
    static ProgramStartTime  pst;
    return &pst;
}

int  main( void )
{
    std::cout.width( 22 );
    std::cout << "Start time: " << getPst()->operator()();
    std::cout << "Reference start time: " << pst_ref();
    std::cout.width( 28 );
    std::cout << "Plain start time: " << getPst()->now_ << std::endl
              << "Plain reference start time: " << pst_ref.now_ << std::endl;
}
g++ -Wall -o test test.cc pst.cc
./test
          Start time: Fri Jul 15 11:41:47 2016
Reference start time: Fri Jul 15 11:41:47 2016
          Plain start time: 1468572107
Plain reference start time: 1468572107

Комментариев нет:

Отправить комментарий