Как мне настроить класс, представляющий интерфейс? Это просто абстрактный базовый класс?





Создайте класс с чистыми виртуальными методами. Используйте интерфейс, создав другой класс, который переопределяет эти виртуальные методы.
Чистый виртуальный метод - это метод класса, который определяется как виртуальный и имеет значение 0.
class IDemo
{
public:
virtual ~IDemo() {}
virtual void OverrideMe() = 0;
};
class Child : public IDemo
{
public:
virtual void OverrideMe()
{
//do stuff
}
};
Почему метод OverrideMe в классе Child является виртуальным? Это необходимо?
@Cemre - нет, в этом нет необходимости, но и не больно.
Обычно рекомендуется оставлять ключевое слово «виртуальный» при переопределении виртуального метода. Хотя это и не обязательно, это может сделать код более понятным - в противном случае у вас нет указаний на то, что этот метод может использоваться полиморфно или даже существует в базовом классе.
@Kevin За исключением override в C++ 11
Не забывайте о точках с запятой в конце объявлений классов.
Необязательно объявлять Child::OverrideMe() как virtual, @Cemre, потому что IDemo::OverrideMe() неявно делает Child::OverrideMe()virtual. Наличие указания на то, что это виртуальная функция (например, virtual или, что еще лучше, override в C++ 11 или новее) послужит напоминанием для программистов, при этом override, в частности, также поможет компилятору проверить, действительно ли он что-то переопределяет.
Вся причина, по которой у вас есть специальная категория типа интерфейса в дополнение к абстрактным базовым классам в C# / Ява, заключается в том, что C# / Java не поддерживает множественное наследование.
C++ поддерживает множественное наследование, поэтому особый тип не требуется. Абстрактный базовый класс без неабстрактных (чисто виртуальных) методов функционально эквивалентен интерфейсу C# / Java.
По-прежнему было бы неплохо иметь возможность создавать интерфейсы, чтобы избавить нас от большого количества наборов (виртуальный, = 0, виртуальный деструктор). Также множественное наследование кажется мне действительно плохой идеей, и я никогда не видел, чтобы оно использовалось на практике, но интерфейсы нужны постоянно. К сожалению, сообщество C++ не будет вводить интерфейсы только потому, что они мне нужны.
Ha11owed: У него есть интерфейсы. Они называются классами с чисто виртуальными методами и без реализации методов.
@ Ha11owed В Java есть некоторые особенности, вызванные отсутствием множественного наследования. Взгляните на класс java.lang.Thread и интерфейс java.lang.Runnable, который существует только потому, что вы не можете расширить Thread вместе с другой базой. Из документации может показаться, что это предусмотрено на ваш выбор, но однажды я был вынужден использовать Runnable из-за отсутствия множественного наследования.
@MilesRout: Я знаю, но все же зачем заставлять меня писать так много шаблонного кода? Также было бы ясно, что вы смотрите на интерфейс, не просматривая все методы и проверяя, что все они являются чисто виртуальными.
@doc: java.lang.Thread имеет методы и константы, которые вы, вероятно, не хотите иметь в своем объекте. Что должен делать компилятор, если вы расширяетесь из Thread и другого класса с помощью общедоступного метода checkAccess ()? Вы действительно предпочли бы использовать строго именованные базовые указатели, как в C++? Это похоже на плохой дизайн, вам обычно нужна композиция там, где вы думаете, что вам нужно множественное наследование.
@ Ha11owed это было давно, поэтому я не помню подробностей, но у него были методы и константы, которые я хотел иметь в своем классе, и, что более важно, я хотел, чтобы объект моего производного класса был экземпляром Thread. Множественное наследование может быть плохим дизайном, а также плохой композицией. Все зависит от случая.
@ Ha11owed +1 за «вам обычно нужна композиция там, где вы думаете, что вам нужно множественное наследование»
В C++ нет ключевого слова abstract.
«Вся причина, по которой у вас есть особая категория типов интерфейса в дополнение к абстрактным базовым классам в C# / Java, заключается в том, что C# / Java не поддерживает множественное наследование». - Утверждение, что причина в этом, совершенно ложное. Интерфейсы существуют в Java, чтобы не объединять концепции согласования с контрактом и совместной реализацией. Это распространенное заблуждение среди разработчиков C++, пытающихся принизить то, что, возможно, является дизайном, более совместимым с чистыми объектно-ориентированными языками.
Objective-C также поддерживает это через протоколы и идет еще дальше (сначала сбивает с толку), делая реализацию метода протокола необязательной. Это предоставило Objective-C многое из того, что скоро получит C++ через Concepts.
@ Дэйв: Правда? Objective-C имеет оценку времени компиляции и шаблоны?
@Deduplicator - я имею в виду ключевое свойство концепций, заключающееся в том, что они позволяют вызываемому объекту (в вашем случае шаблону) указывать ограничения на API, которые аргумент должен / должен поддерживать там, где эти ограничения выражены в объявлении или определении вызываемого, а не внутри иерархии классов аргумента. Нет, в Objective-C нет шаблонов. Шаблоны в этом обсуждении - это вещи, улучшенные «концепцией», в некоторых отношениях схожей с протоколами Objective-C. Но я уверен, что вы это уже знали.
затем приведите пример того, что вы называете множественным наследованием, потому что до сих пор кажется, что java может это сделать, но не C++, java может внедрять много интерфейсов
C++ может напрямую наследовать от многих классов. java может напрямую наследовать только от одного класса.
Чтобы расширить ответ на Bradtgmurray, вы можете сделать одно исключение из списка чистых виртуальных методов вашего интерфейса, добавив виртуальный деструктор. Это позволяет передать право владения указателем другой стороне, не раскрывая конкретный производный класс. Деструктору не нужно ничего делать, потому что интерфейс не имеет конкретных членов. Может показаться противоречивым определение функции как виртуальной и как встроенной, но поверьте мне - это не так.
class IDemo
{
public:
virtual ~IDemo() {}
virtual void OverrideMe() = 0;
};
class Parent
{
public:
virtual ~Parent();
};
class Child : public Parent, public IDemo
{
public:
virtual void OverrideMe()
{
//do stuff
}
};
Вам не нужно включать тело виртуального деструктора - оказывается, у некоторых компиляторов есть проблемы с оптимизацией пустого деструктора, и вам лучше использовать значение по умолчанию.
Виртуальный десктуктор ++! Это очень важно. Вы также можете включить чистые виртуальные объявления оператора = и определения конструктора копирования, чтобы компилятор не генерировал их автоматически.
Альтернативой виртуальному деструктору является защищенный деструктор. Это отключает полиморфную деструкцию, которая может быть более уместной в некоторых обстоятельствах. Ищите «Рекомендацию № 4» в gotw.ca/publications/mill18.htm.
Если вы знаете, что не собираетесь удалять класс через базовый класс, вам не нужен виртуальный деструктор. Однако в любом случае делать деструктор виртуальным никогда не вредно (за исключением одного поиска в vtable, о нет!).
Еще один вариант - определить чистый виртуальный (=0) деструктор с телом. Преимущество здесь в том, что компилятор теоретически может увидеть, что vtable не имеет допустимых членов, и полностью его отбросить. С помощью виртуального деструктора с телом указанный деструктор может быть вызван (виртуально), например в середине построения через указатель this (когда сконструированный объект все еще имеет тип Parent), и поэтому компилятор должен предоставить действительную vtable. Так что, если вы явно не вызываете виртуальные деструкторы через this во время построения :), вы можете сэкономить на размере кода.
Сорок лун назад, но в любом случае - @Mark Ransom, вы написали: «Это позволяет вам передать право владения указателем другой стороне, не раскрывая базовый класс». Вы имели в виду "подкласс"? Потому что тогда в своем комментарии вы написали: «Если вы знаете, что не собираетесь удалять класс через базовый класс, вам не нужен виртуальный деструктор». - Извините, если мое замечание не имеет смысла - я новичок в C++ и пытаюсь извлечь уроки из этих вопросов и ответов.
@ Люми, теперь, когда я думаю об этом, ты абсолютно прав. Интерфейс является базовым классом, потому что конкретный класс является производным от него и реализует методы. Я должен это исправить. Дайте себе золотую звезду за то, что вы заметили первым, я уверен, что этот ответ просматривали тысячу раз.
@MarkRansom, спасибо за виртуальную золотую звезду. :) Между тем, я понял, что вы, должно быть, имели в виду то, что, как я предполагал, имели в виду, потому что я добрался до конца страницы, где Карлос ответ поясняет суть с помощью образца кода. Спасибо за отзыв после такого долгого времени! - Бест, Майкл
@PavelMinaev "С помощью виртуального деструктора с телом указанный деструктор может быть вызван (виртуально), например в середине строительства" серьезно?
@curiousguy, да, вы можете вызвать деструктор в середине построения, когда вы throw из конструктора производного класса.
Рассмотрим иерархию наследования: Base <- Derived <- MostDerived. Вы находитесь в производном ctor. Вы вызываете глобальную функцию, которая принимает аргумент Base*, передавая ему this. Эта функция может явно вызывать деструктор через указатель, и он должен быть отправлен виртуально.
@PavelMinaev, разве не было бы UB явно уничтожать объект до того, как он завершит построение? Я вижу, что конструктор MostDerived очень недоволен. По крайней мере, с уничтожением через throw остальная часть конструкции обходится.
К тому времени, как он дойдет до MostDerived, это определенно будет UB, но я не вижу ничего, что могло бы сделать, например, this->~Foo() в ctor UB сам по себе. Рассмотрим случай, когда вы разрушаете его, а затем никогда не возвращаетесь к ctor (например, просто сидите в бесконечном цикле, принимая и обрабатывая ввод). Насколько я могу судить, внутри этого цикла ваше поведение будет четко определено.
Насколько типично для ответа C++, что верхний ответ не дает прямого ответа на вопрос (хотя, очевидно, код идеален), вместо этого он оптимизирует простой ответ.
Не забывайте, что в C++ 11 вы можете указать ключевое слово override, чтобы разрешить проверку типа аргумента и возвращаемого значения во время компиляции. Например, в объявлении Child virtual void OverrideMe() override;
@Sean, этот ответ на несколько лет предшествует C++ 11, но вы хорошо замечаете - спасибо!
Я согласен с @PavelMinaev, у деструктора практически нет причин быть виртуальным не в чистом виде.
Также обратите внимание, что GCC не может обрабатывать чисто виртуальные деструкторы, и вам необходимо их определить: stackoverflow.com/a/3065223/447490
@Chris Я немного смущен замечанием Марка Рэнсомса об оптимизации компилятора и вашим комментарием. Похоже, что первый предлагает использовать чистые виртуальные деструкторы, а второй запрещает. Я что-то неправильно понял? Если нет, есть ли общее решение для этого или это зависит от компилятора?
@Griddo Нет, вы правильно поняли. Марк Рэнсом предлагает не определять виртуальный деструктор, пока GCC требует этого. Я действительно не думаю, что есть общее решение для этого, кроме загромождения вашего кода с помощью #ifdef и проверок компилятора, но в этом случае я бы предпочел просто определить его и, возможно, потерять некоторые преимущества оптимизации для некоторых компиляторов.
@xan Не могли бы вы объяснить причины, по которым вы хотите сделать оператор присваивания и конструктор копирования чисто виртуальными в классе IDemo.
@ZacharyKraus интерфейс должен быть очень маленьким подмножеством класса, который фактически реализует интерфейс; в частности, у него не будет элементов данных. Это почти гарантирует, что сгенерированные компилятором операторы копирования и присваивания будут делать неправильные вещи. Если вы хотите реализовать их самостоятельно, то выбейте себя, но обычно в этом нет необходимости - вы получаете доступ к интерфейсу через указатель, и копии указателя в порядке, пока вы управляете владением.
@ZacharyKraus: Что сказал MarkRansom. Если вы объявляете интерфейс таким образом, маловероятно, что автоматически сгенерированные компилятором попытки для них будут правильными. Таким образом, безопаснее явно объявить их чистыми виртуальными самостоятельно и убедиться, что вы не можете случайно попасть в ловушку их использования.
@MarkRansom Теперь я понимаю рассуждения, но у меня все еще есть 2 вопроса, которые я просто не понимаю. Во-первых, компилятор автоматически создает конструктор присваивания и копирования для любого класса, даже если вы никогда не используете их в своем коде. Во-вторых, можете ли вы случайно запустить конструктор присваивания или копирования, когда объект является указателем?
@ZacharyKraus их присутствие или отсутствие невозможно обнаружить, если вы не позвоните им, поэтому по правилу «как если бы» все могло идти в любом направлении - я подозреваю, что большинство компиляторов этого не делают. Вы можете сгенерировать вызов, например, *a = *b, но я допускаю, что это нечасто.
Кроме того, это просто не имеет значения, поскольку проверка / копирование интерфейса ничего не делать.
Внутри Child нужно ли записывать OverrideMe() как virtual void?
@ Sparker0i virtual не нужен, поскольку объявление базового класса уже сделало его виртуальным, но это хорошая практика - быть последовательным. void еще нужен.
Было бы неплохо, если бы вы упомянули, что деструктор можно сделать чисто виртуальным, если нет другой функции.
Что делать с предупреждением о том, что IDemo «не имеет внешних определений виртуальных методов»?
Чтобы добавить к тому, что сказал @xan, начиная с C++ 11, я полагаю, вы также можете включить чистое виртуальное объявление конструктора перемещения (и оператор присваивания перемещения, если не используете идиому копирования и обмена).
@gblomqvist, может быть, даже лучше полностью удалить эти методы. Трудно представить себе случай, когда вы использовали бы указатель интерфейса для перемещения объекта.
@MarkRansom Это хороший момент.
В C++ нет понятия «интерфейс» как такового. AFAIK, интерфейсы были впервые представлены в Java, чтобы обойти отсутствие множественного наследования. Эта концепция оказалась весьма полезной, и того же эффекта можно достичь в C++ с помощью абстрактного базового класса.
Абстрактный базовый класс - это класс, в котором по крайней мере одна функция-член (метод на языке Java) является чистой виртуальной функцией, объявленной с использованием следующего синтаксиса:
class A
{
virtual void foo() = 0;
};
Невозможно создать абстрактный базовый класс, т.е. е. вы не можете объявить объект класса A. Вы можете только унаследовать классы от A, но любой производный класс, не обеспечивающий реализацию foo(), также будет абстрактным. Чтобы перестать быть абстрактным, производный класс должен предоставлять реализации для всех чистых виртуальных функций, которые он наследует.
Обратите внимание, что абстрактный базовый класс может быть больше, чем интерфейс, потому что он может содержать элементы данных и функции-члены, которые не являются чисто виртуальными. Эквивалентом интерфейса был бы абстрактный базовый класс без каких-либо данных только с чистыми виртуальными функциями.
И, как указал Марк Рэнсом, абстрактный базовый класс должен предоставлять виртуальный деструктор, как и любой базовый класс, если на то пошло.
Я бы сказал больше, чем «отсутствие множественного наследования», чтобы заменить множественное наследование. Java была разработана таким образом с самого начала, потому что множественное наследование создает больше проблем, чем то, что оно решает. Хороший ответ
Оскар, это зависит от того, являетесь ли вы программистом на C++, изучившим Java, или наоборот. :) ИМХО, при разумном использовании, как и почти все в C++, множественное наследование решает проблемы. Абстрактный базовый класс «интерфейс» является примером очень разумного использования множественного наследования.
@OscarRyz Неправильно. MI создает проблемы только при неправильном использовании. Большинство предполагаемых проблем с MI также возникнет с альтернативными конструкциями (без MI). Когда у людей возникают проблемы с дизайном с помощью MI, это вина MI; если у них есть проблемы с дизайном SI, это их собственная вина. «Алмаз смерти» (повторное наследование) является ярким примером. Нападение на МИ - это не чистое лицемерие, но близко.
Семантически интерфейсы отличаются от абстрактных классов, поэтому интерфейсы Java - это не просто технический обходной путь. Выбор между определением интерфейса или абстрактного класса определяется семантикой, а не техническими соображениями. Давайте представим некоторый интерфейс «HasEngine»: это аспект, особенность, и он может быть применен / реализован очень разными типами (будь то классы или абстрактные классы), поэтому мы определим интерфейс для этого, а не абстрактный класс.
@MarekStanley, возможно, вы правы, но я бы хотел, чтобы вы выбрали лучший пример. Мне нравится думать об этом с точки зрения наследования интерфейса вместо наследования реализации. В C++ вы можете либо наследовать интерфейс и реализацию вместе (публичное наследование), либо наследовать только реализацию (частное наследование). В Java у вас есть возможность наследовать только интерфейс без реализации.
@curiousguy Это очень хорошо подводит итог. Вообще говоря, правильно использовать множественное наследование труднее, чем правильно использовать одиночное, но простая сложность не означает, что это невозможно. Примером может быть что-то вроде этого: class Button; class Image; class ClickableIcon : public Button, public Image {};. Если они спроектированы правильно, Image предоставит графические функции, а Button предоставит интерактивные функции без перекрытия, которое может вызвать проблемы, в то время как ClickableIcon сам обрабатывает функции, для которых предназначена кнопка.
... Я мог бы лучше сформулировать этот пример много. Button предоставляет интерактивный код, Image предоставляет графический код, а ClickableIcon предоставляет код, специфичный для него самого, а также любой код, необходимый для объединения двух родительских классов. Пока два класса спроектированы правильно и не имеют пересекающихся имен членов, класс должен быть полностью жизнеспособным и более простым в реализации, чем если бы один или оба были интерфейсом.
Небольшое дополнение к тому, что там написано:
Во-первых, убедитесь, что ваш деструктор также является чисто виртуальным.
Во-вторых, вы можете захотеть наследовать виртуально (а не обычно) при реализации, просто для хороших мер.
Мне нравится виртуальное наследование, потому что концептуально это означает, что существует только один экземпляр унаследованного класса. По общему признанию, класс здесь не требует места, поэтому он может быть лишним. Я давно не занимался MI на C++, но разве невиртуальное наследование не усложнит апкастинг?
Почему, ну почему кто-то захочет сделать dtor в этом случае чисто виртуальным? Какая от этого польза? Вы просто навязываете производным классам что-то, что им, вероятно, не нужно включать - dtor.
Если есть ситуация, что объект будет уничтожен через указатель на интерфейс, вы должны убедиться, что деструктор виртуальный ...
В чистом виртуальном деструкторе нет ничего плохого. В этом нет необходимости, но в этом нет ничего плохого. Реализация деструктора в производном классе вряд ли станет большим бременем для разработчика этого класса. Смотрите мой ответ ниже, чтобы узнать, почему вы это сделали.
+1 для виртуального наследования, потому что с интерфейсами более вероятно, что класс будет наследовать интерфейс из двух или более путей. Я выбираю защищенные деструкторы в интерфейсах.
@JohannGerell Я считаю, что цель чистых виртуальных деструкторов состоит в том, чтобы предотвратить создание vtable, если только класс, который наследуется от вашего класса интерфейса, сам не унаследован от. Однако, если это намерение, деструктор должен быть объявлен как чистый виртуальный, так и определен следующим образом: class Base { public: ~Base() = 0; }; Base::~Base() {}, чтобы компилятор мог создавать конструкторы по умолчанию для производных классов.
Вы также можете рассмотреть классы контрактов, реализованные с помощью NVI (Non Virtual Interface Pattern). Например:
struct Contract1 : boost::noncopyable
{
virtual ~Contract1() = default;
void f(Parameters p) {
assert(checkFPreconditions(p)&&"Contract1::f, pre-condition failure");
// + class invariants.
do_f(p);
// Check post-conditions + class invariants.
}
private:
virtual void do_f(Parameters p) = 0;
};
...
class Concrete : public Contract1, public Contract2
{
private:
void do_f(Parameters p) override; // From contract 1.
void do_g(Parameters p) override; // From contract 2.
};
Для других читателей этот Статья доктора Доббса «Беседы: виртуально ваши» Джима Хислопа и Херба Саттера подробно разъясняет, почему можно захотеть использовать NVI.
А также эта статья «Виртуальность» Херба Саттера.
Мой ответ в основном такой же, как и другие, но я думаю, что нужно сделать еще две важные вещи:
Объявите виртуальный деструктор в своем интерфейсе или сделайте защищенный не виртуальный, чтобы избежать неопределенного поведения, если кто-то попытается удалить объект типа IDemo.
Используйте виртуальное наследование, чтобы избежать проблем с множественным наследованием. (Когда мы используем интерфейсы, чаще возникает множественное наследование.)
И как другие ответы:
Используйте интерфейс, создав другой класс, который переопределяет эти виртуальные методы.
class IDemo
{
public:
virtual void OverrideMe() = 0;
virtual ~IDemo() {}
}
Или же
class IDemo
{
public:
virtual void OverrideMe() = 0;
protected:
~IDemo() {}
}
И
class Child : virtual public IDemo
{
public:
virtual void OverrideMe()
{
//do stuff
}
}
нет необходимости в виртуальном наследовании, поскольку у вас нет элементов данных в интерфейсе.
Виртуальное наследование важно и для методов. Без него вы столкнетесь с двусмысленностью с OverrideMe (), даже если один из его «экземпляров» является чисто виртуальным (просто попробовал это сам).
@Avishay_ "нет необходимости в виртуальном наследовании, поскольку у вас нет элементов данных в интерфейсе." Неправильно.
Обратите внимание, что виртуальное наследование может не работать в некоторых версиях gcc, например в версии 4.3.3, которая поставляется с WinAVR 2010: gcc.gnu.org/bugzilla/show_bug.cgi?id=35067
-1 за наличие не виртуального защищенного деструктора, извините
Все хорошие ответы выше. Следует иметь в виду еще одну дополнительную вещь - у вас также может быть чистый виртуальный деструктор. Единственная разница в том, что вам еще нужно его реализовать.
Смущенный?
--- header file ----
class foo {
public:
foo() {;}
virtual ~foo() = 0;
virtual bool overrideMe() {return false;}
};
---- source ----
foo::~foo()
{
}
Основная причина, по которой вы захотите это сделать, заключается в том, что вы хотите предоставить методы интерфейса, как у меня, но сделать их переопределение необязательными.
Чтобы сделать класс интерфейсным классом, требуется чистый виртуальный метод, но все ваши виртуальные методы имеют реализации по умолчанию, поэтому единственный метод, который остается сделать чисто виртуальным, - это деструктор.
Повторная реализация деструктора в производном классе совсем не проблема - я всегда повторно реализую деструктор, виртуальный или нет, в своих производных классах.
Почему, ну почему кто-то захочет сделать dtor в этом случае чисто виртуальным? Какая от этого польза? Вы просто навязываете производным классам что-то, что им, вероятно, не нужно включать - dtor.
Обновил свой ответ, чтобы ответить на ваш вопрос. Чистый виртуальный деструктор - допустимый способ создать (единственный способ достичь?) Интерфейсный класс, в котором все методы имеют реализации по умолчанию.
Если вы используете компилятор C++ от Microsoft, вы можете сделать следующее:
struct __declspec(novtable) IFoo
{
virtual void Bar() = 0;
};
class Child : public IFoo
{
public:
virtual void Bar() override { /* Do Something */ }
}
Мне нравится этот подход, потому что он приводит к намного меньшему размеру кода интерфейса, а размер сгенерированного кода может быть значительно меньше. Использование novtable удаляет все ссылки на указатель vtable в этом классе, поэтому вы никогда не сможете создать его экземпляр напрямую. Смотрите документацию здесь - novtable.
Я не совсем понимаю, почему вы использовали novtable вместо стандартного virtual void Bar() = 0;
Это в дополнение к (я только что заметил отсутствующий = 0;, который я добавил). Прочтите документацию, если вы не понимаете ее.
Я прочитал его без = 0; и предположил, что это просто нестандартный способ сделать то же самое.
Насколько я мог проверить, очень важно добавить виртуальный деструктор. Я использую объекты, созданные с помощью new и уничтоженные с помощью delete.
Если вы не добавляете виртуальный деструктор в интерфейс, то деструктор унаследованного класса не вызывается.
class IBase {
public:
virtual ~IBase() {}; // destructor, use it to call destructor of the inherit classes
virtual void Describe() = 0; // pure virtual method
};
class Tester : public IBase {
public:
Tester(std::string name);
virtual ~Tester();
virtual void Describe();
private:
std::string privatename;
};
Tester::Tester(std::string name) {
std::cout << "Tester constructor" << std::endl;
this->privatename = name;
}
Tester::~Tester() {
std::cout << "Tester destructor" << std::endl;
}
void Tester::Describe() {
std::cout << "I'm Tester [" << this->privatename << "]" << std::endl;
}
void descriptor(IBase * obj) {
obj->Describe();
}
int main(int argc, char** argv) {
std::cout << std::endl << "Tester Testing..." << std::endl;
Tester * obj1 = new Tester("Declared with Tester");
descriptor(obj1);
delete obj1;
std::cout << std::endl << "IBase Testing..." << std::endl;
IBase * obj2 = new Tester("Declared with IBase");
descriptor(obj2);
delete obj2;
// this is a bad usage of the object since it is created with "new" but there are no "delete"
std::cout << std::endl << "Tester not defined..." << std::endl;
descriptor(new Tester("Not defined"));
return 0;
}
Если вы запустите предыдущий код без virtual ~IBase() {};, вы увидите, что деструктор Tester::~Tester() никогда не вызывается.
Лучший ответ на этой странице, поскольку в нем приводится практический компилируемый пример. Ваше здоровье!
Testet :: ~ Tester () запускается только в том случае, если объект "Объявлен с помощью тестера".
Фактически, будет вызван деструктор строки privatename, и это все, что будет выделено в памяти. Что касается среды выполнения, когда все конкретные члены класса уничтожаются, то же самое происходит и с экземпляром класса. Я попробовал аналогичный эксперимент с классом Line, который имел две структуры Point, и обнаружил, что обе структуры были разрушены (Ха!) При вызове удаления или возврате из охватывающей функции. valgrind подтвердил 0 утечек.
В C++ 11 можно легко вообще избежать наследования:
struct Interface {
explicit Interface(SomeType& other)
: foo([=](){ return other.my_foo(); }),
bar([=](){ return other.my_bar(); }), /*...*/ {}
explicit Interface(SomeOtherType& other)
: foo([=](){ return other.some_foo(); }),
bar([=](){ return other.some_bar(); }), /*...*/ {}
// you can add more types here...
// or use a generic constructor:
template<class T>
explicit Interface(T& other)
: foo([=](){ return other.foo(); }),
bar([=](){ return other.bar(); }), /*...*/ {}
const std::function<void(std::string)> foo;
const std::function<void(std::string)> bar;
// ...
};
В этом случае интерфейс имеет ссылочную семантику, т.е. вы должны убедиться, что объект переживает интерфейс (также можно создавать интерфейсы с семантикой значений).
У такого типа интерфейсов есть свои плюсы и минусы:
Наконец, наследование - это корень всех зол в сложной разработке программного обеспечения. В Семантика ценностей Шона Родителя и основанный на концепциях полиморфизм (настоятельно рекомендуется, там объясняются лучшие версии этой техники) исследуется следующий случай:
Скажем, у меня есть приложение, в котором я обрабатываю свои формы полиморфно, используя интерфейс MyShape:
struct MyShape { virtual void my_draw() = 0; };
struct Circle : MyShape { void my_draw() { /* ... */ } };
// more shapes: e.g. triangle
В своем приложении вы делаете то же самое с разными формами, используя интерфейс YourShape:
struct YourShape { virtual void your_draw() = 0; };
struct Square : YourShape { void your_draw() { /* ... */ } };
/// some more shapes here...
Теперь предположим, что вы хотите использовать некоторые формы, которые я разработал, в вашем приложении. Концептуально наши фигуры имеют одинаковый интерфейс, но для того, чтобы мои фигуры работали в вашем приложении, вам нужно будет расширить мои фигуры следующим образом:
struct Circle : MyShape, YourShape {
void my_draw() { /*stays the same*/ };
void your_draw() { my_draw(); }
};
Во-первых, изменение моих форм может оказаться невозможным. Более того, множественное наследование ведет к спагетти-коду (представьте, что появляется третий проект, использующий интерфейс TheirShape ... что произойдет, если они также вызовут свою функцию рисования my_draw?).
Обновление: есть несколько новых ссылок о полиморфизме на основе ненаследования:
Наследование TBH гораздо яснее, чем та штука C++ 11, которая претендует на роль интерфейса, а скорее является клеем для связывания некоторых несовместимых проектов. Пример фигур оторван от реальности, а класс Circle - плохой дизайн. В таких случаях следует использовать шаблон Adapter. Извините, если это будет звучать немного жестко, но попробуйте использовать какую-нибудь реальную библиотеку, например Qt, прежде чем делать выводы о наследовании. Наследование значительно облегчает жизнь.
Это совсем не звучит резко. Как пример формы отделен от реальности? Не могли бы вы привести пример (может быть, на идеоне) исправления Circle с помощью паттерна Adapter? Мне интересно увидеть его преимущества.
В ПОРЯДКЕ. Я постараюсь поместиться в эту крохотную коробочку. Прежде всего, вы обычно выбираете библиотеки, такие как «MyShape», прежде чем начать писать собственное приложение, чтобы обезопасить свою работу. Иначе как вы могли знать, что Square еще нет? Предвидение? Вот почему он оторван от реальности. И на самом деле, если вы решите полагаться на библиотеку MyShape, вы можете адаптироваться к ее интерфейсу с самого начала. В примере с фигурами много глупостей (одна из которых состоит в том, что у вас есть две структуры Circle), но адаптер будет выглядеть примерно так -> ideone.com/UogjWk
Как я уже упоминал, Qt. Посмотрите этот пример -> qt-project.org/doc/qt-5/qtwidgets-widgets-scribble-example.h tml, чтобы увидеть, насколько надежно наследование в целом с виртуальными методами. А с запрещенным наследованием и этим «интерфейсом» C++ 11 должен ли я каждый раз реализовывать ВСЕ виртуальные методы, которые есть в QWidget? Нужно ли мне делегировать каждый вызов для проверки, например, положения моего настраиваемого виджета? С другой стороны, Qt должна была стать библиотекой только для заголовков из-за «интерфейсной» вещи, необходимой для вызова моих реализаций.
Тогда это не оторвано от реальности. Когда компания A покупает компанию B и хочет интегрировать кодовую базу компании B в A, у вас есть две полностью независимые кодовые базы. Представьте, что у каждого есть иерархия форм разных типов. Вы не можете легко объединить их с наследованием и добавить компанию C, и у вас будет огромный беспорядок. Думаю, вам стоит посмотреть этот доклад: youtube.com/watch?v=0I0FD3N5cgM Мой ответ более старый, но вы увидите сходство. Вам не нужно все время заново реализовывать все, вы можете предоставить реализацию в интерфейсе и выбрать функцию-член, если она доступна.
Есть также пара выступлений Шона Пэрента о концептуальном полиморфизме, которые могут быть вам интересны.
Упомянутый Qt принадлежал 3 компаниям (Trolltech, Nokia, Digia), но ни одна из них не сделала то, что вы говорите. Он оторван от реальности. Приведите мне пример реального программного обеспечения, в котором происходило такое слияние кода. Даже если это так, вы должны использовать шаблон Adapter. Предоставление реализации в интерфейсе, как в вашем ответе, означает, что я должен предоставить все виртуальные методы, которые в противном случае можно было бы просто унаследовать.
Я просмотрел часть видео, и это совершенно неверно. Я никогда не использую dynamic_cast, кроме как для отладки. Динамическое приведение означает, что с вашим дизайном что-то не так, а дизайны в этом видео неправильны по дизайну :). Гай даже упоминает Qt, но даже здесь он ошибается - QLayout не наследуется ни от QWidget, ни наоборот!
Я обновил ответ со ссылкой на случай, если вам интересно узнать об этом больше. Все три упомянутые вами компании работали с одной и той же библиотекой. Когда дело обстоит не так, наследование становится огромной проблемой. Примеры компаний показаны в разговоре Шона Родителя о концептуальном полиморфизме. По предоставленным ссылкам содержится больше информации, чем я могу здесь предоставить.
Я смотрел видео о концептуальном полиморфизме. Хотя этот метод интересен и может найти некоторые приложения, он не может заменить полиморфизм на основе наследования (и этот полиморфизм на основе концепций по-прежнему использует наследование, скрытое только в частных структурах модели). Полиморфизм предназначен не только для вашего внутреннего устройства, но и для того, чтобы приложение могло взаимодействовать с библиотекой. Photoshop - это не библиотека, поэтому он даже не рассматривался в видео. Я не вижу никаких практических функций в полиморфизме на основе conecpt, которых нельзя было бы достичь с помощью более чистого наследования IMO.
Я согласен с тем, что полиморфизм на основе наследования очень прост в использовании в C++ (эта функция имеет полную языковую поддержку). Например, в C это сделать очень сложно. В Haskell, Rust, Scala ... у вас есть черты и классы типов, которые похожи на языковую поддержку полиморфизма на основе ненаследования. Я использовал его в приложении и остался очень доволен. В основном из-за повышения производительности, которое он мне дал (у меня могут быть векторы каждого типа и вектор интерфейсов, поэтому я использую полиморфизм только там, где он мне действительно нужен). Посмотрим, как это будет развиваться в C++, об этом заговорили совсем недавно ...
Верно. Проблема в том, что я не понимаю, почему наследование является «корнем всех зол». Такое заявление нелепо.
Что ж, утверждение больше похоже на «основанный на наследовании полиморфизм - корень всех зол». Поразмыслив еще немного о решении, которое вы предоставили с помощью шаблона адаптера, я вижу преимущество использования вместо этого интерфейса со стиранием типа (например, одного sean-родителя) в том, что ваш интерфейс находится в одном месте, и вы можете расширить его поведение и предоставить значения по умолчанию с помощью функций, не являющихся членами, не являющимися друзьями. Однако интерфейс намного сложнее реализовать. Механизм расширения ... его ADL против не-ADL. Если вы переходите на ADL, вы в основном «крадете» имена функций из пространств имен пользователей.
И это, ИМО, плохо, потому что вы хотите разделить сложное программное обеспечение на небольшие логические части, и наследование помогает вам в этом. То, что все виды объектов (в данном конкретном случае - модели) выродились в один класс, меня не убеждает. Как мне расширить такой концептуальный интерфейс, если у меня нет доступа к внутренним компонентам object_t и я не могу наследовать после него? Насколько я понимаю, мне нужно реализовать функцию draw() с нуля самостоятельно, и я не привязан к интерфейсу concept_t. Кроме того, с виртуальными методами у меня могут быть реализации по умолчанию, которые я могу переопределить или нет.
Термин «интерфейс» пришел из мира Java. В документе Java говорится: «Реализация интерфейса позволяет классу стать более формально о поведении, которое он обещает обеспечить. Интерфейсы образуют контракт между классом и внешним миром, и этот контракт является принудительный во время сборки компилятором. Если ваш класс утверждает, что реализует интерфейс , все методы, определенные этим интерфейсом, должны появиться в его исходном коде, прежде чем класс будет успешно скомпилирован. " (docs.oracle.com/javase/tutorial/java/concepts/interface.htm l) То, что вы представляете, похоже на перевернутый мир.
Совершенно верно, и я думаю, что это правильный поступок: «сначала напишите свой класс, а потом заставьте его моделировать интерфейс». Он полностью отделяет реализацию вашего класса от моделирования данного интерфейса и позволяет вам использовать свой класс в контексте, о котором вы не могли подумать в момент его написания. Таким образом работают концепции C++, классы типов haskell, черты rust / scala и шаблон адаптера, который вы показали. Те же рассуждения лежат в основе идиомы NVI, но эти другие решения еще больше отделяют дизайн, поскольку класс больше не является интерфейсом (в смысле наследования "есть").
Это путь к беспорядку. Шаблон адаптера существует для привязки несовместимых интерфейсов, если это необходимо, но уже существующих интерфейсов. Я не знаю, зачем вообще использовать классы с подходом «сначала напиши свой класс». Здесь предлагается вернуться к чистому C. На каких предпосылках вы строите свой класс, если вы не знаете, как он будет взаимодействовать с внешним миром? Скажем, вы использовали свою собственную реализацию gnzlbg::vector в своем классе, и я предоставляю интерфейс с моим пользовательским doc::vector, и вам нужно преобразовать ввод и вывод в свои внутренние компоненты. Где повторное использование нашего кода и эффективность, если вам нужно их преобразовать?
Во-первых, обратите внимание, что интерфейс может заключать тип в оболочку либо по значению, либо по уникальной ссылке, либо по общей ссылке. Во-вторых, эта проблема с вектором уже существует, поскольку распределитель находится в типе std :: vector. Решение состоит в том, чтобы либо преобразовать (что включает копирование: дорого), либо вместо этого предоставить интерфейс итератора, используя, например, Boost.Iterator.any_iterator (который использует стирание типов, описанный здесь, и работает медленно). Почему вы говорите, что он просит вернуться к чистому C? Интерфейсы безопасны по типу и могут быть очень тонкими. Я создаю свои классы, зная, что мои знания о моей проблеме (и моих потребностях) будут меняться со временем.
Каким будет решение на основе наследования, если вы не можете изменить мой код? Лучшее, что я могу придумать, это: иметь чистый вектор виртуального базового класса, сделать свой унаследованный от него (всегда платить vtable), сделать вашу векторную функцию виртуальной (всегда платить эту цену) и использовать шаблон адаптера для моего вектора. Альтернативой может быть создание векторного интерфейса, который адаптирует ваш вектор и мой вектор. И используйте интерфейс, когда вы необходимость, чтобы полиморфно получить доступ к вашему вектору и моему вектору, но только в этих местах. И здесь, когда я говорю «интерфейс», я имею в виду свой или тот, который основан на шаблоне адаптера.
Позвольте нам продолжить обсуждение в чате.
Тип упаковки не помешает вам повторно реализовать ту же функциональность и выполнить очень дорогостоящее преобразование между векторными типами. Проблем с вектором не существует, если мы оба используем либо doc::vector, либо gnzlbg::vector в качестве интерфейса. Вы должны научиться правильно использовать наследование, потому что то, что вы сейчас говорите, не имеет никакого смысла!
class Shape
{
public:
// pure virtual function providing interface framework.
virtual int getArea() = 0;
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
class Triangle: public Shape
{
public:
int getArea()
{
return (width * height)/2;
}
};
int main(void)
{
Rectangle Rect;
Triangle Tri;
Rect.setWidth(5);
Rect.setHeight(7);
cout << "Rectangle area: " << Rect.getArea() << endl;
Tri.setWidth(5);
Tri.setHeight(7);
cout << "Triangle area: " << Tri.getArea() << endl;
return 0;
}
Результат: Площадь прямоугольника: 35 Площадь треугольника: 17
Мы видели, как абстрактный класс определяет интерфейс в терминах getArea (), а два других класса реализуют ту же функцию, но с другим алгоритмом для вычисления области, специфичной для формы.
Это не то, что считается интерфейсом! Это просто абстрактный базовый класс с одним методом, который нужно переопределить! Интерфейсы обычно являются объектами, которые содержат только определения методов - «контракт», который должны выполнять другие классы, когда они реализуют интерфейс.
Я все еще новичок в разработке на C++. Я начал с Visual Studio (VS).
Тем не менее, кажется, никто не упомянул __interface в VS (.СЕТЬ). Я нет очень уверен, что это хороший способ объявить интерфейс. Но, похоже, он предоставляет дополнительное исполнение (упомянутый в документы). Таким образом, вам не нужно явно указывать virtual TYPE Method() = 0;, поскольку он будет автоматически преобразован.
__interface IMyInterface {
HRESULT CommitX();
HRESULT get_X(BSTR* pbstrName);
};
However, I don't use it because I am concern about the cross platform compilation compatibility, since it only available under .NET.
Если у кого-то есть что-нибудь интересное, поделитесь, пожалуйста. :-)
Спасибо.
Хотя это правда, что virtual является стандартом де-факто для определения интерфейса, давайте не будем забывать о классическом C-подобном шаблоне, который поставляется с конструктором на C++:
struct IButton
{
void (*click)(); // might be std::function(void()) if you prefer
IButton( void (*click_)() )
: click(click_)
{
}
};
// call as:
// (button.*click)();
Это имеет то преимущество, что вы можете повторно связать среду выполнения событий без необходимости заново создавать свой класс (поскольку C++ не имеет синтаксиса для изменения полиморфных типов, это обходной путь для классов-хамелеонов).
Советы:
click в конструкторе вашего потомка.protected и есть ссылка и / или получатель public.if и изменений состояния в вашем коде, этот мощь будет быстрее, чем switch()es или ifs (ожидается, что результат будет около 3-4 if, но всегда сначала измеряйте.std::function<> вместо указателей на функции, вы мощь сможете управлять всеми данными вашего объекта в IBase. С этого момента у вас могут быть схемы значений для IBase (например, std::vector<IBase> будет работать). Обратите внимание, что этот мощь будет медленнее в зависимости от вашего компилятора и кода STL; также, что текущие реализации std::function<> имеют тенденцию иметь накладные расходы по сравнению с указателями функций или даже виртуальными функциями (это может измениться в будущем).Вот определение abstract class в стандарте С ++
n4687
13.4.2
An abstract class is a class that can be used only as a base class of some other class; no objects of an abstract class can be created except as subobjects of a class derived from it. A class is abstract if it has at least one pure virtual function.
The C++ interfaces are implemented using abstract classes and these abstract classes should not be confused with data abstraction which is a concept of keeping implementation details separate from associated data.
Example:
class Shape {
public:
// pure virtual function providing interface framework.
virtual int getArea() = 0;
virtual ~Shape(){}
protected:
int width;
int height;
};
// Derived classes
class Rectangle: public Shape {
public:
int getArea() {
return (width * height);
}
};
class Triangle: public Shape {
public:
int getArea() {
return (width * height)/2;
}
};
у вас должен быть деструктор ничего не делать в IDemo, чтобы было определено поведение, которое нужно делать: IDemo * p = new Child; / * что угодно * / delete p;