Ти не спадкуєш від std :: vector


189

Гаразд, це визнати насправді важко, але у мене зараз сильна спокуса успадкувати std::vector.

Мені потрібно близько 10 налаштованих алгоритмів для вектора, і я хочу, щоб вони були безпосередньо членами вектора. Але, природно, я хочу також мати інший std::vectorінтерфейс. Ну, моя перша ідея як законослухняного громадянина полягала в тому, щоб мати std::vectorчлена в MyVectorкласі. Але тоді мені доведеться вручну переробити весь інтерфейс std :: vector. Занадто багато набирати. Далі я подумав про приватне успадкування, так що замість того, щоб переосмислити методи, я напишу купу using std::vector::member's у публічний розділ. Це теж нудно насправді.

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

Чи моє рішення абсолютно невиправдане? Якщо так, то чому? Чи можете ви надати альтернативу, яка б містила додаткових членів насправді членів, але не передбачала б переформатування всього інтерфейсу вектора? Я сумніваюся в цьому, але якщо зможете, я просто буду щаслива.

Також, крім того, що якийсь ідіот може написати щось подібне

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

чи є якісь інші реалістичні загрози використання MyVector? Говорячи реалістично, я відкидаю такі речі, як уявіть собі функцію, яка бере покажчик на вектор ...

Ну, я заявив про свою справу. Я згрішив. Тепер саме вам пробачити мене чи ні :)


9
Отже, ви в основному запитуєте, чи нормально порушувати загальноприйняте правило, виходячи з того, що ви просто ліниві повторно реалізувати інтерфейс контейнера? Тоді ні, це не так. Дивіться, ви можете мати найкраще з обох світів, якщо проковтнете цю гірку таблетку і зробите це правильно. Не будь тим хлопцем. Напишіть надійний код.
Джим Бріссом

7
Чому ви не можете / не хочете додати потрібну функціональність за допомогою функцій, які не є членами? Для мене це було б найбезпечніше в цьому сценарії.
Симоне

11
Інтерфейс @Jim: std::vector'є досить величезним, і коли C ++ 1x з'явиться, він значно розшириться. Це багато чого набрати, а ще більше розширитись через кілька років. Я думаю, що це вагомий привід розглянути спадщину замість стримування - якщо дотримуватися передумови, що ці функції мають бути членами (в чому я сумніваюся). Правило не отримувати з контейнерів STL полягає в тому, що вони не є поліморфними. Якщо ви не використовуєте їх таким чином, це не застосовується.
sbi

9
Справжнє м'ясо питання - в одному реченні: "Я хочу, щоб вони були безпосередньо членами вектора". Нічого іншого в питанні насправді не має значення. Чому ви цього "бажаєте"? Яка проблема лише у наданні цієї функціональності як не членам?
jalf

8
@JoshC: "Ти будеш" завжди був більш поширеним, ніж "ти будеш", і це також версія, знайдена в Біблії короля Джеймса (на що люди взагалі натякають, коли пишуть "ти не будеш [...] "). Що на Землі призвело б до того, щоб назвати це "неправильною написанням"?
ruakh

Відповіді:


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 був розроблений таким чином, що алгоритми та контейнери є окремими .

Це призвело до концепції різних типів ітераторів: ітераторів const, ітераторів довільного доступу тощо.

Тому я рекомендую вам прийняти цю конвенцію і розробити алгоритми таким чином, щоб вони не переймалися тим, що є контейнером, над яким вони працюють - і їм буде потрібен лише певний тип ітератора, який їм знадобиться виконувати операції.

Також дозвольте перенаправити вас на кілька хороших зауважень Джеффа Аттвуда .


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();
};

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


