Является ли плохой практикой использование абстрактного базового класса для применения общего интерфейса для типа параметров шаблона?

#c #templates #polymorphism #c -concepts

Вопрос:

Я новичок в программировании шаблонов и наткнулся на эту идиому. Например, что-то вроде:

 class FooBase {
 public:
  virtual void do_something() = 0;
};

template <class Foo> // Foo is derived from FooBase
void g(Fooamp; foo) {
  static_assert(std::is_base_of<FooBase, Foo>::value);
  // ...
  foo.do_something();
  // ...
}
 

По моему собственному мнению, этот шаблон полезен, потому что:

  1. Объявление чистой виртуальной функции имеет явную подпись; по сравнению с предложением концепции C 20 requires (если оно вообще доступно), легко указать атрибуты, аргументы и тип возвращаемого значения.
  2. Это хорошая документация; требования к определению нового класса Foo ясны из простого чтения FooBase заголовка.
  3. Можно реорганизовать общую функциональность всех Foo в один и тот же FooBase класс.

Однако меня беспокоят последствия использования virtual функции для производительности. Насколько я понимаю, во время выполнения нет затрат, так как функция вызывается из производного класса; но компоновщик не сможет встроить функцию.

Еще одним недостатком является то, что функции виртуальных шаблонов не разрешены.

Наконец, я беспокоюсь, что это может быть запах кода в целом, так как он использует функцию полиморфизма во время выполнения для выполнения статической проверки. Концепции C 20, вероятно, являются «правильным» способом сделать это, но они кажутся гораздо менее удобными по трем вышеперечисленным причинам.

Комментарии:

1. Где именно находится правоприменение? Любой класс с do_something членом, который не принимает аргументы в вызове, будет приветствоваться вашим шаблоном функции.

2. В таком случае, разве вы не void g(FooBaseamp; foo) { ... } сделали бы то, что хотите?

3. @Рассказчик-UnslanderMonica правда, я должен включить a static_assert , но оставил его для примера

4. @TedLyngmo, но тип Foo известен во время компиляции, поэтому мне не нужно использовать полиморфизм во время выполнения

5. @NickFP после вашего редактирования мой (теперь удаленный) комментарий остается в силе-ваше «бесплатно во время выполнения, так как функция вызывается из производного класса» не является надежным утверждением, потому что любой FooDerivedamp; тип может быть производным от FooDerived него .

Ответ №1:

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

Суть динамического полиморфизма заключается в том, что он динамичен: определяется во время выполнения таким образом, что вы можете передавать объекты функциям, которые не полностью знают тип, который им задан во время компиляции. Это позволяет писать код в одном месте против базового класса, а не экспортировать его в заголовки и тому подобное. И фактический класс, используемый в любой момент, также не может быть экспортирован в заголовки и тому подобное. Только источник класса должен знать фактический тип.

Полиморфизм во время компиляции, подобный шаблонам, основан на знании всего во время компиляции. Весь код, как исходный, так и конечный, должен знать, каковы типы.

По сути, вам нравится, как динамический полиморфизм формулирует свои требования по сравнению с полиморфизмом во время компиляции, но вы пытаетесь снизить затраты на производительность, используя полиморфизм во время компиляции. Это создает путаницу в коде, так как вы путаете механизмы.

Если кто-то увидит базовый класс с virtual функциями, он будет предполагать, что ваш код будет передавать указатели/ссылки на фактические классы вокруг. Это часть ожиданий с их использованием в большинстве случаев ( virtual стирание типов на основе четности эффективно делает это). Вместо этого вы увидите множество функций шаблонов, которые принимают типы по значению, что приведет к путанице.

Кроме того, у вас нет механизма принудительного исполнения. Функция, которая принимает a FooBaseamp; , гарантирует, что пользователь не сможет вызвать ее с типом, который не является фактическим FooBase производным типом (ну, вы могли бы сделать его неявно конвертируемым в один, но давайте здесь проигнорируем вероломство). Ваши функции шаблона, избегающие концепций, не имеют аналогичного применения. Вы можете задокументировать, что это должен быть FooBase производный тип, но вы не применяете его статически.

По крайней мере, параметр шаблона должен быть объявлен как std::derived_from<FooBase> Foo (и нет, static_assert это не очень хорошая идея).

Но на самом деле, вы должны просто использовать правильную концепцию. Вам нужен полиморфизм во время компиляции, и каковы бы ни были ваши личные представления о концепциях, концепции-это языковой механизм, который C 20 использует для определения прототипа полиморфизма во время компиляции. Никто не будет сбит с толку относительно смысла и цели вашего кода, если вы будете использовать концепции.