Чтение онлайн

ЖАНРЫ

Идиомы и стили С++

Makhmutov Albert

Шрифт:

CClass {

private:

 CClass {}

};

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

от экземпляров), иначе в ней нет смысла.

// Вариант 1: производящая функция-член.

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– шники: слишком велик соблазн щелкнуть по методу формы OnCreate, OnShow (Form_Create, Form_Show), и прописывать инициализации там, или, что еще ужаснее, залезть из одной формы в другую и там изменять значения переменных. Не делайте этого! Граждане дельфинщики! Форма - такой же класс, как и все остальные. Не лишайте ее законного конструктора, дайте ей заслуженную инициализацию! Не чмарите свой инструмент, и он воздаст Вам сторицей!

Ну ладно, чувствую я тут возбужденный такой забуду код нарисовать. Сначала пояснения. Пусть Ваш класс управляет Темными Силами, и они лежат в закрытой части объявления. Не ввязывайтесь в борьбу с ними в конструкторе, там они слишком злобно гнетут. Конструктор пусть инициализирует только примитивные типы, он к тому же их оптимизирует ловчее, но фактически его дело - только выделить память и получить указатель на объект. Инициализация Темных Сил - дело специальной функции, которая грамотно выполняет все нужные действия.

Поделиться с друзьями: