Главная страница Библиотека (скачать книги) Скачать софт Введение в программирование Стандарты для C++ Уроки по C# Уроки по Python HTML Веб-дизайн Ассемблер в среде Windows ActiveX Javascript Общее о Линукс Линукс - подробно Линукс - новое Delphi Паскаль для начинающих Турбопаскаль Новости Партнеры Наши предложения Архив новостей |
49. Избегайте вызовов виртуальных функций в конструкторах и деструкторахРезюмеВнутри конструкторов и деструкторов виртуальные функции теряют виртуальность. Хуже того — все прямые или косвенные вызовы нереализованных чисто виртуальных функций из конструктора или деструктора приводят к неопределенному поведению. Если ваш дизайн требует виртуальной передачи в производный класс из конструктора или деструктора базового класса, следует воспользоваться иной методикой, например, постконструкторами. ОбсуждениеВ C++ полный объект конструируется по одному базовому классу за раз. Пусть у нас есть базовый класс В и класс D, производный от B. При создании объекта D, когда выполняется конструктор B, динамическим типом создаваемого объекта является В. В частности, вызов виртуальной функции В::Fun приведет к выполнению функции Fun, определенной в классе В, независимо от того, перекрывает ее класс D или нет. И это хорошо, поскольку вызов функции-члена D в тот момент, когда члены объекта D еще не инициализированы, может привести к хаосу. Только после завершения выполнения конструктора В выполняется тело конструктора D и объект приобретает тип D. В качестве эмпирического правила следует помнить, что в процессе конструирования В нет никакого способа определить, является ли B отдельным объектом или базовой частью некоторого иного производного объекта. Кроме того, следует помнить, что вызов из конструктора чисто виртуальной функции, не имеющей определения, приводит к неопределенному поведению. С другой стороны, в некоторых случаях дизайн требует использования "постконструктора", т.е. виртуальной функции, которая должна быть вызвана после того, как полный объект оказывается сконструирован. Некоторые методики, применяемые для решения этой задачи, описаны в приводимых ниже ссылках. Вот (далеко не исчерпывающий) список возможных решений.
Ни одна из методик постконструирования не является идеальной. Наихудшее, что можно сделать, — это потребовать, чтобы пользователь класса вручную вызывал постконструктор. Однако даже наилучшие способы решения данной задачи требуют отличного от обычного синтаксиса конструирования объекта (что легко проверяется в процессе компиляции) и/или сотрудничества с авторами производных классов (что невозможно проверить в процессе компиляции). ПримерыПример. Использование функции-фабрики для принудительного вызова постконструктора. Рассмотрим следующий код: class B { // корень иерархии protected: В() { /*...*/ } virtual void PostInitialize() // вызывается сразу { /*...*/ } // после конструирования publiс: template<class T> static shared_ptr<T> Create() // интерфейс для { // создания объектов shared_ptr<T> p( new T ); p->Postinitialize(); return p; } }; class D : public B { /* ... */ }; // некоторый производный // класс shared_ptr<D> p = D::Create<D>(); // создание объекта D Этот не вполне надежный дизайн основан на ряде компромиссов.
50. Делайте деструкторы базовых классов открытыми и виртуальными либо защищенными и невиртуальнымиРезюмеУдалять или не удалять — вот в чем вопрос! Если следует обеспечить возможность удаления посредством указателя на базовый класс, то деструктор базового класса должен быть открытым и виртуальным. В противном случае он должен быть защищенным и невиртуальным. ОбсуждениеЭто простое правило иллюстрирует достаточно тонкий вопрос и отражает современное использование наследования и принципов объектно-ориентированного проектирования. Для данного базового класса Base вызывающий код может пытаться удалять производные от него объекты посредством указателей на Base. Если деструктор Base открытый и невиртуальный (свойства по умолчанию), он может быть случайно вызван для указателя, который в действительности указывает на производный объект, и в этом случае мы получим неопределенное поведение. Такое состояние дел привело к тому, что в старых стандартах кодирования требовалось делать деструкторы всех без исключения базовых классов виртуальными. Однако это уже перебор (даже если это и оправдано в общем случае); вместо этого следует использовать правило, согласно которому деструктор базового класса должен быть виртуальным тогда и только тогда, когда он открытый. Написание базового класса представляет собой определение абстракции (см. рекомендации с 35 по 37). Вспомним, что для каждой функции-члена, участвующей в абстракции, вы должны решить:
Как описано в рекомендации 39, для обычных функций-членов следует сделать выбор между возможностью их вызова посредством указателя Base* невиртуально (но, возможно, с виртуальным поведением, если эти функции вызывают виртуальные функции, как, например, в шаблонах проектирования NVI или Template Method), виртуально либо невозможностью такого вызова вообще. Шаблон проектирования NVI представляет собой методику, которая позволяет обойтись без открытых виртуальных функций. Деструкция может рассматриваться как просто одна из операций, хотя и со специальной семантикой, которая делает невиртуальные вызовы опасными или ошибочными. Следовательно, для деструктора базового класса выбор сужается до виртуального вызова через указатель Base* или невозможности вызова вообще; выбор невиртуального вызова в данном случае неприменим. Следовательно, деструктор базового класса виртуален, если он может быть вызван (т.е. открыт), и невиртуален в противном случае. Заметим, что шаблон проектирования NVI не может быть применен к деструктору, поскольку конструкторы и деструкторы не могут осуществлять глубокие виртуальные вызовы (см. рекомендации 39 и 55). Вывод: всегда явно пишите деструктор базового класса, поскольку неявно сгенерированный деструктор является открытым и невиртуальным. ПримерыКлиентский код должен либо быть способен полиморфно удалять объекты посредством указателя на базовый класс, либо не должен этого делать. Каждый выбор определяет свой дизайн.
Классы стратегий часто используются в качестве базовых для удобства, а не для обеспечения полиморфного поведения. Их деструкторы рекомендуется делать защищенными и невиртуальными. ИсключенияНекоторые компонентные архитектуры (например, СОМ или CORBA) не используют стандартный механизм удаления и применяют для этого различные собственные протоколы освобождения объектов. Следуйте шаблонам и идиомам этих архитектур и соответствующим образом адаптируйте данную рекомендацию. Рассмотрим также еще один редкий случай.
Тогда несмотря на то, что деструктор оказывается открытым, имеется огромный соблазн не делать его виртуальным, поскольку такая первая виртуальная функция может привести к излишним расходам времени выполнения из-за добавления функциональности, которая нам совсем не нужна В этом редком случае вы можете сделать конструктор открытым и невиртуальным, но четко документировать, что производные объекты не могут использоваться полиморфно в качестве объектов базового класса. Именно это было сделано в случае класса std::unary_function. В общем случае, однако, следует избегать конкретных базовых классов (см. рекомендацию 35). Например, unary_function представляет собой набор определений typedef, который не предназначен для создания объектов данного типа. В действительности бессмысленно давать ему открытый деструктор; лучше было бы последовать совету данной рекомендации и сделать его деструктор защищенным и невиртуальным. |
|
Библиотека программиста. 2009. |
|