CClass {
public:
static CClass* factory (void);
private:
CClass () {}
};
CClass* CClass::factory(void) { return new CClass(); }
// Где-то в коде
CClass* cc = CClass::factory(void);
// Вариант 2. Дружественная функция.
CClass {
friend CClass* factory (void);
private:
CClass () {}
};
// Дружественная Функция, создающая экземпляры класса.
CClass* factory (void) {
return new CClass;
}
// Где-то в коде
CClass* cc = factory(void);
Вы видите, что разницы между двумя вариантами практически нет? Единственно, что дружественная функция лежит вне области видимости класса. Но она фактически является элементом его интерфейса! Именно это наблюдение позволило Мейерсу сделать несколько неожиданный вывод: дружественные функции могут улучшать инкапсуляцию класса! Не знаю, как для Вас, но мне пришлось прочитать его статью дважды, а потом еще найти перевод на русский язык, потому как сразу это не в голове не уложилось. Подробности читайте в 'С++ Journal', апрель 2000 года.
Желая продолжить изыскания в области ограничения конструирования, зададим вопрос: А можно ли совсем запретить конструирование экземпляров класса, даже для друзей и для статических функций? Ответ: Да. Можно. Нужно сделать как минимум одну функцию чистой виртуальной (pure virtual). Для этого есть специальный синтаксис:
virtual void f(void)=0;
В этом случае компилятор не может создать для класса виртуальную таблицу, и соответственно не может создать экземпляр.
Вернемся опять к статической функции. Статическую функцию класса можно вызвать двумя способами - указав либо имя класса, либо через экземпляр класса.
CClass* cc1 = CClass::factory(void);
CClass* cc2 = cc1-›factory(void); // Вызов производящей функции
// Не знаю, откуда мы его берем, но это стековый экземпляр
CClass cc3;
CClass* cc4 = cc3.factory(void); // Еще один вызов производящей функции
Тут-то и делается самый прикол. Мы делаем виртуальный конструктор: виртуальную производящую функцию:
CClass {
public:
// Теперь виртуальная, а не статическая.
virtual CClass* factory (void);
// Конструктор делаем для простоты открытым,
// поскольку все-таки нам нужен
// базовый способ получения экземпляров
CClass () {}
};
CClass* CClass::factory(void) { return new CClass(); }
// Где-то в коде
CClass* cc = new CClass();
// Виртуальное конструирование!!!
CClass* cc1 = cc-›factory(void);
Думаю, что на этом следует закончить этот шаг. К конструированию объектов мы будем возвращаться еще не раз… но не сегодня.
Примером производящих функций являются макросы DECLARE_SERIAL, IMPLEMENT_SERIAL, DECLARE_DYNCREATE, IMPLEMENT_DYNCREATE в MFC. Они конечно сложнее и делают много чего еще, но в конечном итоге это замазанные макросом производящие функции.
Шаг 12 - Двухэтапная инициализация.
Когда мы создаем нестековый экземпляр, то пишем такой код:
CClass* cc = new CClass();
Попробуем поразбираться. new - это глобальный оператор с определением:
void* operator new (size_t bytes);
Он получает от компилятора количество байт, необходимое для хранения объекта, а потом передает управление конструктору, чтобы тот правильно произвел нужные инициализации. То есть, в одном выражении исполняется два совершенно разных логических действия:
1. Выделение памяти;
2. Конструирование.
Оба действия могут кончиться неудачей. Либо память не выделится, тогда негде будет инициализировать объект, либо память выделится, но инициализация будет неудачной. С 1998 года стандарт C++ предусматривает, что если инициализация прошла неудачно, то выделенная память должна автоматически освободиться - то есть вызваться оператор delete, но без передачи управления деструктору. До того это оставалось на совести разработчика компилятора, и довольно часто выделенная память могла застрять, и больше не вернуться в систему. Кроме того, конструктор ничего не возвращает. Только что проверить на NULL. Ну еще конечно исключения, да… но все так сложно, елы… Короче, не след бы нам смешивать разные вещи, даже если это совсем не суп и не мухи, а совсем выделение памяти и инициализация.
В какой-то степени по этой причине, но так же и по некоторым другим соображениям, в C+ + применяется прием двухэтапной инициализации. На мой взгляд это не есть идиома, а довольно простой прием, но есть весьма важная причина, почему я должен о нем рассказать: его не используют.
Особенно часто этим грешат начинающие Delphi-щики, и VB-