Подтвердить что ты не робот

Шаблон или абстрактный базовый класс?

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

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

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

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

Версия 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, см. Часто задаваемые вопросы по С++.

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

4b9b3361

Ответ 1

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

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

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

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() { ... }
};

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

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

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

Прочитайте Использование и злоупотребления наследования от Herb Sutter.

Ответ 2

Правило большого пальца:

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

Ответ 3

Другие параметры:

  • Используйте шаблон посетителя (пусть внешний код работает с вашим классом).
  • Извлеките часть своего класса, например, с помощью итераторов, чтобы на них мог работать общий код, основанный на итераторе. Это лучше всего работает, если ваш объект является контейнером других объектов.
  • См. также Шаблон стратегии (есть примеры С++ внутри)

Ответ 4

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

Ответ 5

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

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

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

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

(Заметьте, я не говорю, что шаблоны всегда должны быть предпочтительным решением. Есть и другие проблемы, которые могут повлиять на это: от скорости компиляции до "того, с кем программисты в моей команде знакомы" или просто "что я предпочитаю",. И иногда вам нужен полиморфизм времени выполнения, и в этом случае решение шаблона просто невозможно)

Ответ 6

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

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

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()));

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

Ответ 7

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

Ответ 8

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

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

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

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

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

Ответ 9

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

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

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

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