Есть ли реальный риск получения из контейнеров C ++ STL? - PullRequest
30 голосов
/ 28 мая 2009

Меня удивляет утверждение, что когда-либо использовать стандартный контейнер C ++ в качестве базового класса было ошибкой.

Если это не злоупотребление языком, чтобы объявить ...

// Example A
typedef std::vector<double> Rates;
typedef std::vector<double> Charges;

... тогда в чем конкретно заключается опасность декларирования ...

// Example B
class Rates : public std::vector<double> { 
    // ...
} ;
class Charges: public std::vector<double> { 
    // ...
} ;

Положительные преимущества для B включают:

  • Включает перегрузку функций, поскольку f (Rates &) и f (Charges &) являются разными сигнатурами
  • Позволяет специализировать другие шаблоны, поскольку X и X являются различными типами
  • Форвардное объявление тривиально
  • Отладчик, вероятно, сообщает вам, является ли объект тарифами или сборами
  • Если с течением времени ставки и тарифы развивают личность & mdash; синглтон для тарифов, выходной формат для сборов & mdash; есть очевидные возможности для реализации этой функциональности.

Положительные преимущества для A включают:

  • Не нужно предоставлять тривиальные реализации конструкторов и т. Д.
  • Пятнадцатилетний предстандартный компилятор, единственное, что скомпилирует ваше наследие, не задохнется
  • Поскольку специализации невозможны, шаблон X и шаблон X будут использовать один и тот же код, поэтому бессмысленное раздувание.

Оба подхода превосходят использование необработанного контейнера, потому что, если реализация меняется с вектора на вектор , есть только одно место, которое можно изменить с помощью B, и , возможно, , только одно место, чтобы изменить с A (это может быть больше, потому что кто-то мог поместить одинаковые операторы typedef в нескольких местах).

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

Edit:

Без сомнения, добавление деструктора к классу Rates или классу Charges было бы риском, потому что std :: vector не объявляет свой деструктор виртуальным. В примере нет деструктора, и он не нужен. Уничтожение объекта Rates or Charges вызовет деструктор базового класса. Здесь также нет необходимости в полиморфизме. Задача состоит в том, чтобы показать что-то плохое происходит в результате использования деривации вместо typedef.

Редактировать:

Рассмотрим этот вариант использования:

#include <vector>
#include <iostream>

void kill_it(std::vector<double> *victim) { 
    // user code, knows nothing of Rates or Charges

    // invokes non-virtual ~std::vector<double>(), then frees the 
    // memory allocated at address victim
    delete victim ; 

}

typedef std::vector<double> Rates;
class Charges: public std::vector<double> { };

int main(int, char **) {
  std::vector<double> *p1, *p2;
  p1 = new Rates;
  p2 = new Charges;
  // ???  
  kill_it(p2);
  kill_it(p1);
  return 0;
}

Есть ли какая-либо возможная ошибка, которую мог бы внести даже произвольно несчастный пользователь в ??? раздел, который приведет к проблеме с Charges (производный класс), но не с Rates (typedef)?

В реализации Microsoft вектор сам реализуется через наследование. vector является публично полученным из _Vector_Val Должно ли быть предпочтительным включение?

Ответы [ 8 ]

25 голосов
/ 28 мая 2009

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

16 голосов
/ 28 мая 2009

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

Для получения дополнительной информации прочитайте статью «Почему бы нам не наследовать класс от классов STL?»

Руководство

Базовый класс должен иметь:

  • публичный виртуальный деструктор
  • или защищенный не виртуальный деструктор
6 голосов
/ 28 мая 2009

Один сильный контраргумент, на мой взгляд, заключается в том, что вы навязываете интерфейс и реализацию своим типам. Что происходит, когда вы обнаружите, что стратегия выделения векторной памяти не соответствует вашим потребностям? Будете ли вы получать от std:deque? Как насчет тех 128К строк кода, которые уже используют ваш класс? Нужно ли будет все перекомпилировать? Будет ли он даже компилироваться?

