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

Gcc4.9.2 libstdС++ реализация std::vector наследуется от _Vector_base (не виртуальный destuctor). Почему это нормально?

Итак, я использовал контейнер, полученный из std::vector в течение некоторого времени. Возможно, это плохое дизайнерское решение по нескольким причинам, и вопрос о том, нужно ли вам делать такую ​​вещь, здесь широко обсуждается:

Вы не должны наследовать от std::vector

Подкласс/наследование стандартных контейнеров?

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

Можно ли наследовать реализацию из контейнеров STL, а не делегировать?

Я уверен, что я пропустил некоторые из обсуждений... но разумные аргументы для обеих точек обзора найдены в ссылках. Насколько я могу судить, "потому что ~ vector() не является виртуальным" является основой для "правила", которое вы не должны наследовать из stl-контейнеров. Однако, если я посмотрю на реализацию для std::vector в g++ 4.9.2, я обнаружил, что std::vector наследует от _Vector_base и _Vector_base не виртуальный деструктор.

template<typename _Tp, typename _Alloc = std::allocator<_Tp> >
class vector : protected _Vector_base<_Tp, _Alloc>
{
...
  ~vector() _GLIBCXX_NOEXCEPT
  { std::_Destroy(this->_M_impl._M_start, this->_M_impl._M_finish,
    _M_get_Tp_allocator()); }

...
}

где:

template<typename _Tp, typename _Alloc>
struct _Vector_base
{
...
  ~_Vector_base() _GLIBCXX_NOEXCEPT
  { _M_deallocate(this->_M_impl._M_start, this->_M_impl._M_end_of_storage
    - this->_M_impl._M_start); }

...
}

Таким образом, реализация gcc 4.9.2 std::vector наследуется от базовых классов с не виртуальным деструктором. Это заставляет меня думать, что это приемлемая практика. Почему это нормально? Каковы конкретные условия, при которых такая практика не опасна?

4b9b3361

Ответ 1

1: std::vector не наследуется от _Vector_base

Вернее, не в моей стандартной библиотечной реализации. libС++ реализует вектор так:

namespace std
{

template <class T, class Allocator = allocator<T> >
class vector
{
...

Конечно, ваша реализация может наследоваться от базового класса, но моя реализация не делает этого. Это приводит к первой ошибке вашей интерпретации "правила":

Стандартная библиотека С++ может быть реализована различными способами, и мы действительно не можем делать широкие, радикальные предположения или утверждения о стандартной библиотеке (той, которая определена ISO/IEC 14882:2014) из одна его реализация.

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

2: Хорошо, да, поэтому std::vector наследует от _Vector_base

Но прежде чем продолжить, допустим, что ваша реализация может наследовать от _Vector_base, и это хорошо. Итак, почему std::vector разрешено наследовать от базового класса, но вам не разрешено наследовать от std::vector?

3: вы можете наследовать от std::vector, если хотите (просто будьте осторожны)

std::vector может наследовать от _Vector_base по тем же причинам, которые вы можете наследовать от std::vector: разработчики библиотеки очень осторожны (и вы тоже должны быть).

std::vector не является delete d полиморфно с указателем _Vector_base. Поэтому нам не нужно беспокоиться о том, что ~vector() не является виртуальным. Если вы не delete ваш унаследованный класс с полиморфным указателем std::vector, то ~vector(), не являющийся виртуальным, не является проблемой. Это нормально. Не беспокойся. Пусть не суетится. Полиморфный delete означает, что нам не нужно беспокоиться о том, что деструкторы являются виртуальными или нет.

Но помимо этого, разработчики библиотеки гарантировали, что std::vector никогда не нарезать с помощью ссылки _Vector_base. То же самое для вас: если вы можете гарантировать, что ваш пользовательский векторный класс никогда не нарезается (кто-то использует ссылку std::vector), то вы тоже здесь. Никакая нарезка означает, что нам не нужно беспокоиться о том, что плохие копии сделаны.

4: Почему вы не должны наследовать от std::vector

Об этом было много сказано в других вопросах, но я скажу это здесь (и с учетом всей проблемы наследования _Vector_base): вы (вероятно) не должны наследовать от std::vector.

Проблема заключается в том, что если вы это сделаете, кто-то, использующий ваш пользовательский векторный класс, может полиморфно удалить его (у них может быть указатель std::vector, а delete он, который является Bad Thing, если они это сделают). Или они могут иметь ссылку std::vector на ваш пользовательский объект и попытаться сделать его копию (что бы срезать объект, что также, вероятно, было бы плохим вещью) (я продолжаю считать ссылку std::vector на ваш пользовательский объект необходимо, чтобы вызвать обрезку объекта при копировании, потому что я продолжаю считать, что вы достаточно осторожны, чтобы случайно не обрезать объект с небрежным назначением или вызов функции, вы бы никогда не были настолько небрежны, я уверен, но кто-то еще с ссылкой std::vector может быть (и да, я здесь немного увлекаюсь)).

Вы можете контролировать, что вы делаете с кодом. И если вы можете контролировать его (тщательно) достаточно, чтобы убедиться, что нет полиморфного delete или фрагментации объектов, вы в порядке.

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

Хуже того, если ваш код используется клиентом, вы действительно не можете контролировать, что они делают, и если они совершают одну из этих плохих вещей, вы тот, кто, вероятно, будет обвинен и поручен (воспользуйтесь рефакторингом всего вашего кода, который использовался для использования вашего пользовательского класса, наследующего от std::vector) (или просто сообщите клиенту, что они не могут этого сделать, но у вас, вероятно, будет сварливый клиент, который потратил время на отладку странная проблема, с которой они не ожидали столкнуться).

Ваши реализаторы стандартной библиотеки С++ могут уйти с этим наследованием, хотя они могут очень хорошо управлять вещами: никто не может использовать _Vector_base. Вы можете использовать std::vector. Только std::vector. Поскольку вам не разрешено когда-либо использовать _Vector_base, разработчикам стандартной библиотеки не нужно беспокоиться о разрезании объектов или полиморфных delete s. И поскольку они очень осторожны в своей реализации в контролируемой среде, все прекрасно работает.

Но даже лучше, не делая предположений о том, как std::vector реализовано, и вместо этого рассматривая его как (полезный) черный ящик, мы можем убедиться, что использование std::vector переносимо для других реализаций стандартной библиотеки. Если вы сделаете предположения о std::vector наследовании от некоторого базового класса, вы будете ограничивать переносимость вашего кода.

Ответ 2

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

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

Ответ 3

Просто точка данных.

В "A Tour of С++" Bjarne Stroustrup определяет шаблон класса, который происходит от std::vector<T>.. Цель состоит в том, чтобы иметь проверку диапазона при использовании оператора []. Все остальное делегировано базовому классу.

Ответ 4

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

