Идиомы и стили С++ - Albert Makhmutov
Шрифт:
Интервал:
Закладка:
Итак, мы уже решили иметь к некоему сложному объекту набор стандартных интерфейсов, и реализовать их при помощи smart-указателей. Но сами по себе они не имеют никакой пользы, если мы не сможем получать интерфейсы и объект друг из друга; для этого надо заиметь специальные средства, ибо интерфейсные указатели есть самостоятельные объекты, и вообще могут изменять свой указываемый объект в течение своего существования, и даже изменять тип объекта, если угодно (этого не может даже BASIC).
Для получения интерфейса по объекту проще всего нарисовать конструктор, получающий в качестве аргумента объект:
// Это объект
CComplexObject {};
// Это интерфейс
CInterface {
private:
CComplexObject* co; // Укаэатель на объект
public:
CInterface (CComplexObject _co){}; // Это конструктор
};
Немного подумав, решаем перенести обязанности по порождению интерфейсов на объект. Конструктор интерфейса перекладываем в private, объявляем класс объекта дружественным классу интерфейса, в классе объекта перегружаем операторы преобразования (или русским языком говоря - рисуем операторы преобразования объекта к интерфейсу).
// Это объект
class CComplexObject {
operator CInterface() { return new CInterface(this); } // оператор преобразования
};
// Это интерфейс
CInterface {
private:
CComplexObject* co; // Укаэатель на объект
CInterface (CComplexObject* _co) {} // Это частный конструктор
};
Думаем еще раз: перенести ответственность за преобразование интерфейсов на специально выделенный smart-указатель, и временно назовем его Super-указателем. Идея с супером просто счастливая - мало того, что не надо изменять объект (код класс объекта), так еще и преобразование упрощается: сначала получим супер по интерфейсу, а потом другой интерфейс по суперу. Да, конечно, два преобразования подряд, но это все же лучше чем в каждом интерфейсе определять преобразование ко всем остальным. Зато интерфейсы ничего не знают друг о друге, им нет нужды, если им известен супер. И потом, поскольку интерфейсы являются простыми smart-ами, надо пожалуй задать функциюшечку, которая бы проверяла - есть ли вообще в природе изрядно подзабытый нами объект. Это место небезуспешно может занять перегруженный оператор operator!().
// Предварительные объявления классов
class CComplexObject;
class CInterface;
class CSuperObject;
// Определение объекта пропускаю, это Ваше занятие.
// Определение супера
class CSuperObject {
private:
CComplexObject* co; // указатель на объект
public:
// конструктор супера,
CSuperObject(CComplexObject* _co): co(_co) {}
// Живой ли наш объект? Дима! Помаши рукой маме!
bool operator!(){ return co==NULL; }
// преобразование к интерфейсу
operator CInterface();
}
// Это интерфейс
CInterface {
private:
CComplexObject* co; // Укаэатель на объект
CSuperObject* cs; // указатель на супер
CInterface (CComplexObject* _co) {} // Это частный конструктор
public:
bool operator!(){ return co==NULL; } //проверка на существование объекта
operator CSuperObject (); //преобразование к суперу
};
Ну все, с этой темой я закругляюсь, но думаю, что идея понятна. Комбинации умных, ведущих, интерфейсных указателей, наследование смартов от абстрактных базовых классов, наследование смартов и указываемых объектов от одних и тех же базовых классов позволяют Вам достичь удивительной гибкости. Помните, как Шалтай-Болтай говорил Алисе "с таким именем ты можешь оказаться кем угодно… просто КЕМ УГОДНО!"? Мы лучше. Мы оказываемся кем угодно, когда угодно, и по собственному желанию.
Напоследок прописная истина для тех, кто не знает: общие определения полиморфизма, наследования, инкапсуляции, понимание перегрузки операторов, перегрузки и переопределения функций, абстрактных классов, виртуальных и чистых виртуальных функций, конструкторов и деструкторов КРИТИЧЕСКИ ВАЖНЫ ПРИ ПОИСКЕ РАБОТЫ ЗА БУГРОМ!!! Если Вы запнетесь хоть на одном из этих терминов, то Ваш интервьюер никогда Вам больше не позвонит, а если он еще и работает на крупную фирму, то Вас занесут в базу данных как никчемного ламера, и с ней будут сверяться десятки рекрутеров по всем Юнидос Эстадос. Вот так.
Шаг 11 - Нетривиальное конструирование объектов.
В прошлом шаге мы уже столкнулись с ситуацией, когда явное конструирование объектов нежелательно. Выход в таком случае - убрать конструкторы из открытой части объявления. Возможны два варианта - если Вы планируете наследование от этого класса, то конструкторы перемещается в защищенную часть:
CClass {
protected:
CClass () {}
};
А если Вы еще хотите так же и запретить наследование, то конструкторы перемещаются в закрытую часть объявления:
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. Они конечно сложнее и делают много чего еще, но в конечном итоге это замазанные макросом производящие функции.