Ты не будешь наследовать от std :: vector

189

Хорошо, это действительно трудно признаться, но у меня сейчас есть сильное искушение унаследовать std::vector.

Мне нужно около 10 индивидуальных алгоритмов для вектора, и я хочу, чтобы они были непосредственно членами вектора. Но, естественно, я хочу иметь и остальную часть std::vectorинтерфейса. Ну, моя первая идея, как законопослушного гражданина, была иметь std::vectorчлена в MyVectorклассе. Но тогда мне пришлось бы вручную заново предоставить весь интерфейс std :: vector. Слишком много, чтобы напечатать. Затем я подумал о частном наследовании, чтобы вместо повторного предоставления методов я написал несколько записей using std::vector::memberв открытом разделе. Это слишком утомительно на самом деле.

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

Мое решение абсолютно неоправданно? Если так, то почему? Можете ли вы предложить альтернативу , которая будет иметь дополнительные член собственно член , но не будет включать перепечатывать все интерфейс вектора? Я сомневаюсь в этом, но если вы можете, я просто буду счастлив.

Кроме того, кроме того, что какой-то идиот может написать что-то вроде

std::vector<int>* p  = new MyVector

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

Ну, я изложил свой случай. Я согрешил. Теперь ты должен простить меня или нет :)

Армен Цирунян
источник
9
Итак, вы в основном спрашиваете, можно ли нарушать общее правило, основанное на том факте, что вам просто лень повторно реализовывать интерфейс контейнера? Тогда нет, это не так. Видите, вы можете получить лучшее из обоих миров, если проглотите эту горькую таблетку и сделаете это правильно. Не будь этим парнем. Напишите надежный код.
Джим Бриссом
7
Почему вы не можете / не хотите добавлять нужные вам функции с помощью функций, не являющихся членами? Для меня это было бы самое безопасное в этом сценарии.
Симона
11
Интерфейс @Jim: std::vectorдовольно большой, и когда появится C ++ 1x, он значительно расширится. Это много, чтобы напечатать и еще больше, чтобы расшириться через несколько лет. Я думаю, что это хорошая причина для рассмотрения наследования, а не сдерживания - если следовать предпосылке, что эти функции должны быть членами (в чем я сомневаюсь). Правило не выводить из контейнеров STL - они не полиморфны. Если вы не используете их таким образом, это не относится.
sbi
9
Настоящая суть вопроса в одном предложении: «Я хочу, чтобы они были непосредственно членами вектора». Ничто другое в этом вопросе не имеет значения. А зачем тебе это? В чем проблема с предоставлением этой функциональности в качестве нечленов?
Джалф
8
@JoshC: «Ты будешь» всегда был более распространенным, чем «ты должен», и это также версия, найденная в Библии короля Иакова (на которую обычно ссылаются люди, когда пишут «ты не будешь [...]» «). Что, черт возьми, заставит вас назвать это «орфографической ошибкой»?
Руах

Ответы:

155

На самом деле, нет ничего плохого в публичном наследовании std::vector. Если вам это нужно, просто сделайте это.

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

Проблема в том, что MyVectorэто новая сущность. Это означает, что новый разработчик C ++ должен знать, что это, черт возьми, перед его использованием. Какая разница между std::vectorа MyVector? Какой из них лучше использовать здесь и там? Что делать , если мне нужно , чтобы перейти std::vectorк MyVector? Могу я просто использовать swap()или нет?

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

Стас
источник
7
Мой единственный контраргумент на это состоит в том, что нужно действительно знать, что он делает, чтобы сделать это. Например, не вводите дополнительные элементы данных в, MyVectorа затем попробуйте передать их функциям, которые принимают std::vector&или std::vector*. Если существует какое-либо назначение копирования, использующее std :: vector * или std :: vector &, у нас есть проблемы с нарезкой, когда новые члены-данные MyVectorне будут скопированы. То же самое можно сказать и о вызове swap через базовый указатель / ссылку. Я склонен думать, что любая иерархия наследования, которая рискует разрезать объекты, является плохой.
stinky472
13
std::vectorдеструктора нет virtual, поэтому никогда не наследуй его
Андре Фрателли
2
По этой причине я создал класс, который публично унаследовал std :: vector: у меня был старый код с векторным классом, отличным от STL, и я хотел перейти на STL. Я переопределил старый класс как производный класс std :: vector, что позволило мне продолжать использовать старые имена функций (например, Count (), а не size ()) в старом коде, при написании нового кода с использованием std :: vector функции. Я не добавил никаких элементов данных, поэтому деструктор std :: vector работал нормально для объектов, созданных в куче.
Грэм Ашер
3
@GrahamAsher Если вы удаляете объект через указатель на базу, а деструктор не является виртуальным, ваша программа демонстрирует неопределенное поведение. Одним из возможных результатов неопределенного поведения является "это работало нормально в моих тестах". Другая причина в том, что она отправляет по электронной почте вашей бабушке историю посещенных страниц. Оба соответствуют стандарту C ++. Это изменение от одного к другому с точечными выпусками компиляторов, ОС или фазы луны также совместимо.
Якк - Адам Невраумонт
2
@GrahamAsher Нет, всякий раз, когда вы удаляете какой-либо объект через указатель на базу без виртуального деструктора, это стандартное поведение по умолчанию. Я понимаю, что вы думаете, что происходит; ты просто ошибаешься «Деструктор базового класса вызывается, и он работает» - это один из возможных (и наиболее распространенных) признаков этого неопределенного поведения, потому что это наивный машинный код, который обычно генерирует компилятор. Это не делает ни безопасным, ни отличной идеей.
Якк - Адам Невраумонт
92

Весь STL был разработан таким образом, что алгоритмы и контейнеры являются отдельными .

Это привело к концепции различных типов итераторов: константных итераторов, итераторов с произвольным доступом и т. Д.

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

Кроме того, позвольте мне перенаправить вас на несколько хороших замечаний Джеффа Этвуда .

Кос
источник
63

Основная причина, по которой вы не наследуете std::vectorпублично, - это отсутствие виртуального деструктора, который эффективно предотвращает полиморфное использование потомков. В частности, вы не допускаются к deleteчерез std::vector<T>*которые на самом деле точек на производный объект (даже если производный класс не добавляет пользователей), но компилятор обычно не может предупредить вас об этом.

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

class AdVector: private std::vector<double>
{
    typedef double T;
    typedef std::vector<double> vector;
public:
    using vector::push_back;
    using vector::operator[];
    using vector::begin;
    using vector::end;
    AdVector operator*(const AdVector & ) const;
    AdVector operator+(const AdVector & ) const;
    AdVector();
    virtual ~AdVector();
};

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

Basilevs
источник
IIUC, отсутствие виртуального деструктора является проблемой только в том случае, если производный класс выделяет ресурсы, которые должны быть освобождены при уничтожении. (Они не будут освобождены в полиморфном случае использования, потому что контекст, неосознанно берущий на себя владение производным объектом через указатель на базу, вызовет деструктор базы только тогда, когда это время.) Подобные проблемы возникают из-за других переопределенных функций-членов, поэтому следует соблюдать осторожность. принять, что базовые действительны для вызова. Но отсутствуют дополнительные ресурсы, есть ли другие причины?
Питер - Восстановить Монику
2
vectorВыделенное хранилище не является проблемой - в конце концов, vectorдеструктор будет вызываться через указатель на vector. Это только то , что стандарт запрещает deleteИНГ свободное хранилище объектов с помощью выражения базового класса. Несомненно, причина в том, что механизм (de) выделения может попытаться вывести размер фрагмента памяти, чтобы освободить его от deleteоперанда, например, когда есть несколько областей выделения для объектов определенных размеров. Это ограничение, на самом деле, не распространяется на обычное уничтожение объектов со статическим или автоматическим сроком хранения.
Питер - Восстановить Монику
@DavisHerring Я думаю, что мы согласны там :-).
Питер - восстановить Монику
@DavisHerring Ах, я вижу, вы ссылаетесь на мой первый комментарий - в этом комментарии был IIUC, и он закончился вопросом; Позже я увидел, что это действительно всегда запрещено. (Базилевс выступил с общим заявлением «эффективно предотвращает», и я задумался о том, каким образом это предотвращает.) Так что да, мы согласны: UB.
Питер - Восстановить Монику
@Basilevs Это должно быть непреднамеренно. Исправлена.
ThomasMcLeod
36

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