IIUC, відсутність віртуального деструктора є лише проблемою, якщо похідний клас виділяє ресурси, які повинні бути звільнені після знищення. (Вони не будуть звільнені у випадку поліморфного використання, оскільки контекст, який несвідомо переймає право власності на похідний об'єкт через покажчик на базу, викликає деструктор бази лише тоді, коли настає час. слід врахувати, що базові дзвінки дійсні для виклику. Але відсутні додаткові ресурси, чи є інші причини?
Пітер - Відновіть Моніку

2
vectorВиділене сховище не є проблемою - зрештою, vectorдеструктор буде викликаний все в порядку через покажчик на vector. Це тільки те , що стандарт забороняє deleteІНГ вільний сховище об'єктів з допомогою виразу базового класу. Причина, безумовно, полягає в тому, що механізм (де) розподілу може спробувати визначити розмір фрагмента пам'яті, щоб звільнити deleteоперанд, наприклад, коли є кілька арен розподілу для об'єктів певного розміру. Це обмеження, по суті, не стосується звичайного знищення об'єктів зі статичною або автоматичною тривалістю зберігання.
Пітер - Відновіть Моніку

@DavisHerring Я думаю, що ми згідні там :-).
Пітер - Відновіть Моніку

@DavisHerring Ах, я бачу, ви посилаєтесь на мій перший коментар - в цьому коментарі був МКП, і він закінчився питанням; Пізніше я побачив, що дійсно це завжди заборонено. (Базилев виступив із загальним твердженням, "ефективно перешкоджає", і я задумався про конкретний спосіб, яким він перешкоджає.) Так, так, ми згодні: UB.
Пітер - Відновіть Моніку

@Basilevs Це, мабуть, було випадково. Виправлено.
ThomasMcLeod

36

Якщо ви розглядаєте це, ви, очевидно, вже вбили мовних педантів у вашому офісі. З ними не виходить, чому б просто не зробити

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

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


А викриття контейнерів та алгоритмів? Дивіться відповідь Коса вище.
bruno nery


19

Що ви сподіваєтеся досягти? Просто надання певної функціональності?

Ідіоматичний спосіб C ++ - це просто написати деякі безкоштовні функції, що реалізують функціонал. Швидше за все, вам дійсно не потрібен std :: vector, спеціально для функціоналу, який ви реалізуєте, а це означає, що ви насправді програєте на повторне використання, намагаючись успадкувати від std :: vector.

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


5
Я не переконаний. Не могли б ви оновити якийсь із запропонованих кодів, щоб пояснити, чому?
Карл Кнечтел

6
@Armen: окрім естетики, чи є вагомі причини?
snemarch

12
@Armen: краще естетика, і більше типовості, будуть забезпечувати вільний frontі backфункцію теж. :) (Також розглянемо приклад безкоштовного beginта endв C ++ 0x та boost.)
UncleBens

3
Я все одно не маю нічого поганого з безкоштовними функціями. Якщо вам не подобається "естетика" STL, можливо, C ++ є для вас неправильним, естетично. І додавання деяких функцій-членів це не виправить, оскільки багато інших алгоритмів все ще є безкоштовними функціями.
Френк Остерфельд

17
Важко кешувати результат важкої роботи у зовнішньому алгоритмі. Припустимо, ви повинні обчислити суму всіх елементів у векторі або розв’язати поліноміальне рівняння з векторними елементами як коефіцієнтами. Ці операції важкі і лінь була б їм корисною. Але ви не можете представити його без упаковки або спадкування з контейнера.
Василевс

14

Я думаю, що дуже мало правил слід сліпо слідувати 100% часу. Це здається, що ви досить багато думали, і переконані, що це шлях. Тож - якщо хтось не придумає конкретних причин не робити цього - я думаю, ви повинні продовжувати свій план.


9
Ваше перше речення відповідає 100% часу. :)
Стів Фоллз

5
На жаль, друге речення не є. Він не думав про це багато. Більшість питань не має значення. Єдина його частина, яка демонструє його мотивацію, - це "я хочу, щоб вони були безпосередньо членами вектора". Я хочу. Немає причини, чому це бажано. Що звучить так, що він зовсім не думав про це .
jalf

7

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

Причини другого варіанту можуть бути лише ідеологічними, тому що std::vectors не є поліморфними, і в іншому випадку немає різниці, ви піддаєте std::vectorпублічний інтерфейс через публічну спадщину або через публічне членство. (Припустимо, вам потрібно зберегти деякий стан у вашому об’єкті, щоб ви не могли піти з вільних функцій). З менш обгрунтованої ноти та з ідеологічної точки зору, видається, що std::vectors є своєрідною "простою ідеєю", тому будь-яка складність у вигляді предметів різних можливих класів на їх місці ідеологічно не приносить користі.


