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

Есть ли какой-либо реальный риск для получения из контейнеров STL С++?

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

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

// 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 (Тарифы) и f (Сборы &) являются различными сигнатурами
  • Позволяет специализировать другие шаблоны, потому что X <Rates> и X < Сборы > являются различными типами.
  • Переслать декларацию тривиально
  • Отладчик, вероятно, говорит вам, является ли объект тарифами или тарифами
  • Если со временем тарифы и сборы будут развиваться личностями; синглтон для тарифов, формат вывода для тарифов — существует очевидная возможность для реализации этой функциональности.

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

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

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

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

Edit:

Без сомнения, добавление деструктора в класс "Тарифы" или "Сцены класса" было бы риском, потому что std::vector не объявляет его деструктор виртуальным. В примере нет деструктора, и нет необходимости в нем. Уничтожение объекта тарифов или сборов вызовет деструктор базового класса. Здесь нет необходимости в полиморфизме. Задача состоит в том, чтобы показать что-то плохое из-за использования деривации вместо 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;
}

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

В реализации Microsoft вектор <T> сам реализуется через наследование. вектор < Т, А > является публично полученным из _Vector_Val < T, A > Должно быть предпочтительным сдерживание?

4b9b3361

Ответ 1

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

Ответ 2

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

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

Руководство

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

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

Ответ 3

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

Ответ 4

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

Я нашел на практике, что на самом деле не так уж больно создавать собственные собственные классы списков с помощью только тех методов, которые определены моим кодом (и частного члена "родительского" класса). Фактически, это часто приводит к более продуманным классам.

Ответ 5

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

Ответ 6

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

Тарифы и сборы, если на то пошло, ЯВЛЯЮТСЯ ТОЛЬКО КАК вектор двойников в вашем примере выше. По вашему собственному утверждению "... со временем, тарифы и сборы развивают личностей...", то есть утверждение, что ставки ВСЕ ЕЩЕ ОСТАЮТСЯ КАК вектор двойников на данный момент? Например, вектор двойников не является синглоном, поэтому, если я использую ваши ставки для объявления моего вектора удвоений для виджетов, я могу понести некоторую головную боль из вашего кода. Что еще о тарифах и сборах могут быть изменены? Являются ли какие-либо изменения базового класса безопасно изолированными от клиентов вашего проекта, если они меняются фундаментальным образом?

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

... Или просто опубликовано более кратко до моего ответа: Замена.

Ответ 8

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

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

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

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

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

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

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

  • "предоставление тривиальных реализаций конструкторов" - это сложность, но один совет для С++ 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, по умолчанию значения, явные-vs-неявные конструкторы, а также масштаб до огромного количества аргументов. С++ 11 обеспечивает лучшее общее решение.

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

  • Нет риска, связанного с деструктором производного класса, если объект не удаляется полиморфно; если существует undefined поведение, независимо от того, имеет ли ваш производный класс определенный пользователем деструктор. Тем не менее, вы переходите от "вероятно-ok-for-a-cowboy" к "почти-конечно-не-ок", когда добавляете элементы данных или другие базы с деструкторами, выполняющими очистку (освобождение памяти, разблокирование мьютексов, файл закрытие и т.д.)

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