struct MyVector
{
   std::vector<Thingy> v;  // public!
   void func1( ... ) ; // and so on
}

Это позволит обойти все возможные грубые ошибки, которые могут возникнуть в результате случайного изменения класса MyVector, и вы все равно сможете получить доступ ко всем векторным операциям, просто добавив немного .v.

Crashworks
источник
И разоблачать контейнеры и алгоритмы? Смотрите ответ Коса выше.
Бруно Нери
19

Чего ты надеешься достичь? Просто предоставляя некоторую функциональность?

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

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

Карл Кнехтель
источник
5
Я не убежден. Не могли бы вы обновить некоторые из предложенного кода, чтобы объяснить, почему?
Карл Кнехтель
6
@Armen: кроме эстетики, есть ли веские причины?
Снегурх
12
@Armen: лучше эстетика, и больше типичности, будут обеспечивать свободный frontи backфункцию тоже. :) (Также рассмотрим пример бесплатно beginи endв C ++ 0x и boost.)
UncleBens
3
Я до сих пор не понимаю, что не так с бесплатными функциями. Если вам не нравится «эстетика» STL, возможно, C ++ не подходит вам эстетически. И добавление некоторых функций-членов не исправит это, поскольку многие другие алгоритмы все еще являются свободными функциями.
Франк Остерфельд
17
Трудно кэшировать результат тяжелой работы во внешнем алгоритме. Предположим, вам нужно вычислить сумму всех элементов в векторе или решить полиномиальное уравнение с векторными элементами в качестве коэффициентов. Эти операции тяжелы, и лень была бы полезна для них. Но вы не можете представить его без упаковки или наследования от контейнера.
Basilevs
14

Я думаю, что очень немногие правила должны соблюдаться вслепую в 100% случаев. Похоже, вы много об этом думали и убеждены, что это правильный путь. Так что - если кто-то не придумает веские конкретные причины не делать этого - я думаю, что вы должны продолжить свой план.

NPE
источник
9
Ваше первое предложение верно в 100% случаев. :)
Стив Фэллоуз
5
К сожалению, второе предложение не так. Он не задумывался об этом. Большая часть вопроса не имеет значения. Единственная его часть, которая показывает его мотивацию - «Я хочу, чтобы они были непосредственно членами вектора». Я хочу. Нет причин, почему это желательно. Похоже, он дал это не думал об этом вообще .
Джалф
7

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

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

Евгений
источник
Отличный ответ. Добро пожаловать на ТАК!
Армен Цирунян
4

С практической точки зрения: если у вас нет членов данных в производном классе, у вас нет проблем, даже в полиморфном использовании. Вам нужен только виртуальный деструктор, если размеры базового класса и производного класса различны и / или у вас есть виртуальные функции (что означает v-таблицу).

НО в теории: из [expr.delete] в C ++ 0x FCD: в первом варианте (удаление объекта), если статический тип удаляемого объекта отличается от его динамического типа, статический тип должен быть базовый класс динамического типа объекта, который должен быть удален, и статический тип должен иметь виртуальный деструктор, или поведение не определено.

Но вы можете получить конфиденциально из std :: vector без проблем. Я использовал следующий шаблон:

