Як налаштувати клас, який представляє інтерфейс? Це просто абстрактний базовий клас?
Як налаштувати клас, який представляє інтерфейс? Це просто абстрактний базовий клас?
Відповіді:
Щоб розширити відповідь на 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
тип), і тому компілятор повинен надати дійсну версію. Тож якщо ви не отримаєте явного виклику віртуальних деструкторів через this
будівництво :), ви можете заощадити на розмірі коду.
override
ключове слово, щоб дозволити аргумент часу компіляції та перевірку типу повернення значення. Наприклад, у декларації про дитинуvirtual void OverrideMe() override;
Складіть клас із чистими віртуальними методами. Використовуйте інтерфейс, створивши інший клас, який перекриває ці віртуальні методи.
Чистий віртуальний метод - метод класу, який визначається як віртуальний і присвоюється 0.
class IDemo
{
public:
virtual ~IDemo() {}
virtual void OverrideMe() = 0;
};
class Child : public IDemo
{
public:
virtual void OverrideMe()
{
//do stuff
}
};
override
C ++ 11
Вся причина, що у вас є спеціальна категорія типу інтерфейсу на додаток до абстрактних базових класів у C # / Java , тому що C # / Java не підтримують багатократне успадкування.
C ++ підтримує багаторазове успадкування, тому спеціальний тип не потрібен. Абстрактний базовий клас, що не має абстрактних (чистих віртуальних) методів, функціонально еквівалентний інтерфейсу C # / Java.
Thread
примірником. Багаторазове успадкування може бути поганим дизайном, а також композицією. Все залежить від випадку.
У C ++ немає поняття "інтерфейс". AFAIK, інтерфейси вперше були введені на Java, щоб подолати відсутність багаторазового успадкування. Ця концепція виявилася досить корисною, і такого ж ефекту можна досягти в C ++, використовуючи абстрактний базовий клас.
Абстрактний базовий клас - це клас, у якому принаймні один член-функція (метод у Java lingo) є чистою віртуальною функцією, оголошеною за допомогою наступного синтаксису:
class A
{
virtual void foo() = 0;
};
Абстрактний базовий клас не може бути ініціалізований, тобто ви не можете оголосити об'єкт класу A. Ви можете отримати класи лише з A, але будь-який похідний клас, який не забезпечує реалізацію foo()
, також буде абстрактним. Щоб перестати бути абстрактним, похідний клас повинен забезпечити реалізацію для всіх чистих віртуальних функцій, які він успадковує.
Зауважте, що абстрактний базовий клас може бути більше, ніж інтерфейс, тому що він може містити дані учасників та функції членів, які не є чисто віртуальними. Еквівалентом інтерфейсу був би абстрактний базовий клас без будь-яких даних, що мають лише чисті віртуальні функції.
І, як зазначав Марк Рансом, абстрактний базовий клас повинен мати для цього віртуальний деструктор, як і будь-який базовий клас.
Наскільки я міг протестувати, дуже важливо додати віртуальний деструктор. Я використовую об'єкти, створені 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() {}
}
Або
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;
// ...
};
У цьому випадку Інтерфейс має еталонну семантику, тобто ви повинні переконатися, що об'єкт пережив інтерфейс (можливо також зробити інтерфейси зі значеннями семантики).
Ці інтерфейси мають свої плюси і мінуси:
Нарешті, успадкування - корінь усього зла в складній програмі. У поліморфізмі, що базується на значеннях сеансів і концепцій батьків (настійно рекомендується, тут роз'яснюються кращі версії цієї методики), вивчається наступний випадок:
Скажіть, у мене є програма, в якій я полімерно використовую свої форми за допомогою 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
?).
Оновлення: Є кілька нових посилань про поліморфізм, заснований на спадщині:
Circle
клас - поганий дизайн. У Adapter
таких випадках слід використовувати шаблон. Вибачте, якщо це здасться трохи суворим, але спробуйте скористатися якоюсь реальною бібліотекою на зразок, Qt
перш ніж приймати судження про спадщину. Спадкування значно полегшує життя.
Adapter
шаблону? Мені цікаво побачити його переваги.
Square
його вже немає? Передбачення? Ось чому вона відірвана від реальності. І насправді, якщо ви вирішите покластися на бібліотеку "MyShape", ви можете використовувати її інтерфейс з самого початку. У прикладі фігур багато дурниць (одна з яких полягає у тому, що у вас є дві Circle
структури), але адаптер виглядав би приблизно так -> ideone.com/UogjWk
Усі хороші відповіді вище. Ще одне, що слід пам’ятати - ви також можете мати чистий віртуальний деструктор. Єдина відмінність полягає в тому, що вам все-таки потрібно це здійснити.
Плутати?
--- 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 pointer у цьому класі, тому ви ніколи не можете його інстанціювати безпосередньо. Дивіться документацію тут - новинка .
novtable
над стандартнимиvirtual void Bar() = 0;
= 0;
яке я додав). Прочитайте документацію, якщо ви її не розумієте.
= 0;
і припускав, що це просто нестандартний спосіб зробити саме те саме.
Невелике доповнення до написаного там:
По-перше, переконайтеся, що ваш деструктор також чистий віртуальний
По-друге, ви, можливо, захочете успадковувати практично (а не зазвичай) під час виконання, лише для хороших заходів.
Ви також можете розглянути класи контрактів, реалізовані за допомогою NVI (Non Virtual Interface Pattern). Наприклад:
struct Contract1 : boost::noncopyable
{
virtual ~Contract1();
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:
virtual void do_f(Parameters p); // From contract 1.
virtual void do_g(Parameters p); // From contract 2.
};
Я все ще новий в розробці 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-4 if
с, але завжди вимірюйте спочатку.std::function<>
функціональні вказівники, ви, можливо, зможете керувати всіма вашими об'єктними даними IBase
. З цього моменту ви можете мати схеми значень для IBase
(наприклад, std::vector<IBase>
буде працювати). Зауважте, що це може бути повільніше, залежно від вашого компілятора та коду STL; також, що поточні реалізації, std::function<>
як правило, мають накладні витрати порівняно з покажчиками функцій або навіть віртуальними функціями (це може змінитися в майбутньому).Ось визначення abstract class
стандарту c ++
n4687
13.4.2
Абстрактний клас - це клас, який можна використовувати лише як базовий клас якогось іншого класу; жодні об'єкти абстрактного класу не можуть бути створені, крім як суб'єкти класу, похідні від нього. Клас є абстрактним, якщо він має принаймні одну чисту віртуальну функцію.
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 () та два інші класи реалізували ту саму функцію, але з різним алгоритмом для обчислення площі, специфічної для форми.