Неявні проти явних інтерфейсів


9

Я думаю, що я розумію фактичні обмеження поліморфізму під час компіляції та поліморфізму під час виконання. Але в чому полягають концептуальні відмінності між явними інтерфейсами (поліморфізм під час виконання. Тобто віртуальні функції та покажчики / посилання) та неявними інтерфейсами (поліморфізм компіляції, тобто шаблони) .

Мої думки полягають у тому, що два об’єкти, які пропонують один і той же явний інтерфейс, повинні бути одного типу об’єктів (або мати спільного предка), тоді як два об'єкти, які пропонують той самий неявний інтерфейс, не повинні бути однотипними об'єктами, і, виключаючи неявні Інтерфейс, який вони обидва пропонують, може мати зовсім інший функціонал.

Будь-які думки з цього приводу?

І якщо два об’єкти пропонують один і той же неявний інтерфейс, то які причини (крім технічної вигоди від не потребує динамічної диспетчеризації з таблицею пошуку віртуальних функцій тощо) є тим, що ці об'єкти не успадковуються від базового об'єкта, який оголошує цей інтерфейс, таким чином що робить його явним інтерфейсом? Інший спосіб сказати це: чи можете ви надати мені випадок, коли два об’єкти, які пропонують однаковий неявний інтерфейс (і тому можуть використовуватися як типи до зразкового класу шаблонів), не повинні успадковувати базовий клас, який робить цей інтерфейс явним?

Деякі пов’язані публікації:


Ось приклад, щоб зробити це питання більш конкретним:

Неявний інтерфейс:

class Class1
{
public:
  void interfaceFunc();
  void otherFunc1();
};

class Class2
{
public:
  void interfaceFunc();
  void otherFunc2();
};

template <typename T>
class UseClass
{
public:
  void run(T & obj)
  {
    obj.interfaceFunc();
  }
};

Явний інтерфейс:

class InterfaceClass
{
public:
  virtual void interfaceFunc() = 0;
};

class Class1 : public InterfaceClass
{
public:
  virtual void interfaceFunc();
  void otherFunc1();
};

class Class2 : public InterfaceClass
{
public:
  virtual void interfaceFunc();
  void otherFunc2();
};

class UseClass
{
public:
  void run(InterfaceClass & obj)
  {
    obj.interfaceFunc();
  }
};

Ще більш поглиблений, конкретний приклад:

Деякі проблеми C ++ можна вирішити будь-яким:

  1. шаблонний клас, тип шаблону якого дає неявний інтерфейс
  2. не шаблонований клас, який приймає покажчик базового класу, який забезпечує явний інтерфейс

Код, який не змінюється:

class CoolClass
{
public:
  virtual void doSomethingCool() = 0;
  virtual void worthless() = 0;
};

class CoolA : public CoolClass
{
public:
  virtual void doSomethingCool()
  { /* Do cool stuff that an A would do */ }

  virtual void worthless()
  { /* Worthless, but must be implemented */ }
};

class CoolB : public CoolClass
{
public:
  virtual void doSomethingCool()
  { /* Do cool stuff that a B would do */ }

  virtual void worthless()
  { /* Worthless, but must be implemented */ }
};

Випадок 1 . Неспланований клас, який приймає покажчик базового класу, що забезпечує явний інтерфейс:

class CoolClassUser
{
public:  
  void useCoolClass(CoolClass * coolClass)
  { coolClass.doSomethingCool(); }
};

