Есть ли какой-либо реальный риск для получения из контейнеров 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 > Должно быть предпочтительным сдерживание?
Ответы
Ответ 1
Стандартные контейнеры не имеют виртуальных деструкторов, поэтому вы не можете обрабатывать их полиморфно. Если вы этого не сделаете, и все, кто использует ваш код, этого не делают, это не "неправильно", само по себе. Тем не менее, вам лучше использовать композицию в любом случае, для ясности.
Ответ 2
Потому что вам нужен виртуальный деструктор, а в std-контейнерах его нет. Контейнеры std не предназначены для использования в качестве базового класса.
Для получения дополнительной информации прочитайте статью "Почему мы не должны наследовать класс из классов STL?"
Руководство
Базовый класс должен иметь:
- публичный виртуальный деструктор
- или защищенный не виртуальный деструктор
Ответ 3
Один сильный контраргумент, на мой взгляд, заключается в том, что вы вводите реализацию интерфейса и на свои типы. Что произойдет, когда вы узнаете, что стратегия распределения памяти в векторе не соответствует вашим потребностям? Вы получите от std:deque
? Как насчет тех 128K строк кода, которые уже используют ваш класс? Все ли нужно перекомпилировать все? Будет ли он даже компилироваться?
Ответ 4
Проблема не является философской, это проблема реализации. Деструкторы стандартных контейнеров не являются виртуальными, а это значит, что нет возможности использовать для них полиморфный режим работы, чтобы получить надлежащий дескриптор.
Я нашел на практике, что на самом деле не так уж больно создавать собственные собственные классы списков с помощью только тех методов, которые определены моим кодом (и частного члена "родительского" класса). Фактически, это часто приводит к более продуманным классам.
Ответ 5
Кроме того, в большинстве случаев вам следует предпочесть состав или агрегацию по наследованию, если это возможно.
Ответ 6
Помимо того, что базовому классу нужен виртуальный деструктор или защищенный не виртуальный деструктор, вы делаете следующее утверждение в своем дизайне:
Тарифы и сборы, если на то пошло, ЯВЛЯЮТСЯ ТОЛЬКО КАК вектор двойников в вашем примере выше. По вашему собственному утверждению "... со временем, тарифы и сборы развивают личностей...", то есть утверждение, что ставки ВСЕ ЕЩЕ ОСТАЮТСЯ КАК вектор двойников на данный момент? Например, вектор двойников не является синглоном, поэтому, если я использую ваши ставки для объявления моего вектора удвоений для виджетов, я могу понести некоторую головную боль из вашего кода. Что еще о тарифах и сборах могут быть изменены? Являются ли какие-либо изменения базового класса безопасно изолированными от клиентов вашего проекта, если они меняются фундаментальным образом?
Точка - это класс, который является элементом многих из С++, чтобы выразить намерения дизайна. Говорить о том, что вы имеете в виду и что означает, что вы говорите, является причиной против использования наследования таким образом.
... Или просто опубликовано более кратко до моего ответа: Замена.
Ответ 7
Одно слово: Substitutability
Ответ 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" к "почти-конечно-не-ок", когда добавляете элементы данных или другие базы с деструкторами, выполняющими очистку (освобождение памяти, разблокирование мьютексов, файл закрытие и т.д.)
-
Высказывание "вызовет деструктор базового класса" заставляет его звучать так, как будто это делается напрямую, без неявно заданного деструктора производного класса, связанного или выполняющего вызов, - все детали оптимизации и не указаны стандартом.