Шаблон или абстрактный базовый класс? - PullRequest
37 голосов
/ 02 марта 2009

Если я хочу сделать класс адаптируемым и сделать возможным выбор различных алгоритмов извне - какова лучшая реализация в C ++?

Я вижу в основном две возможности:

  • Используйте абстрактный базовый класс и передавайте конкретный объект в
  • Использовать шаблон

Вот небольшой пример, реализованный в различных версиях:

Версия 1: Абстрактный базовый класс

class Brake {
public: virtual void stopCar() = 0;  
};

class BrakeWithABS : public Brake {
public: void stopCar() { ... }
};

class Car {
  Brake* _brake;
public:
  Car(Brake* brake) : _brake(brake) { brake->stopCar(); }
};

Версия 2a: Шаблон

template<class Brake>
class Car {
  Brake brake;
public:
  Car(){ brake.stopCar(); }
};

Версия 2b: Шаблон и личное наследование

template<class Brake>
class Car : private Brake {
  using Brake::stopCar;
public:
  Car(){ stopCar(); }
};

Исходя из Java, я, естественно, склонен всегда использовать версию 1, но версии шаблонов, похоже, часто предпочитаются, например, в коде STL? Если это правда, это только из-за эффективности памяти и т. Д. (Без наследования, без вызовов виртуальных функций)?

Я понимаю, что между версиями 2a и 2b нет большой разницы, см. C ++ FAQ .

Можете ли вы прокомментировать эти возможности?

Ответы [ 9 ]

33 голосов
/ 02 марта 2009

Это зависит от ваших целей. Вы можете использовать версию 1, если вы

  • Намерены заменить тормоза автомобиля (во время работы)
  • Намерен передать Car не шаблонным функциям

Как правило, я бы предпочел версию 1 с использованием полиморфизма времени выполнения, потому что он все еще гибкий и позволяет иметь Car по-прежнему того же типа: Car<Opel> - это другой тип, чем Car<Nissan>. Если ваши цели - отличная производительность при частом использовании тормозов, я рекомендую вам использовать шаблонный подход. Кстати, это называется политическим дизайном. Вы предоставляете политику торможения . Пример, потому что вы сказали, что программируете на Java, возможно, вы еще не слишком разбираетесь в C ++. Один из способов сделать это:

template<typename Accelerator, typename Brakes>
class Car {
    Accelerator accelerator;
    Brakes brakes;

public:
    void brake() {
        brakes.brake();
    }
}

Если у вас много политик, вы можете сгруппировать их в свою собственную структуру и передать ее, например, SpeedConfiguration, собирающую Accelerator, Brakes и некоторые другие. В своих проектах я стараюсь сохранить большую часть кода без шаблонов, позволяя им быть скомпилированными один раз в свои собственные объектные файлы, не нуждаясь в их коде в заголовках, но все же допуская полиморфизм (через виртуальные функции). Например, вы можете захотеть сохранить общие данные и функции, которые не шаблонный код, вероятно, будут вызывать во многих случаях в базовом классе:

class VehicleBase {
protected:
    std::string model;
    std::string manufacturer;
    // ...

public:
    ~VehicleBase() { }
    virtual bool checkHealth() = 0;
};


template<typename Accelerator, typename Breaks>
class Car : public VehicleBase {
    Accelerator accelerator;
    Breaks breaks;
    // ...

    virtual bool checkHealth() { ... }
};

Между прочим, это также подход, который используют потоки C ++: std::ios_base содержит флаги и прочее, которые не зависят от типа char или характеристик, таких как openmode, флаги форматирования и прочее, тогда как std::basic_ios является шаблоном класса наследует это. Это также уменьшает раздувание кода за счет совместного использования кода, который является общим для всех экземпляров шаблона класса.

Частное наследство?

В целом следует избегать частного наследства. Это очень редко полезно, и сдерживание - лучшая идея в большинстве случаев. Распространенный случай, когда верно обратное, когда размер действительно важен (например, строковый класс на основе политики): Оптимизация пустого базового класса может применяться при выводе из пустого класса политики (просто содержащего функции).

Чтение Использование и злоупотребление Наследством Хербом Саттером.

32 голосов
/ 02 марта 2009

Эмпирическое правило:

1) Если выбор конкретного типа сделан во время компиляции, предпочтите шаблон. Это будет безопаснее (ошибки времени компиляции или ошибки времени выполнения) и, вероятно, лучше оптимизировано. 2) Если выбор сделан во время выполнения (то есть в результате действий пользователя), выбора действительно нет - используйте наследование и виртуальные функции.