Чудова відповідь. Ласкаво просимо до SO!
Армен Цирунян

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;
    ...

3
"Вам потрібен віртуальний деструктор лише у тому випадку, якщо розміри базового класу та похідного класу відрізняються над / або у вас віртуальні функції (що означає v-таблиця)." Це твердження практично правильне, але теоретично
Армен Цирунян

2
Так, в принципі це все-таки невизначена поведінка.
jalf

Якщо ви стверджуєте, що це невизначена поведінка, я хотів би побачити доказ (цитата зі стандарту).
hmuelner

8
@hmuelner: На жаль, Армен і джельф на цьому правильні. З [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.


2

Так, це безпечно до тих пір, як ви обережно не робити те, що не є безпечним ... Я не думаю, що я ніколи не бачив, щоб хтось використовував вектор з новим, так що на практиці у вас все буде добре. Однак це не звичайна ідіома в c ++ ....

Чи можете ви дати більше інформації про алгоритми?

Іноді ви закінчуєтесь по одній дорозі з дизайном, а потім не можете побачити інші шляхи, які ви могли взяти - той факт, що ви стверджуєте, що потрібно, щоб вектори з 10 новими алгоритмами дзвонили для мене дзвіночками тривоги - чи справді є 10 загальних цілей алгоритми, які вектор може реалізувати, або ви намагаєтеся зробити об'єкт, який є одночасно вектором загального призначення І, який містить функції, що застосовуються для додатків?

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


2

Я також успадкував std::vectorнещодавно і вважав, що це дуже корисно, і до цих пір я не відчував з цим жодних проблем.

Мій клас - це розріджений матричний клас, що означає, що мені потрібно зберігати елементи матриці десь, а саме в an 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.

0

Це питання гарантовано створює задихане перлинне зчеплення, але насправді немає жодних причин для уникнення або "зайвого примноження об'єктів", щоб уникнути, виведення з стандартного контейнера. Найпростіший, найкоротший вираз - найясніший і найкращий.

Вам потрібно проявляти всю звичайну опіку навколо будь-якого похідного типу, але у випадку з базою зі стандарту немає нічого особливого. Перевизначення функції базового члена може бути складним, але це було б нерозумно з будь-якою невіртуальною базою, тому особливого тут немає. Якщо ви хочете додати учасника даних, вам слід потурбуватися про зрізання, якщо член повинен відповідати вмісту бази, але знову ж таки, що це те саме для будь-якої бази.

Місце, де я виявив, що виходить із стандартного контейнера, особливо корисне, - це додати єдиний конструктор, який робить саме необхідну ініціалізацію, без жодних шансів замішання чи викрадення з боку інших конструкторів. (Я дивлюся на вас, конструктори inicijalization_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_in , що технічно потрібно чекати C ++ 20, є те, що попередні Стандарти не вимагають збереження порожнього вектора в ході руху. Це визнано як нагляд, і його можна обґрунтовано очікувати, що воно буде виправлено як дефект у часі до 20 року. Ми також можемо очікувати, що виправлення буде фактично відкладено до попередніх Стандартів, оскільки всі існуючі реалізації насправді зберігають потужність в ході руху; Стандарти цього просто не вимагають. резервування пістолета майже завжди є лише оптимізацією.)

Дехто може стверджувати, що випадок reserve_inкраще обслуговується вільним шаблоном функцій:

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

Така альтернатива, безумовно, життєздатна - і навіть часом може бути нескінченно швидшою через * RVO. Але вибір деривації або вільної функції повинен бути зроблений з власних достоїнств, а не з необґрунтованих (хе!) Забобонів щодо походження від стандартних компонентів. У наведеному вище прикладі лише друга форма працювала б із вільною функцією; хоча поза контекстом класу це можна було б написати трохи більш стисло:

  auto given_names{reserve_in<std::vector<T>>(1000)}; // 2
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.