3 окт. 2009 г.

Игры: статическая всячина

Даже самые простые вещи иногда заставляют задуматься. Обычно это случается из-за отсутствия достаточного опыта в конкретном вопросе. Как получить такой опыт? Ну конечно же, поиграть!

Я надеюсь, это не последний пост в этом блоге. Под рубрикой "Игры" я собираюсь размещать описания вещей которые вы давно хотели попробовать сами, но из-за лени или высокой занятости боялись это сделать. Итак, давайте играть!

Сегодня я хочу поиграть со статическими данными в С++.

Теоретические представления о том, что же такое static и каким образом его следует использовать можно получить прочитав первые N ссылок по запросу static keyword [C++]. Поэтому, сразу переходим к практике.

В своих экспериментах я буду использовать простой класс, при создании и удалении объектов которого на печать будет выводиться строка, переданная в качестве аргумента в конструктор:
#include <stdio.h>
class CThing
{
    CThing();
    const char *const m_name;
public:
    CThing(const char *const name): m_name(name)
    { printf("CThing::CThing(\"%s\")\n", name); }
    ~CThing()
    { printf("CThing::~CThing(\"%s\")\n", m_name); }
    void echo() const { printf("echo from \"%s\"\n", m_name);
}
Для начала, посмотрим когда вызывается конструктор и деструктор статических переменных:
void someProc1()
{
    static CThing thing("thing in someProc1");
}
CThing thing1("thing1");
int main(int argc, char *argv[])
{
    (void)argc; (void)argv;
    printf("---- main() ----\n");
    someProc1();
    someProc1();
    printf("---- return ----\n");
    return 0;
}
После запуска программы получаем:
CThing::CThing("thing1")
---- main() ----
CThing::CThing("thing in someProc1")
---- return ----
CThing::~CThing("thing in someProc1")
CThing::~CThing("thing1")
Как видно, "thing in someProc1" действительно создаётся на момент первого вызова функции someProc1(). Деструкторы же, как и положено, вызываются в обратном порядке вызова конструкторов. Некоторые люди не до конца понимают как работают статические переменные и в связи с этим наделяют компилятор магическими свойствами. На самом деле, конечено же, никакой магии нет.
Определение статической переменной:
void someProc1()
{
    static CThing thing("thing");
}
компилятор преобразует примерно в следующий код:
bool g_thingInitialized = false;
CThing *g_thingPtr;
void g_thingDelete() { delete g_thingPtr; }
void someProc1()
{
    if (!g_thingInitialized)
    {
        g_thingInitialized = true;
        g_thingPtr = new CThing("thing");
        atexit(g_thingDelete);
    }
}
Вот так просто. И именно с такой простой реализацией статических переменных связаны следующие две особенности (которые очень подробно описаны в C++ scoped static initialization is not thread-safe, on purpose!):
  1. Вопреки расхожему мнению, факт инициализированности статической переменной будет проверяться каждый раз при входе в функцию.
  2. Использование статических переменных не потоко безопасно. Компилятор ничего не знает о потоках и не обеспечивает синхронизации при инициализации переменной. Таким образом, может быть создано сразу несколько экземпляров класса CThing (плюс целый букет неприятностей, который можно получить, понадеявшись, что всю работу по синхронизации компилятор выполнит сам).
Теперь рассмотрим статические члены классов:
class CBigThing
{
    static CThing m_thing;
    const char *const m_name;
public:
    CBigThing(const char *const name): m_name(name)
    { printf("CBigThing::CBigThing(\"%s\")\n", name); }
    ~CBigThing()
    { printf("CBigThing::~CBigThing(\"%s\")\n", m_name); }
};

class CAnyThing
{
    static CThing m_thing;
    const char *const m_name;
public:
    CAnyThing(const char *const name): m_name(name)
    { printf("CAnyThing::CAnyThing(\"%s\")\n", name); }
    ~CAnyThing()
    { printf("CAnyThing::~CAnyThing(\"%s\")\n", m_name); }
};

CThing CAnyThing::m_thing("m_thing from CAnyThing");
CThing thing1("thing1");
CThing CBigThing::m_thing("m_thing from CBigThing");

int main(int argc, char *argv[])
{
    (void)argc; (void)argv;
    printf("---- main() ----\n");
    CBigThing bigThing("bigThing");
    printf("---- return ----\n");
    return 0;
}
После компиляции и запуска получаем:
CThing::CThing("m_thing from CAnyThing")
CThing::CThing("thing1")
CThing::CThing("m_thing from CBigThing")
---- main() ----
CBigThing::CBigThing("bigThing")
---- return ----
CBigThing::~CBigThing("bigThing")
CThing::~CThing("m_thing from CBigThing")
CThing::~CThing("thing1")
CThing::~CThing("m_thing from CAnyThing")
Как можно видеть, статические члены классов создаются не в момент создания первого экземпляра класса (как можно было бы предположить). Они создаются вместе с другими глобальными переменными
в порядке следования определений (и уничтожаются в обратном порядке).

Стоит заметить, что порядок создания глобальных переменных (как и статических членов классов) точно известен только для переменных определённых в одном файле. Порядок создания переменных определённых в разных файлах не определён и зависит от факторов, на которые вы скорее всего не захотите положиться.
Вот наглядный пример того, что может произойти если неаккуратно использовать глобальные переменные из разных файлов:
// it CAN be in file actions.cpp:
extern CThing thingA;
extern CThing thingB;

bool doThings()
{
    thingA.echo();
    thingB.echo();
    return true;
}

bool doneThings = doThings();

// it CAN be in file things.cpp:
CThing thingA("thingA");
CThing thingB("thingB");

// it CAN be in file main.cpp:
int main(int argc, char *argv[])
{
    (void)argc; (void)argv;
    printf("---- main() ----\n");
    printf("---- return ----\n");
    return 0;
}
Если скомпилировать это одним файлом, то получим следующий вывод:
echo from "(null)"
echo from "(null)"
CThing::CThing("thingA")
CThing::CThing("thingB")
---- main() ----
---- return ----
CThing::~CThing("thingB")
CThing::~CThing("thingA")
Из чего видно, что объекты thingA и thingB были использованы не инициализированными.
Когда код сосредоточен в одном файле, причина такого поведения очевидна - doneThings определяется до thingA и thingB. Однако, если разнести код по файлам actions.cpp, things.cpp и main.cpp (как указано в комментариях), то ситуация становится более интересной. Программа может заработать как надо, выводя на печать:
CThing::CThing("thingA")
CThing::CThing("thingB")
echo from "thingA"
echo from "thingB"
---- main() ----
---- return ----
CThing::~CThing("thingB")
CThing::~CThing("thingA")
А может и нет. Зависит от реализации используемого компилятора (его версии, имён исходных файлов, порядка указания файлов в командной строке компилятора, ...). Думаю никто не захочет иметь программу, которая может сломаться при добавлении нового файла или при переименовании уже существующего.

Всё, пожалуй достаточно для начала :)

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

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