5 голосов
/ 28 мая 2009

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

На практике я обнаружил, что на самом деле не так уж и сложно создавать свои собственные классы списков, используя только те методы, которые определены в моем коде (и закрытый член «родительского» класса). На самом деле, это часто приводит к более продуманным занятиям.

3 голосов
/ 29 мая 2009

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

Тарифы и сборы в этом отношении, ТАК ЖЕ, КАК вектор двойников в вашем примере выше. По вашему собственному утверждению «... с течением времени, тарифы и платежи развивают личность ...», тогда является ли утверждение, что ставки все еще одинаковы * вектор удваивается в этой точке? Например, вектор двойных чисел не является синглтоном, поэтому, если я использую ваши тарифы для объявления своего вектора двойных значений для виджетов, у меня может возникнуть головная боль из вашего кода. Что еще о тарифах могут быть изменены? Надежно ли изолированы какие-либо изменения базового класса от клиентов вашего проекта, если они изменятся фундаментальным образом?

Дело в том, что класс - это элемент из многих в C ++, выражающий намерения проекта. То, что вы имеете в виду, и то, что вы говорите, является причиной против использования наследования таким образом.

... Или просто кратко выложили перед моим ответом: Замена.

3 голосов
/ 28 мая 2009

Кроме того, в большинстве случаев вам следует отдавать предпочтение составу или агрегации, а не наследованию.

1 голос
/ 08 октября 2013

Есть ли какая-либо возможная ошибка, которую даже произвольно несчастный пользователь может внести в ??? раздел, который приведет к проблеме с Charges (производный класс), но не с Rates (typedef)?

Во-первых, есть замечательная точка зрения Манкарса:

Комментарий в kill_it неправильный. Если динамический тип жертвы не std::vector, то delete просто вызывает неопределенное поведение. Вызов kill_it(p2) приводит к тому, что это происходит, и поэтому в раздел //??? не нужно добавлять ничего, чтобы это имело неопределенное поведение. - Mankarse 3 сентября 2011 г. в 10: 53

Во-вторых, скажем, что они вызывают f(*p1);, где f специализируется для std::vector<double>: специализация vector не будет найдена - вы можете в конечном итоге соответствовать специализации шаблона по-другому - обычно работает (медленнее или меньше) эффективный) общий код или получение ошибки компоновщика, если на самом деле не определена неспециализированная версия. Не часто значительное беспокойство.

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

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

Тем не менее, несколько замечаний по вашей оценке:

  • «Предоставление тривиальных реализаций конструкторов» - это хлопотно, но один совет для C ++ 03: template <typename A> Classname(const A& a) : Base(a) { } template <typename A, typename B> Classname(const A& a, const B& b) : Base(a, b) { } ... иногда может быть проще, чем перечисление всех перегрузок, но не обрабатывает не const параметры, по умолчанию значения, явно-против-не-явные конструкторы, не масштабируются до огромного количества аргументов. C ++ 11 обеспечивает лучшее общее решение.

Без сомнения, добавление деструктора к class Rates или class Charges было бы опасно, поскольку std::vector не объявляет его деструктор виртуальным. В примере нет деструктора, и он не нужен. Уничтожение объекта Rates or Charges вызовет деструктор базового класса. Здесь также нет необходимости в полиморфизме.

  • Деструктор производного класса не представляет опасности , если объект не удален полиморфно; если это там неопределенное поведение, есть ли у вашего производного класса определенный пользователем деструктор. Тем не менее, вы переходите от «вероятно, хорошо для ковбоя» к «почти наверняка не в порядке», когда вы добавляете элементы данных или дополнительные базы с деструкторами, которые выполняют очистку (освобождение памяти, разблокировка мьютекса, файл закрытие ручки и т. д.)

  • Говоря «вызовет деструктор базового класса», звучит так, как будто это сделано напрямую, без какого-либо неявно определенного деструктора производного класса, участвующего или вызывающего - все это детали оптимизации и не определены Стандартом.

1 голос
/ 28 мая 2009

Одно слово: Заменимость

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