Как мне настроить класс, представляющий интерфейс? Это просто абстрактный базовый класс?
Как вы объявляете интерфейс на C ++?
Ответы (17)
Чтобы расширить ответ 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
}
};
Вам не нужно включать тело виртуального деструктора - оказывается, у некоторых компиляторов есть проблемы с оптимизацией пустого деструктора, и вам лучше использовать значение по умолчанию.
=0
) деструктор с телом. Преимущество здесь в том, что компилятор теоретически может увидеть, что vtable не имеет допустимых членов, и полностью отбросить его. С помощью виртуального деструктора с телом указанный деструктор может быть вызван (виртуально), например в середине построения через указатель this
(когда построенный объект все еще имеет тип Parent
), и поэтому компилятор должен предоставить действительную vtable. Так что, если вы явно не вызываете виртуальные деструкторы через this
во время построения :), вы можете сэкономить на размере кода.
- person Pavel Minaev; 31.12.2009
throw
из конструктора производного класса.
- person Mark Ransom; 16.08.2012
Base
‹- Derived
‹ - MostDerived
. Вы находитесь в производном ctor. Вы вызываете глобальную функцию, которая принимает Base*
аргумент, передавая его this
. Эта функция может явно вызывать деструктор через указатель, и он должен быть отправлен виртуально.
- person Pavel Minaev; 16.08.2012
MostDerived
очень недоволен. По крайней мере, при уничтожении через throw
остальная часть конструкции не выполняется.
- person Mark Ransom; 16.08.2012
MostDerived
, это определенно будет UB, но я не вижу ничего, что могло бы сделать, например. this->~Foo()
в ctor UB само собой. Рассмотрим случай, когда вы разрушаете его, а затем никогда не возвращаетесь к ctor (например, просто сидите в бесконечном цикле, принимая и обрабатывая ввод). Насколько я понимаю, внутри этого цикла ваше поведение будет четко определено.
- person Pavel Minaev; 17.08.2012
override
, чтобы разрешить аргумент во время компиляции и проверку типа возвращаемого значения. Например, в объявлении Child virtual void OverrideMe() override;
- person Sean; 15.01.2013
*a = *b
но я допускаю, что это нечасто.
- person Mark Ransom; 17.12.2014
Child
нужно ли писать OverrideMe()
как virtual void
?
- person Sparker0i; 10.08.2018
virtual
не является обязательным, поскольку объявление базового класса уже сделало его виртуальным, но хорошая практика - быть последовательной. void
по-прежнему нужен.
- person Mark Ransom; 10.08.2018
Создайте класс с чистыми виртуальными методами. Используйте интерфейс, создав другой класс, который переопределяет эти виртуальные методы.
Чистый виртуальный метод - это метод класса, который определяется как виртуальный и имеет значение 0.
class IDemo
{
public:
virtual ~IDemo() {}
virtual void OverrideMe() = 0;
};
class Child : public IDemo
{
public:
virtual void OverrideMe()
{
//do stuff
}
};
override
в C ++ 11
- person keyser; 27.11.2014
Child::OverrideMe()
как virtual
, @Cemre, потому что IDemo::OverrideMe()
неявно делает Child::OverrideMe()
virtual
. Наличие указания на то, что это виртуальная функция (например, virtual
или, что еще лучше, override
в C ++ 11 или новее) послужит напоминанием для программистов, а override
, в частности, также поможет компилятору проверить, действительно ли он что-то переопределяет.
- person Justin Time - Reinstate Monica; 28.07.2019
Вся причина того, что у вас есть особая категория типа интерфейса в дополнение к абстрактным базовым классам в C # / Java - это потому, что C # / Java не поддерживает множественное наследование.
C ++ поддерживает множественное наследование, поэтому особый тип не требуется. Абстрактный базовый класс без неабстрактных (чисто виртуальных) методов функционально эквивалентен интерфейсу C # / Java.
java.lang.Thread
класс и java.lang.Runnable
интерфейс, который существует только потому, что вы не можете расширить Thread
вместе с другой базой. Из документации может показаться, что это предусмотрено на ваш выбор, но однажды я был вынужден использовать Runnable
из-за отсутствия множественного наследования.
- person doc; 15.10.2014
Thread
. Множественное наследование может быть плохим дизайном, а также плохой композицией. Все зависит от случая.
- person doc; 28.11.2014
abstract
.
- person Don Larynx; 24.03.2015
В C ++ нет понятия «интерфейс» как такового. AFAIK, интерфейсы были впервые представлены в Java, чтобы обойти отсутствие множественного наследования. Эта концепция оказалась весьма полезной, и того же эффекта можно достичь в C ++ с помощью абстрактного базового класса.
Абстрактный базовый класс - это класс, в котором по крайней мере одна функция-член (метод на языке Java) является чистой виртуальной функцией, объявленной с использованием следующего синтаксиса:
class A
{
virtual void foo() = 0;
};
Невозможно создать экземпляр абстрактного базового класса, т.е. е. вы не можете объявить объект класса A. Вы можете только унаследовать классы от A, но любой производный класс, не обеспечивающий реализацию foo()
, также будет абстрактным. Чтобы перестать быть абстрактным, производный класс должен предоставлять реализации для всех чистых виртуальных функций, которые он наследует.
Обратите внимание, что абстрактный базовый класс может быть больше, чем интерфейс, потому что он может содержать элементы данных и функции-члены, которые не являются чисто виртуальными. Эквивалент интерфейса был бы абстрактным базовым классом без каких-либо данных только с чистыми виртуальными функциями.
И, как указал Марк Рэнсом, абстрактный базовый класс должен предоставлять виртуальный деструктор, как и любой базовый класс, если на то пошло.
class Button; class Image; class ClickableIcon : public Button, public Image {};
. Если они спроектированы правильно, Image предоставит графические функции, а Button предоставит интерактивные функции без перекрытия, которое может вызвать проблемы, в то время как ClickableIcon сам обрабатывает функции, для которых предназначена кнопка.
- person Justin Time - Reinstate Monica; 08.04.2016
Button
предоставляет интерактивный код, Image
предоставляет графический код, а ClickableIcon
предоставляет код, специфичный для него самого, а также любой код, необходимый для объединения двух родительских классов. Пока два класса спроектированы правильно и не имеют пересекающихся имен членов, класс должен быть полностью жизнеспособным и более простым в реализации, чем если бы один или оба были интерфейсом.
- person Justin Time - Reinstate Monica; 08.04.2016
Насколько я мог проверить, очень важно добавить виртуальный деструктор. Я использую объекты, созданные с помощью 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()
никогда не вызывается.
Мой ответ в основном такой же, как и другие, но я думаю, что нужно сделать еще две важные вещи:
Объявите виртуальный деструктор в своем интерфейсе или сделайте защищенный не виртуальный, чтобы избежать неопределенного поведения, если кто-то попытается удалить объект типа
IDemo
.Используйте виртуальное наследование, чтобы избежать проблем с множественным наследованием. (Когда мы используем интерфейсы, чаще возникает множественное наследование.)
И как другие ответы:
- Создайте класс с чистыми виртуальными методами.
Используйте интерфейс, создав другой класс, который переопределяет эти виртуальные методы.
class IDemo { public: virtual void OverrideMe() = 0; virtual ~IDemo() {} }
Or
class IDemo { public: virtual void OverrideMe() = 0; protected: ~IDemo() {} }
А также
class Child : virtual public IDemo { public: virtual void OverrideMe() { //do stuff } }
В 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;
// ...
};
В этом случае интерфейс имеет ссылочную семантику, т.е. вы должны убедиться, что объект переживает интерфейс (также можно создавать интерфейсы с семантикой значений).
У такого типа интерфейсов есть свои плюсы и минусы:
- Они требуют больше памяти, чем полиморфизм на основе наследования.
- Они в целом быстрее, чем полиморфизм на основе наследования. .
- В тех случаях, когда вы знаете окончательный тип, они намного быстрее! (некоторые компиляторы, такие как gcc и clang, выполняют больше оптимизаций в типах, которые не имеют / наследуются от типов с виртуальными функциями).
Наконец, наследование - это корень всех зол в сложной разработке программного обеспечения. В Семантике значений Шона Родителя и полиморфизме на основе концепций (настоятельно рекомендуется, лучшие версии этого там объясняется техника) исследуется следующий случай:
Скажем, у меня есть приложение, в котором я обрабатываю свои фигуры полиморфно, используя интерфейс 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
?).
Обновление: есть несколько новых ссылок о полиморфизме на основе ненаследования:
- Наследование является базовым классом для зло говорить.
- Обсуждение семантики значений и концептуального полиморфизма Шона Родителя.
- Доклад Пири Яхколы о полиморфизме без наследования и документы библиотеки poly.
- Выступление Зака Лейна, Прагматическое стирание типов: решение проблем ООП с помощью элегантного шаблона дизайна.
- Блог Анджея C ++ - Введите Erasure parts i, ii, iii и iv.
- Полиморфное универсальное программирование во время выполнения - смешивание объектов и концепций в ConceptC ++
- Boost.TypeErasure docs
- Adobe Poly docs
- Boost.Any, std :: any offer (revision 3), Boost.Spirit :: hold_any .
Circle
класс - плохой дизайн. В таких случаях следует использовать шаблон Adapter
. Извините, если это прозвучит немного жестко, но попробуйте использовать какую-нибудь реальную библиотеку, например Qt
, прежде чем делать выводы о наследовании. Наследование значительно облегчает жизнь.
- person doc; 15.10.2014
Adapter
? Мне интересно увидеть его преимущества.
- person gnzlbg; 15.10.2014
Square
там еще нет? Предвидение? Вот почему он оторван от реальности. И на самом деле, если вы решите полагаться на библиотеку MyShape, вы можете адаптироваться к ее интерфейсу с самого начала. В примере с фигурами много глупостей (одна из которых состоит в том, что у вас есть две Circle
структуры), но адаптер будет выглядеть примерно так - ›ideone.com/UogjWk
- person doc; 15.10.2014
Adapter
. Предоставление реализации в интерфейсе, как в вашем ответе, означает, что я должен предоставить все виртуальные методы, которые в противном случае можно было бы просто унаследовать.
- person doc; 16.10.2014
object_t
внутренним компонентам и я не могу наследовать после него? Насколько я понимаю, мне нужно реализовать draw()
функцию с нуля самостоятельно, и я не привязан к concept_t
интерфейсу. Кроме того, с виртуальными методами у меня могут быть реализации по умолчанию, которые я могу переопределить или нет.
- person doc; 18.10.2014
gnzlbg::vector
в своем классе, и я предоставляю интерфейс с моим пользовательским doc::vector
, и вам нужно преобразовать ввод и вывод в свои внутренние компоненты. Где повторное использование и эффективность нашего кода, если вам нужно их преобразовать?
- person doc; 18.10.2014
doc::vector
, либо gnzlbg::vector
в предпосылке интерфейса. Вы должны научиться правильно использовать наследование, потому что то, что вы сейчас говорите, не имеет никакого смысла!
- person doc; 19.10.2014
Все хорошие ответы выше. Следует иметь в виду еще одну дополнительную вещь - у вас также может быть чистый виртуальный деструктор. Единственная разница в том, что вам еще нужно его реализовать.
Смущенный?
--- header file ----
class foo {
public:
foo() {;}
virtual ~foo() = 0;
virtual bool overrideMe() {return false;}
};
---- source ----
foo::~foo()
{
}
Основная причина, по которой вы захотите это сделать, заключается в том, что вы хотите предоставить методы интерфейса, как у меня, но сделать их переопределение необязательными.
Чтобы сделать класс интерфейсным классом, требуется чистый виртуальный метод, но все ваши виртуальные методы имеют реализации по умолчанию, поэтому единственный метод, который остается сделать чисто виртуальным, - это деструктор.
Повторная реализация деструктора в производном классе вообще не представляет большого труда - я всегда повторно реализую деструктор, виртуальный или нет, в своих производных классах.
Если вы используете компилятор 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;
- person Flexo; 17.09.2011
= 0;
, который я добавил). Прочтите документацию, если вы не понимаете ее.
- person Mark Ingram; 20.09.2011
= 0;
и предположил, что это просто нестандартный способ сделать то же самое.
- person Flexo; 20.09.2011
Вы также можете рассмотреть классы контрактов, реализованные с помощью 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.
};
Небольшое дополнение к тому, что там написано:
Во-первых, убедитесь, что ваш деструктор также является чисто виртуальным.
Во-вторых, вы можете захотеть наследовать виртуально (а не обычно) при реализации, просто для хороших мер.
class Base { public: ~Base() = 0; }; Base::~Base() {}
, чтобы компилятор мог создавать конструкторы по умолчанию для производных классов.
- person Justin Time - Reinstate Monica; 08.04.2016
Я все еще новичок в разработке на C ++. Я начал с Visual Studio (VS).
Тем не менее, похоже, никто не упомянул __interface
в VS (.NET). Я не уверен, что это хороший способ объявить интерфейс. Но, похоже, он обеспечивает дополнительное обеспечение (упоминается в документы). Таким образом, вам не нужно явно указывать virtual TYPE Method() = 0;
, поскольку он будет автоматически преобразован.
__interface IMyInterface {
HRESULT CommitX();
HRESULT get_X(BSTR* pbstrName);
};
Однако я не использую его, потому что меня беспокоит кроссплатформенная совместимость компиляции, поскольку он доступен только в .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
s и изменений состояния в вашем коде это может быть быстрее, чемswitch()
es илиif
s (время выполнения ожидается примерно через 3-4if
s, но всегда сначала измеряйте. - Если вы выберете
std::function<>
вместо указателей на функции, вы можете иметь возможность управлять всеми данными вашего объекта вIBase
. С этого момента у вас могут быть схемы значений дляIBase
(например,std::vector<IBase>
будет работать). Обратите внимание, что этот может работать медленнее в зависимости от вашего компилятора и кода STL; также, что текущие реализацииstd::function<>
имеют тенденцию иметь накладные расходы по сравнению с указателями функций или даже виртуальными функциями (это может измениться в будущем).
В C ++ 20 вы можете использовать concept
вместо класса. Это более эффективно, чем наследование.
template <class T>
concept MyInterface = requires (T t) {
{ t.interfaceMethod() };
};
class Implementation {
public:
void interfaceMethod();
};
static_assert(MyInterface<Implementation>);
Затем вы можете использовать его в функции:
void myFunction(MyInterface auto& arg);
Ограничение в том, что вы не можете использовать его в контейнере.
Вот определение abstract class
в стандарте С ++
n4687
13.4.2
Абстрактный класс - это класс, который может использоваться только как базовый класс какого-либо другого класса; никакие объекты абстрактного класса не могут быть созданы, кроме как подобъекты производного от него класса. Класс является абстрактным, если он имеет хотя бы одну чистую виртуальную функцию.
Если вам нужна только статическая привязка интерфейса (без виртуального, без экземпляров самого типа интерфейса, интерфейс действует только как руководство):
#include <iostream>
#include <string>
// Static binding interface
// Notice: instantiation of this interface should be usefuless and forbidden.
class IBase {
protected:
IBase() = default;
~IBase() = default;
public:
// Methods that must be implemented by the derived class
void behaviorA();
void behaviorB();
void behaviorC() {
std::cout << "This is an interface default implementation of bC().\n";
};
};
class CCom : public IBase {
std::string name_;
public:
void behaviorA() { std::cout << "CCom bA called.\n"; };
};
class CDept : public IBase {
int ele_;
public:
void behaviorB() { std::cout << "CDept bB called.\n"; };
void behaviorC() {
// Overwrite the interface default implementation
std::cout << "CDept bC called.\n";
IBase::behaviorC();
};
};
int main(void) {
// Forbid the instantiation of the interface type itself.
// GCC error: ‘constexpr IBase::IBase()’ is protected within this context
// IBase o;
CCom acom;
// If you want to use these interface methods, you need to implement them in
// your derived class. This is controled by the interface definition.
acom.behaviorA();
// ld: undefined reference to `IBase::behaviorB()'
// acom.behaviorB();
acom.behaviorC();
CDept adept;
// adept.behaviorA();
adept.behaviorB();
adept.behaviorC();
// adept.IBase::behaviorC();
}
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 (), а два других класса реализуют ту же функцию, но с другим алгоритмом для вычисления области, специфичной для формы.