Неявные и явные интерфейсы

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всегда ожидал бы реализацию a CoolClass. Но предположим, что этот код будет частью структуры API, поэтому я не могу предсказать, захотят ли пользователи создавать подклассы CoolClassили прокрутить свой собственный класс, имеющий doSomethingCool()функцию.

Крис Моррис
источник
Может быть, я что-то упускаю, но разве это важное отличие, уже кратко изложенное в вашем первом абзаце, заключается в том, что явные интерфейсы - это полиморфизм времени выполнения, тогда как неявные интерфейсы - это полиморфизм времени компиляции?
Роберт Харви
2
Есть некоторые проблемы, которые могут быть решены с помощью класса или функции, которая получает указатель на абстрактный класс (который предоставляет явный интерфейс), или с помощью шаблонного класса или функции, которая использует объект, который обеспечивает неявный интерфейс. Оба решения работают. Когда бы вы хотели использовать первое решение? Второй?
Крис Моррис
Я думаю, что большинство из этих соображений распадаются, когда вы открываете концепции немного больше. например, где бы вы подходили статический полиморфизм без наследования?
Хавьер

Ответы:

8

Вы уже определили важный момент: один - время выполнения, а другой - время компиляции . Реальная информация, которая вам нужна, это последствия этого выбора.

Compiletime:

  • Pro: Интерфейсы во время компиляции намного более детализированы, чем во время выполнения. Под этим я подразумеваю то, что вы можете использовать только требования одной функции или набора функций, как вы их вызываете. Вам не нужно всегда делать весь интерфейс. Требования только и именно то, что вам нужно.
  • Pro: Такие методы, как CRTP, означают, что вы можете использовать неявные интерфейсы для реализации по умолчанию таких вещей, как операторы. Вы никогда не могли бы сделать такую ​​вещь с наследованием во время выполнения.
  • Pro: неявные интерфейсы гораздо проще создавать и умножать «наследовать», чем интерфейсы времени выполнения, и не накладывают никаких двоичных ограничений - например, классы POD могут использовать неявные интерфейсы. Нет необходимости в virtualнаследовании или других махинациях с неявными интерфейсами - большое преимущество.
  • Pro: Компилятор может сделать больше оптимизаций для интерфейсов времени компиляции. Кроме того, дополнительная безопасность типа делает для более безопасного кода.
  • Pro: Невозможно ввести значения для интерфейсов времени выполнения, потому что вы не знаете размер или выравнивание конечного объекта. Это означает, что любой случай, который нуждается / выигрывает от ввода значений, получает большие преимущества от шаблонов.
  • Против: Шаблоны - это сука для компиляции и использования, и их можно легко портировать между компиляторами.
  • Con: Шаблоны не могут быть загружены во время выполнения (очевидно), поэтому они имеют ограничения в выражении динамических структур данных, например.

Runtime:

  • Pro: Окончательный тип не должен быть определен до времени выполнения. Это означает, что наследование во время выполнения может намного проще выразить некоторые структуры данных, если шаблоны могут это делать вообще. Также вы можете экспортировать полиморфные типы во время выполнения через границы C, например, COM.
  • Pro: намного проще определить и реализовать наследование во время выполнения, и вы не получите никакого поведения, специфичного для компилятора.
  • Con: наследование во время выполнения может быть медленнее, чем наследование во время компиляции.
  • Con: наследование во время выполнения теряет информацию о типе.
  • Против: наследование во время выполнения является гораздо менее гибким.
  • Против: множественное наследование сука.

Учитывая относительный список, если вам не нужно определенное преимущество наследования во время выполнения, не используйте его. Это медленнее, менее гибко и менее безопасно, чем шаблоны.

Изменить: Стоит отметить , что в C ++ в частности , есть использую для наследования другого , чем время выполнения полиморфизма. Например, вы можете наследовать typedefs, или использовать его для маркировки типов, или использовать CRTP. В конечном счете, однако, эти методы (и другие) действительно подпадают под «время компиляции», даже если они реализованы с использованием class X : public Y.

DeadMG
источник
Что касается вашего первого профессионала во время компиляции, это связано с одним из моих главных вопросов. Хотели бы вы когда-нибудь дать понять, что вы хотите работать только с явным интерфейсом. То есть. «Мне все равно, если у вас есть все функции, которые мне требуются, если вы не наследуете от класса Z, тогда я не хочу иметь с вами ничего общего». Кроме того, наследование во время выполнения не теряет информацию о типе при использовании указателей / ссылок, правильно?
Крис Моррис
@ChrisMorris: Нет. Если это работает, то это работает, и это все, что вам нужно заботиться. Зачем заставлять кого-то писать такой же код в другом месте?
Jmoreno
1
@ChrisMorris: нет, я бы не стал. Если мне нужен только X, то это один из основных фундаментальных принципов инкапсуляции, о котором мне следует только просить и заботиться о X. Кроме того, он теряет информацию о типах. Вы не можете, например, стек выделять объект такого типа. Вы не можете создать экземпляр шаблона с его истинным типом. Вы не можете вызывать шаблонные функции-члены на них.
DeadMG
Как насчет ситуации, когда у вас есть класс Q, который использует какой-то класс. Q принимает параметр шаблона, поэтому подойдет любой класс, который предоставляет неявный интерфейс, или мы так думаем. Оказывается, класс Q также ожидает, что его внутренний класс (назовите его H) будет использовать интерфейс Q. Например, когда объект H разрушается, он должен вызывать некоторую функцию из Q. Это не может быть указано в неявном интерфейсе. Таким образом, шаблоны терпят неудачу. Проще говоря, тесно связанный набор классов, который требует не просто неявных интерфейсов друг от друга, по-видимому, лишает возможности использования шаблонов.
Крис Моррис
Время компиляции: уродливо для отладки, необходимость поместить определения в заголовок
JFFIGK