Страницы

Поиск по вопросам

воскресенье, 29 декабря 2019 г.

Должны ли все функции в абстрактном классе быть виртуальными?

#cpp #ооп


Собственно основной вопрос в шапке, но есть еще пара порождённых им же(достаточно
глупых, но не я никак не могу в них въехать). Кому не лень, расставьте для меня все
точки над i ;)  


Почему можно объявить виртуальный деструктор, который сможет разрушить любого потомка,
а виртуальный конструктор нет?(читал про
   технику конверт-письмо, но как-то не доосмыслил). Гипотетически понимаю, что все
наследуемые объекты будут разные, но , разве, только из-за этого?
Зачем нам вообще нужен виртуальный деструктор, если хорошим тоном считают написание
самостоятельного деструктора для всех
   классов(следовательно и для потомков, как я понимаю)?
Почему можно объявить указатель на абстрактный класс, а создать экземпляр абстрактного
класса нельзя?(понимаю,что виртуальный класс является ,
   по сути, формой(интерфейсом) всех классов порождённых им => смысла
   объявлять простой абстрактный класс нет, но не понятна ситуация с
   указателем).
Есть ли смысл вообще писать конструктор в абстрактном классе? ( по личному мнению
- нет, но в примерах абстрактных классов
   конструкторы встречал ).
Какой смысл объявлять виртуальную функцию одного класса дружественной в другом?

    


Ответы

Ответ 1



Нет совершенно никаких причин для того, чтобы в абстрактном классе все функции были виртуальными. Например, существует классический паттерн, когда при помощи абстрактного класса реализуют некий "скелет" общего алгоритма, а при помощи виртуальных функций заполняют уже частные детали конкретного применения этого алгоритма. Детали алгоритма реализуются виртуальными функциями, а сам алгоритм - невиртуальной функцией. Элементарным примером такого паттерна может быть некий класс "Программа": class Program { protected: virtual void init() {} virtual void run() = 0; virtual void done() {} public: void execute() { init(); run(); done(); } }; В данном примере собственно основной "алгоритм" реализуется функцией execute(), которой нет никаких причин быть виртуальной. Все разнообразие поведений разных "программ" реализуется классами-наследниками через перекрытие функций init(), run() и done(), а сам "алгоритм" остается неперекрываемым. Поведение виртуальных функций в языке С++ фундаментально основано на понятии динамического типа объекта. Виртуальный вызов функции - это по определению вызов, при котором конкретная версия функции выбирается на основе анализа динамического типа объекта, использованного в этом вызове. По этой причине виртуальный вызов возможен только тогда, когда динамический тип объекта уже определен (проинициализирован, сформирован, зафиксирован - выберете тот термин, который вам нравится). Так вот конструктор - это именно та специальная функция, которая формирует, инициализирует тип объекта. До того момента, как конструктор отработал, объекта еще не существует и тип объекта еще не определен. Т.е. понятие виртуального вызова к такому "сырому" объекту еще не применимо. По этой причине в С++ не может быть виртуальных конструкторов. Другими словами, в С++ конструктор - эта та самая функция, которая инициализирует механизм виртуальных вызовов. Поэтому сам конструктор вызвать виртуально невозможно - на этот момент функциональность виртуальных вызовов еще не проинициализирована. Физически, в повсеместно используемой реализации полиморфизма, функциональность виртуальных функций обеспечивается так называемой таблицей виртуальных функций, указатель на которую хранится в каждом полиморфном объекте. Так вот конструктор, кроме прочего, как раз таки и занимается привязкой каждого полиморфного объекта к его правильной таблице виртуальных функций. До того, как такая привязка выполнена, виртуальность работать не может. Хм... Совершенно верно у каждого класса - свой деструктор. Но виртуальность нужна именно для того, чтобы при полиморфном удалении объекта был правильно выбран именно правильный деструктор, в соответствии с динамическим типом удаляемого объекта. Т.е. если у вас есть указатель на базовый класс Base *p, который на самом деле указывает на объект наследного типа Derived Base *p = new Derived; то при выполнении delete p (полиморфное удаление) вам нужно, чтобы был вызван именно деструктор класса Derived. Чтобы это произошло, деструктор должен быть виртуальным. Если деструктор в такой ситуации невиртуален, то, как правильно заметил @VladD в комментариях, поведение программы не определено. Создать самостоятельный экземпляр абстрактного класса нельзя потому, что спецификация языка это запрещает. Да и какой смысл в этом создании? Абстрактный класс - это класс с "несуществующими" (ненаписанными) виртуальными функциями. Зачем и кому могут понадобиться объекты такого неполноценного класса? Что вы предлагаете делать, если пользователь попробует вызвать такую несуществующую виртуальную функцию? Вызывать неопределенное поведение? Авторы языка решили, что разумнее просто запретить создание самостоятельных экземпляров абстрактных классов. В то же время можно сказать, что экземпляры абстрактных классов все таки можно создать, но только как базовые подобъекты неабстрактных классов-наследников, а не как самостоятельные объекты. Собственно, в этом и заключается назначение абстрактных классов - служить в качестве базовых классов для классов-наследников. Тут еще можно добавить, что таки есть лазейка, позволяющая попытаться выполнить вызов несуществующей виртуальной функции абстрактного класса: это вызов виртуальной функции из конструктора (или деструктора) абстрактного класса. Чтобы "обмануть" компилятор такой вызов обычно приходится делать через промежуточную функцию struct S { virtual void foo() = 0; void bar() { foo(); } S() { bar(); } }; struct D : S { virtual void foo() {} }; int main() { D d; } Вышеприведенный код приводит к неопределнному поведению (практически - к падению программы) именно из-за того, что во время работы конструктора класса S делается попытка вызова несуществующего метода S::foo(). Можно условно сказать, что в течение того "короткого мига", когда работает конструктор (или деструктор) абстрактного класса, соответствующий объект является самостоятельным абстрактным объектом со всеми вытекающими последствиями, как, например, падение программы при попытке вызова несуществующей виртуальной функции. Не понимаю, почему возникает такой вопрос. Смысл писать конструктор в абстрактном классе, разумеется, есть, если для такого конструктора в этом классе есть работа. Конструктор должен что-то инициализировать. У вас есть что инициализировать в вашем абстрактном классе? Если есть - то пишите конструктор. Другое дело, что обычно в абстрактных класса инициализировать нечего. Соответственно и конструктор чаще всего не нужен. Не понимаю сути вопроса. Дружественность не имеет никакого отношения к виртуальности. Поэтому объявлять виртуальную функцию другом имеет ровно тот же смысл, что и объявлять любую другую (невиртуальную) функцию другом. Тут стоит еще раз повторить, что дружественность ничего не знает о виртуальности (а виртуальность - о дружественности). Дружественность не распространяется по иерархии классов, т.е. объявленная вами дружественность никак не будет распространяться на "ту же самую" виртуальную функцию в классах-наследниках.