class PointVector : private std::vector<PointType>
{
    typedef std::vector<PointType> Vector;
    ...
    using Vector::at;
    using Vector::clear;
    using Vector::iterator;
    using Vector::const_iterator;
    using Vector::begin;
    using Vector::end;
    using Vector::cbegin;
    using Vector::cend;
    using Vector::crbegin;
    using Vector::crend;
    using Vector::empty;
    using Vector::size;
    using Vector::reserve;
    using Vector::operator[];
    using Vector::assign;
    using Vector::insert;
    using Vector::erase;
    using Vector::front;
    using Vector::back;
    using Vector::push_back;
    using Vector::pop_back;
    using Vector::resize;
    ...
hmuelner
источник
3
«Вам нужен только виртуальный деструктор, если размеры базового класса и производного класса различны и / или у вас есть виртуальные функции (что означает v-таблицу)». Это утверждение практически правильно, но не теоретически
Армен Цирунян
2
да, в принципе это все еще неопределенное поведение.
Джалф
Если вы утверждаете, что это неопределенное поведение, я хотел бы увидеть доказательство (цитата из стандарта).
hmuelner
8
@hmuelner: К сожалению, Армен и Джальф правы в этом. From [expr.delete]в C ++ 0x FCD: <quote> В первом варианте (удалить объект), если статический тип удаляемого объекта отличается от его динамического типа, статический тип должен быть базовым классом динамического типа. объекта, который должен быть удален, и статический тип должен иметь виртуальный деструктор или поведение не определено. </ quote>
Бен Фойгт
1
Что забавно, потому что я действительно думал, что поведение зависит от присутствия нетривиального деструктора (в частности, что классы POD могут быть уничтожены с помощью указателя на базу).
Бен Фойгт
3

Если вы следуете хорошему стилю C ++, отсутствие виртуальной функции - не проблема, а нарезка (см. Https://stackoverflow.com/a/14461532/877329 )

Почему отсутствие виртуальных функций не проблема? Потому что функция не должна пытаться получить deleteкакой-либо указатель, который она получает, поскольку она не имеет владельца. Следовательно, если следовать строгой политике владения, виртуальные деструкторы не нужны. Например, это всегда неправильно (с виртуальным деструктором или без него):

void foo(SomeType* obj)
    {
    if(obj!=nullptr) //The function prototype only makes sense if parameter is optional
        {
        obj->doStuff();
        }
    delete obj;
    }

class SpecialSomeType:public SomeType
    {
    // whatever 
    };

int main()
    {
    SpecialSomeType obj;
    doStuff(&obj); //Will crash here. But caller does not know that
//  ...
    }

Напротив, это всегда будет работать (с виртуальным деструктором или без него):

void foo(SomeType* obj)
    {
    if(obj!=nullptr) //The function prototype only makes sense if parameter is optional
        {
        obj->doStuff();
        }
    }

class SpecialSomeType:public SomeType
    {
    // whatever 
    };

int main()
    {
    SpecialSomeType obj;
    doStuff(&obj);
//  The correct destructor *will* be called here.
    }

Если объект создается фабрикой, фабрика также должна возвращать указатель на работающее средство удаления, которое следует использовать вместо delete, поскольку фабрика может использовать свою собственную кучу. Звонящий может получить его в виде share_ptrили unique_ptr. Короче говоря, не делает deleteничего , вы не получите непосредственно от new.

user877329
источник
2

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

Вы можете дать больше информации о алгоритмах?

Иногда вы заканчиваете тем, что идете по одной дороге с дизайном, а затем не видите других путей, которые вы, возможно, выбрали - тот факт, что вы утверждаете, что вам нужно вектор с 10 новыми алгоритмами, звонит мне в тревогу - действительно ли существует 10 общих целей? алгоритмы, которые вектор может реализовать, или вы пытаетесь создать объект, который является одновременно вектором общего назначения И который содержит специфические для приложения функции?

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

jcoder
источник
2

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

Мой класс - это класс разреженных матриц, что означает, что мне нужно где-то хранить свои матричные элементы, а именно в std::vector. Моя причина наследования заключалась в том, что мне было немного лень писать интерфейсы для всех методов, а также я связываю класс с Python через SWIG, где уже есть хороший интерфейсный код дляstd::vector . Я обнаружил, что гораздо проще расширить этот код интерфейса для своего класса, чем писать новый с нуля.

Единственная проблема , которую я могу видеть , с подходом не столько с невиртуальномом деструктором, а некоторые другие методы, которые я хотел бы перегрузки, такие как push_back(), resize(), и insert()т.д. Частное наследование действительно может быть хорошим вариантом.

Спасибо!

Джоэл Андерссон
источник
10
По моему опыту, наихудший долгосрочный ущерб часто наносят люди, которые пробуют что-то дурное, и « до сих пор не испытывали (читай заметили ) никаких проблем с этим».
Разочарован
0

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

  1. Позвольте мне добавить еще один способ упаковки std::vectorбез написания большого количества функциональных оболочек.

#include <utility> // For std:: forward
struct Derived: protected std::vector<T> {
    // Anything...
    using underlying_t = std::vector<T>;

    auto* get_underlying() noexcept
    {
        return static_cast<underlying_t*>(this);
    }
    auto* get_underlying() const noexcept
    {
        return static_cast<underlying_t*>(this);
    }

    template <class Ret, class ...Args>
    auto apply_to_underlying_class(Ret (*underlying_t::member_f)(Args...), Args &&...args)
    {
        return (get_underlying()->*member_f)(std::forward<Args>(args)...);
    }
};
  1. Унаследовать от std :: span вместо std::vectorи избежать проблемы dtor.
JiaHao Xu
источник
0

Этот вопрос гарантированно вызывает зловонное сцепление с жемчугом, но на самом деле нет веской причины избегать или «излишне умножать сущности», чтобы избежать вывода из стандартного контейнера. Самое простое, самое короткое из возможных выражений - самое ясное и лучшее.

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

Место, где я нашел вывод из стандартного контейнера, особенно полезно, это добавить единственный конструктор, который точно выполняет необходимую инициализацию, без шансов путаницы или взлома другими конструкторами. (Я смотрю на вас, конструкторы initialization_list!) Затем вы можете свободно использовать результирующий объект, разрезать его - передать его по ссылке на что-то, ожидающее базу, перейти от него к экземпляру базы, что у вас есть. Не нужно беспокоиться о крайних случаях, если только это не мешает вам связать аргумент шаблона с производным классом.

Место, где эта техника будет сразу полезна в C ++ 20, - это резервирование. Где мы могли бы написать

  std::vector<T> names; names.reserve(1000);

мы можем сказать

  template<typename C> 
  struct reserve_in : C { 
    reserve_in(std::size_t n) { this->reserve(n); }
  };

а затем, даже как ученики,

  . . .
  reserve_in<std::vector<T>> taken_names{1000};  // 1
  std::vector<T> given_names{reserve_in<std::vector<T>>{1000}}; // 2
  . . .

(в соответствии с предпочтениями) и не нужно писать конструктор просто для того, чтобы вызывать их в Reserve ().

( reserve_inТехнически, потому что техническая необходимость ждать C ++ 20 заключается в том, что предыдущие стандарты не требуют сохранения емкости пустого вектора при перемещениях. Это признается как недосмотр, и можно ожидать, что оно будет исправлено. как дефект во времени для 20-го. Мы также можем ожидать, что исправление будет эффективно задним числом к ​​предыдущим Стандартам, потому что все существующие реализации действительно сохраняют емкость при перемещениях; Стандарты просто не требовали этого. в любом случае резервирование оружия - это почти всегда просто оптимизация.)

Некоторые утверждают, что случай reserve_inлучше обслуживается бесплатным шаблоном функции:

  template<typename C> 
  auto reserve_in(std::size_t n) { C c; c.reserve(n); return c; }

Такая альтернатива, безусловно, жизнеспособна - и даже иногда может быть бесконечно быстрее из-за * RVO. Но выбор деривации или свободной функции должен быть сделан сам по себе, а не из безосновательного (хе!) Суеверия о производных от стандартных компонентов. В приведенном выше примере только вторая форма будет работать с функцией free; хотя вне контекста класса это можно было бы написать немного более кратко:

  auto given_names{reserve_in<std::vector<T>>(1000)}; // 2
Натан Майерс
источник