6 голосов
/ 02 марта 2009

Другие опции:

  1. Используйте Шаблон посетителя (пусть внешний код работает в вашем классе).
  2. Выведите какую-то часть вашего класса, например, через итераторы, чтобы общий код на основе итераторов мог с ними работать. Это лучше всего работает, если ваш объект является контейнером других объектов.
  3. См. Также шаблон стратегии (есть примеры c ++ внутри)
5 голосов
/ 02 марта 2009

Шаблоны - это способ позволить классу использовать переменную, тип которой вам не безразличен. Наследование - это способ определения класса на основе его атрибутов. Вопрос "is-a" против "has-a" .

4 голосов
/ 02 марта 2009

На большинство ваших вопросов уже дан ответ, но я хотел бы уточнить этот бит:

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

Это часть этого. Но еще одним фактором является дополнительная безопасность типов. Когда вы обрабатываете BrakeWithABS как Brake, вы теряете информацию о типе. Вы больше не знаете, что объект на самом деле BrakeWithABS. Если это параметр шаблона, у вас есть точный доступный тип, который в некоторых случаях может позволить компилятору выполнить лучшую проверку типов. Или это может быть полезно, чтобы гарантировать, что корректная перегрузка функции вызывается. (если stopCar() передает объект Brake во вторую функцию, которая может иметь отдельную перегрузку для BrakeWithABS, она не будет вызываться, если вы использовали наследование, и ваш BrakeWithABS был приведен к Brake.

Еще одним фактором является то, что он обеспечивает большую гибкость. Почему все реализации тормозов должны наследоваться от одного базового класса? Есть ли у базового класса что-нибудь, что можно взять с собой? Если я напишу класс, который предоставляет ожидаемые функции-члены, разве это не достаточно хорошо, чтобы действовать как тормоз? Часто явное использование интерфейсов или абстрактных базовых классов ограничивает ваш код более, чем необходимо.

(Обратите внимание, я не говорю, что шаблоны всегда должны быть предпочтительным решением. Существуют и другие проблемы, которые могут повлиять на это, от скорости компиляции до «того, что программисты в моей команде знакомы» или просто «что я предпочитаю» И иногда вам нужен полиморфизм времени выполнения, и в этом случае шаблонное решение просто невозможно)

3 голосов
/ 02 марта 2009

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

Однако , использование шаблонов не мешает вам выполнять оба действия (что делает версию шаблона более гибкой):

struct Brake {
    virtual void stopCar() = 0;
};

struct BrakeChooser {
    BrakeChooser(Brake *brake) : brake(brake) {}
    void stopCar() { brake->stopCar(); }

    Brake *brake;
};

template<class Brake>
struct Car
{
    Car(Brake brake = Brake()) : brake(brake) {}
    void slamTheBrakePedal() { brake.stopCar(); }

    Brake brake;
};


// instantiation
Car<BrakeChooser> car(BrakeChooser(new AntiLockBrakes()));

При этом, я бы, вероятно, НЕ использовал бы шаблоны для этого ... Но на самом деле это просто личный вкус.

2 голосов
/ 02 марта 2009

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

0 голосов
/ 10 апреля 2009

Лично я всегда предпочел бы использовать интерфейсы поверх шаблонов по нескольким причинам:

  1. Шаблоны Иногда ошибки компиляции и компоновки загадочны
  2. Трудно отлаживать код, основанный на шаблонах (по крайней мере, в Visual Studio IDE)
  3. Шаблоны могут увеличить ваши двоичные файлы.
  4. Шаблоны требуют, чтобы вы поместили весь его код в заголовочный файл, что затрудняет понимание класса шаблона.
  5. Шаблоны трудно поддерживать начинающим программистам.

Я использую шаблоны только тогда, когда виртуальные таблицы создают какие-то издержки.

Конечно, это только мое мнение.

0 голосов
/ 02 марта 2009

Используйте интерфейс, если вы предполагаете одновременную поддержку различных классов Break и их иерархии.

Car( new Brake() )
Car( new BrakeABC() )
Car( new CoolBrake() )

И вы не знаете эту информацию во время компиляции.

Если вы знаете, какой перерыв вы собираетесь использовать, 2b - правильный выбор для вас, чтобы указать различные классы автомобилей. Тормозом в этом случае станет ваша машина «Стратегия», и вы можете установить ее по умолчанию.

Я бы не использовал 2a. Вместо этого вы можете добавить статические методы в Break и вызывать их без экземпляра.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...