int main()
{
  CoolA * c1 = new CoolClass;
  CoolB * c2 = new CoolClass;

  CoolClassUser user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

Випадок 2 . Шаблон шаблону, тип шаблону якого надає неявний інтерфейс:

template <typename T>
class CoolClassUser
{
public:  
  void useCoolClass(T * coolClass)
  { coolClass->doSomethingCool(); }
};

int main()
{
  CoolA * c1 = new CoolClass;
  CoolB * c2 = new CoolClass;

  CoolClassUser<CoolClass> user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

Випадок 3 . Шаблон шаблону, тип шаблону якого надає неявний інтерфейс (цього разу не випливає з CoolClass:

class RandomClass
{
public:
  void doSomethingCool()
  { /* Do cool stuff that a RandomClass would do */ }

  // I don't have to implement worthless()! Na na na na na!
}


template <typename T>
class CoolClassUser
{
public:  
  void useCoolClass(T * coolClass)
  { coolClass->doSomethingCool(); }
};

int main()
{
  RandomClass * c1 = new RandomClass;
  RandomClass * c2 = new RandomClass;

  CoolClassUser<RandomClass> user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

Випадок 1 вимагає, щоб об'єкт, що передається, useCoolClass()був дитиною CoolClass(і реалізовувався worthless()). Випадки 2 і 3, з іншого боку, прийматимуть будь-який клас, який має doSomethingCool()функцію.

Якби користувачі коду завжди були тонкокласифіковані CoolClass, тоді Case 1 має інтуїтивний сенс, тому що CoolClassUserзавжди очікував би впровадження а CoolClass. Але припустимо, що цей код буде частиною рамки API, тому я не можу передбачити, чи користувачі захочуть підклас CoolClassабо згорнути власний клас, який має doSomethingCool()функцію.


Можливо, мені чогось не вистачає, але чи не важлива відмінність вже стисло викладена у вашому першому абзаці, це те, що явні інтерфейси є поліморфізмом під час виконання, тоді як неявні інтерфейси - це поліморфізм часу компіляції?
Роберт Харві

2
Є деякі проблеми, які можна вирішити або класом, або функцією, яка переносить вказівник на абстрактний клас (що забезпечує явний інтерфейс), або з шаблоновим класом або функцією, яка використовує об'єкт, що забезпечує неявний інтерфейс. Обидва рішення працюють. Коли ви хочете використати перше рішення? Другий?
Кріс Морріс

Я думаю, що більшість цих міркувань розпадаються, коли ви трохи більше відкриваєте поняття. наприклад, куди б ви помістили статичний поліморфізм без успадкування?
Хав'єр

Відповіді:


8

Ви вже визначили важливий момент - один - час виконання, а другий - час компіляції . Справжня інформація, яка вам потрібна, - це наслідки цього вибору.

Час компіляції:

  • Pro: Інтерфейси часу компіляції набагато більш детальні, ніж інтерфейси під час виконання. Я маю на увазі те, що ви можете використовувати лише вимоги однієї функції або набору функцій, як ви їх називаєте. Вам не потрібно завжди робити весь інтерфейс. Вимоги є лише і саме те, що вам потрібно.
  • Pro: Такі методи, як CRTP, означають, що ви можете використовувати неявні інтерфейси для реалізації за замовчуванням таких речей, як оператори. Ви ніколи не могли зробити подібне з успадкуванням часу.
  • Pro: Неявні інтерфейси набагато простіше складати та множити "успадковувати", ніж інтерфейси під час виконання, і не накладають будь-яких видів бінарних обмежень - наприклад, класи POD можуть використовувати неявні інтерфейси. Немає необхідності в virtualспадкуванні або інших шенагінгах із неявними інтерфейсами - велика перевага.
  • Pro: Компілятор може зробити більше оптимізацій для інтерфейсів часу компіляції. Крім того, безпека додаткового типу забезпечує безпечніший код.
  • Pro: Вводити значення інтерфейсів під час виконання неможливо, оскільки ви не знаєте розмір або вирівнювання кінцевого об'єкта. Це означає, що будь-який випадок, який потребує / виграє від типізації цінності, отримує великі вигоди від шаблонів.
  • Con: Шаблони - це сука для компіляції та використання, і вони можуть ненавмисно переноситися між компіляторами
  • Con: Шаблони не можна завантажувати під час виконання (очевидно), тому вони мають обмеження у вираженні динамічних структур даних, наприклад.

Час виконання:

  • Про: остаточний тип не повинен визначатися до запуску. Це означає, що успадкування часу виконання може виражати деякі структури даних набагато простіше, якщо шаблони можуть це зробити взагалі. Крім того, ви можете експортувати поліморфні типи під час виконання через межі C, наприклад, COM.
  • Pro: Набагато простіше вказати та реалізувати успадкування під час виконання, і ви дійсно не отримаєте особливості компілятора.
  • Con: Успадкування часу виконання може бути повільніше, ніж спадкове час компіляції.
  • Con: Спадщина виконання часу втрачає інформацію про тип.
  • Кон: Надання успадкування під час виконання є менш гнучким.
  • Кон: Багатократне успадкування - сука.

Враховуючи відносний список, якщо вам не потрібна певна перевага успадкування під час виконання, не використовуйте його. Він повільніше, менш гнучкий та менш безпечний, ніж шаблони.

Змінити: Варто зазначити , що в C ++ в зокрема , є використовую для наслідування іншого , ніж час виконання поліморфізму. Наприклад, ви можете успадкувати typedefs або використовувати його для тегування типів або використовувати CRTP. Зрештою, ці методи (та інші) дійсно підпадають під "час компіляції", навіть якщо вони реалізовані з використанням class X : public Y.


Що стосується вашого першого професіонала для компіляції, це пов’язано з одним із моїх головних питань. Чи хотіли б ви коли-небудь зрозуміти, що ви хочете працювати лише з явним інтерфейсом. Тобто "Мені все одно, чи ти маєш усі потрібні мені функції, якщо ти не успадковуєш Клас Z, то я не хочу нічого спільного з тобою". Крім того, спадкування під час виконання не втрачає інформацію про тип при використанні покажчиків / посилань, правильно?
Кріс Морріс

@ChrisMorris: Ні. Якщо це працює, то це працює, то все, що вам слід подбати. Навіщо змусити когось записати такий самий код в іншому місці?
jmoreno

1
@ChrisMorris: Ні, я б не став. Якщо мені потрібен лише X, то це один з основних фундаментальних принципів інкапсуляції, про який я повинен коли-небудь просити і дбати про X. Також він втрачає інформацію про тип. Наприклад, ви не можете стеком виділяти об'єкти такого типу. Ви не можете створити інстанціювання шаблону з істинним типом. Ви не можете викликати на них шаблонні функції членів.
DeadMG

Як щодо ситуації, коли у вас клас Q, який використовує якийсь клас. Q приймає параметр шаблону, тому будь-який клас, який надає неявний інтерфейс, буде робити, або так ми думаємо. Виявляється, клас Q також очікує, що його внутрішній клас (називайте його H) використовувати інтерфейс Q. Наприклад, коли об'єкт H знищений, він повинен викликати деяку функцію Q. Це не можна вказати в неявному інтерфейсі. Таким чином, шаблони виходять з ладу. Більш чітко кажучи, щільно поєднаний набір класів, який вимагає більше, ніж просто неявні інтерфейси один від одного, схоже, унеможливлює використання шаблонів.
Кріс Морріс

Con компіляція: Некрасиво налагоджувати, необхідність вводити визначення в заголовок
JFFIGK
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.