Ответ 2



Типы в C++ не являются объектами первого класса. Вы не можете записать тип в переменную, и по этой переменной вызвать конструктор. Соответственно, виртуальные конструкторы в принципе невозможны в C++. Представьте себе, как вы смогли бы вызвать конструктор «виртуальным» образом? При вызове new вам всегда нужно указывать точный тип. Конструктор по сути является статической функцией. Однако, есть идиома, называемая иногда виртуальным конструктором. Это по сути виртуальный фабричный метод в той же самой иерархии классов (или параллельной ей). Например, вы можете объявить виртуальную (а значит, нестатическую) функцию Base* createNew(), и реализовать её во всех потомках (компилятор не проверит наличие перегрузки за вас, так что вам придётся быть аккуратным). Здесь вместо переменной-класса у вас экземпляр объекта этого же класса. Виртуальный деструктор нужен, как и любая виртуальная функция, для того, чтобы при вызове по указателю на базовый класс была выполнена функция производного класса. Если у вас есть указатель на Base (обычно такое бывает, если у вас коллекция указателей на порождённые классы), и вы удаляете объект по этому указателю, то для случая невиртуального деструктора вызовется деструктор лишь базового класса. Если вам деструктор производного класса делает что-то нетривиальное, это в данном случае будет «пропущено». Это была лишь мотивация. Хуже всего то, что такая ситуация (удаление объекта по указателю на базовый класс, у которого нет виртуального деструктора) вообще объявлено undefined behaviour в стандарте. Брать здесь и внимательно читать §5.3.5/3: Если статический тип объекта, который удаляется, не совпадает с динамическим типом, статический тип обязан быть базовым классом динамического типа удаляемого объекта, и у статического класса обязан быть виртуальный деструктор. В противном случае поведение программы неопределено. (перевод мой) Указатель на абстрактный класс, как и на любой класс, можно объявить, чтобы указывать на объекты данного типа, а также производных от него. В случае абстрактного класса это только указатели на объекты производных типов, разумеется. Создать экземпляр абстрактного класса нельзя потому, что он не готов к использованию. Если в классе есть абстрактный метод, его нельзя вызвать, а в реальном классе каждый метод должно быть можно вызвать. Противоречия между этими двумя фактами я не вижу. Более того, если у вас есть несколько неабстрактных классов, производных непосредственно от абстрактного, у вас не другого метода определить указатель на любой из них. Да, смысл есть. Абстрактный класс по сути является лишь «заготовкой» класса, но даже у этой заготовки могут быть свои инварианты, которые и нужно устанавливать в конструкторе. Другое дело, если вы используете абстрактный класс как интерфейс: просто определяете, какие методы будут доступны. В этом случае конструктор не нужен и даже является излишним. C++, к сожалению, не различает эти два случая. Больше о разнице между интерфейсом и абстрактным классом здесь. В принципе, такой же, как и обычный смысл объявления дружественной функции: управление доступом. Дружба не наследуется, так что потомки дружественного класса не будут автоматически друзьями. По поводу вопроса, вынесенного в заголовок: здесь опять-таки разница между тем, используете ли вы абстрактный класс как интерфейс, или как полуфабрикат другого класса. Оба случая валидны. В первом у вас не должно быть конструктора, данных, и невиртуальных/неабстрактных функций. Во втором, наоборот, скорее всего у вас будет нетривиальный конструктор, данные, и всего несколько абстрактных методов. (Смотрите ссылку из п. 4.)

Ответ 3



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

Комментариев нет:

Отправить комментарий