Глобальная инициализация объекта C ++ - PullRequest
2 голосов
/ 04 августа 2011

Многие программисты на C ++ пострадали от ожесточенных столкновений с инициализацией / очисткой глобальных объектов C ++.В конце концов я нашел достаточно хорошее решение этой проблемы, я использую (и радуюсь) это в течение многих лет.У меня вопрос: полностью ли это решение соответствует стандарту C ++ или «зависит от платформы / реализации»?

Проблема

Вообще говоря, есть дваОсновные проблемы с глобальными объектами:

  • Непредсказуемый порядок их постройки / уничтожения.Это кусает, если эти объекты зависят друг от друга.
  • Код построения / разрушения выполняется во время инициализации / очистки CRT вне основной точки входа в программу.Нет способа обернуть этот код с помощью try / catch или выполнить какую-либо предварительную инициализацию.

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

Однако этот метод также имеет некоторые недостатки.Это связано с тем, что отличается не только создание / уничтожение этих объектов, но и различается их доступ .Обычно глобальный объект находится в разделе данных, который выделяется загрузчиком, и его виртуальный адрес известен во время сборки.Использование глобальных указателей приводит к следующим недостаткам:

  • Несколько более медленный доступ к объекту, дополнительная разыменование указателя.Во время выполнения вместо предположения, что объект находится по указанному адресу, компилятор генерирует код, который разыменовывает глобальный указатель.
  • Более слабые оптимизации.Компилятор может не понимать, что указатель всегда указывает на один и тот же объект.
  • Если фактические объекты размещены в куче:
    • Хуже производительность (выделения кучи "тяжелые"")
    • Фрагментация памяти
    • Вероятность исключения нехватки памяти
  • Если фактические объекты размещены в стеке (автоматические переменные в main):
    • Размер стека обычно ограничен.В некоторых обстоятельствах его потребление "толстыми" объектами неоптимально.

Решение

Решение, которое я нашел, заключается впереопределить операторы new / delete.

// class definition
class MyObject
{
    // some members
    // ...

    static char s_pMyPlaceholder[];

public:

    // methods
    // ...

    static MyObject& Instance()
    {
        return *(MyObject*) s_pMyPlaceholder;
    }

    void* operator new (size_t) { return s_pMyPlaceholder; }
    void operator delete (void*) {}
};

// object placeholder instantiated
char MyObject::s_pMyPlaceholder[sizeof(MyObject)];

void main()
{
    // global initialization
    std::auto_ptr<MyObject> pMyObj(new MyObject);

    // run the program
    // ...

}

Хитрость заключается в том, чтобы выделить достаточно места в глобальной памяти (объявив глобальный массив соответствующего размера), а затем использовать фиктивное выделение памятидля необходимого объекта, который будет «распределять» эту глобальную память ». Таким образом мы достигаем следующего:

  • Семантически мы выделяем объект динамически. Следовательно, мы имеем полный контроль над его временем жизни.
  • На самом деле объект находится в глобальной памяти 1080 *. Следовательно, все недостатки, связанные с "указательным методом", неприменимы к нашему случаю.
  • Объект виден везде вПрограмма вызывает один MyObject::Instance(), чтобы получить ссылку на него. И, кстати, этот вызов функции легко встроен компилятором.

Так что все выглядит нормально с этим методомд.Мне просто любопытно, законно ли это с точки зрения стандарта C ++.

Ответы [ 3 ]

3 голосов
/ 04 августа 2011

Я вижу две проблемы с этим, одна проблема легальности и одна проблема юзабилити.

Первая проблема - выравнивание: MyObject::s_pMyPlaceholder не гарантируется, что будет правильно выровнено для удержания MyObject.

Вторая проблема заключается в том, что вы ограничиваете себя одним объектом типа MyObject. Создайте секунду, и вы перезаписали первую без предупреждения.

Я бы предложил использовать boost::optional для отсрочки инициализации объектов.

2 голосов
/ 04 августа 2011

Я не думаю, что у вас есть формальная гарантия того, что ваше решение работает на каждой совместимой реализации, потому что стандарт C ++ не гарантирует, что статически расположенные массивы char выровнены так, как этого требует любой объект такого же размера.

1 голос
/ 04 августа 2011

Начиная с 3.7.3.1 (функции распределения, [basic.stc.dynamic.allocation]) (ИСО / МЭК 14882/2003):

2 / [...] Возвращаемый указатель долженбыть соответствующим образом выровненным, чтобы его можно было преобразовать в указатель любого полного типа объекта, а затем использовать для доступа к объекту или массиву в выделенном хранилище (до тех пор, пока хранилище не будет явно освобождено путем вызова соответствующей функции освобождения).

Я сомневаюсь, что вы не можете точно гарантировать, что адрес s_MyPlaceHolder[0] выровнен правильно.

Я не вижу ничего плохого (в однопоточной среде) с:

#include <cstdlib>

class MyObject
{
    static MyObject* instance;

    static void release_instance() { delete instance; }

public:
    static MyObject& get_instance()
    {
        if (!instance) 
        {
            instance = new MyObject();
            std::atexit(&release_instance);
        }

        return *instance;
    }
};

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

Поскольку вызаинтересованы в управлении временем жизни объекта, вы можете использовать RAII:

class MyObject
{
    MyObject() { ... }
    ~MyObject() { ... }

    // Never defined
    MyObject(const MyObject&);
    void operator=(const MyObject&);


    static MyObject* instance = 0;
    static void create_instance()
    {
        if (instance) throw std::logic_error("Instance already created");
        else instance = new MyObject();
    }

    static void release_instance() { delete instance; }

public:
    struct InstanceKeeper
    {
        InstanceKeeper(InstanceKeeper& x) : must_clean(x.must_clean)
        { x.must_clean = false; }

        ~InstanceKeeper() { if (must_clean) release_instance(); }

    private:
        friend class MyObject;
        InstanceKeeper() : must_clean(true) { create_instance(); }  

        bool must_clean;
    };

    friend struct InstanceKeeper;
    static InstanceKeeper instance_keeper() { return InstanceKeeper(); }  

    static MyObject& instance()
    {
        if (!instance) throw std::logic_error("Instance not created");
        return *instance;
    }
};

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

int main()
{ 
    MyObject::InstanceKeeper k = MyObject::instance_keeper();

    MyObject::instance().do_something();
    ...

}

Вы даже можете передавать объект InstanceKeeper в функции, этоТо же поведение, что и std::auto_ptr.

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

...