  • Всякий раз, когда вы delete объект без виртуального деструктора, вызываемый деструктор не зависит от динамического типа. То есть, если объявленный тип указателя, который вы delete отличается от динамического типа объекта, вызывается неправильный деструктор.

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

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

class Foo {
    protected:
        Foo();
        ~Foo();
};

class Bar : public Foo {
    public:
        Bar();
        ~Bar();
};

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

Теперь вернемся к std::vector<>. Класс std::vector<> - это тот, который предназначен для использования, и он не имеет виртуального деструктора. Но это не требует, чтобы базовый класс не был std::vector<>. Библиотека бесплатна для реализации std::vector<>, разделив ее реализацию на два класса, следуя шаблону Foo и Bar выше. Единственное, чего следует избегать, это то, что пользователь использует указатель базового класса для уничтожения производного объекта. Конечно, тип _Vector_base является приватным для вашей стандартной реализации библиотек, и пользователи никогда не должны его использовать, поэтому это нормально.

Однако подклассификация std::vector<> представляет собой совершенно другую историю: деструктор std::vector<> является общедоступным, поэтому вы не можете остановить пользователей класса

template <class T> class MyVector : public std::vector<T> {
    ...
};

чтобы использовать указатель базового класса, который они могли бы получить, чтобы уничтожить MyVector<>. Вы можете использовать личное или защищенное наследование, чтобы избежать проблемы, но это не дает преимуществ публичного наследования. Итак, да, вы никогда не должны подкласса std::vector<>, хотя для std::vector<> вполне нормально наследовать от другого частного класса.

Ответ 5

Ответ состоит из двух частей:

  • Поскольку _Vector_base знал, что он будет наследоваться std::vector и был разработан как таковой

  • Есть исключение для каждого правила. Если наследование от std::vector имеет смысл в вашем случае, тогда просто делайте то, что имеет смысл.

Ответ 6

Нет такого правила "не наследуйте от базовых классов с не виртуальными деструкторами". Если есть правило, это будет: "если у вас есть хотя бы один виртуальный метод, сделайте свой деструктор виртуальным, что также означает, что он не наследуется от базовых классов с не виртуальными деструкторами". IMHO нормально наследовать stl-контейнеры, если вы выполните это.

Кажется, что некоторые архитекторы компилятора также согласны с этим. В буквальном смысле это предупреждение, которое указывает на это: для ссылки: Что означает "имеет виртуальный метод... но не виртуальный деструктор" означает во время компиляции С++?