Чи безпечно push_back елемент з того самого вектора?


126
vector<int> v;
v.push_back(1);
v.push_back(v[0]);

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

vector<int> v;
v.push_back(1);
v.reserve(v.size() + 1);
v.push_back(v[0]);

Це робить його безпечним?


4
Примітка. Зараз на форумі стандартних пропозицій триває дискусія. У рамках цього хтось наводив приклад реалізаціїpush_back . Інший плакат відзначив помилку в ньому , що він неправильно обробляє описаний вами випадок. Ніхто інший, наскільки я можу сказати, не стверджував, що це не помилка. Не кажучи, що це переконливий доказ, просто спостереження.
Бенджамін Ліндлі

9
Вибачте, але я не знаю, яку відповідь прийняти, оскільки досі існує суперечка щодо правильної відповіді.
Ніл Кірк

4
Мене попросили прокоментувати це запитання 5-м коментарем за адресою : stackoverflow.com/a/18647445/576911 . Я роблю це, оскаржуючи кожну відповідь, яка в даний час говорить: так, це безпечно push_back елемент з того самого вектора.
Говард Хінант

2
@BenVoigt: <shrug> Якщо ви не згодні з тим, що говорить стандарт, або навіть якщо ви згодні зі стандартом, але не думаєте, що він говорить це досить чітко, це завжди варіант для вас: cplusplus.github.io/LWG/ lwg-active.html # submit_issue Я сама брала цей варіант більше разів, ніж пам'ятаю. Іноді успішно, іноді ні. Якщо ви хочете обговорити те, що говорить стандарт, або що він повинен говорити, ТАК не є ефективним форумом. Наша розмова не має нормативного значення. Але ви можете мати шанс на нормативний вплив, перейшовши за посиланням вище.
Говард Хінант

2
@ Polaris878 Якщо push_back змушує вектор досягти своєї ємності, вектор виділить новий більший буфер, скопіює старі дані та видалить старий буфер. Тоді він вставить новий елемент. Проблема полягає в тому, що новий елемент - це посилання на дані в старому буфері, який щойно видалили. Якщо видалення не зробить копію значення перед видаленням, це буде поганою посиланням.
Ніл Кірк

Відповіді:


31

Схоже, http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-closed.html#526 вирішив цю проблему (або щось подібне до неї) як потенційний дефект стандарту:

1) Параметри, взяті за допомогою посилання const, можуть бути змінені під час виконання функції

Приклади:

Дано std :: вектор v:

v.insert (v.begin (), v [2]);

v [2] можна змінювати, переміщуючи елементи вектора

Запропонована постанова полягала в тому, що це не дефект:

vector :: insert (iter, value) потрібно працювати, оскільки стандарт не дає дозволу, щоб він не працював.


Я знаходжу дозвіл у 17.6.4.9: "Якщо аргумент функції має недійсне значення (наприклад, значення поза доменом функції або покажчик, недійсний для його цільового використання), поведінка не визначена." Якщо відбувається перерозподіл, то всі ітератори та посилання на елементи недійсні, тобто посилання на параметр, переданий функції, також недійсне.
Ben Voigt

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

3
@NeilKirk Я думаю, що це має бути авторською відповіддю, про це також згадує Стефан Т. Лававей у Reddit, використовуючи по суті ті ж аргументи.
TemplateRex

v.insert(v.begin(), v[2]);не може викликати перерозподіл. То як це відповідає на питання?
ThomasMcLeod

@ThomasMcLeod: так, очевидно, це може викликати перерозподіл. Ви збільшуєте розмір вектора, вставляючи новий елемент.
Фіолетова жирафа

21

Так, це безпечно, і стандартні реалізації бібліотеки стрибають через обручі, щоб зробити це так.

Я вважаю, що виконавці так чи інакше відслідковують цю вимогу до 23.2 / 11, але я не можу зрозуміти, як і я не можу знайти щось конкретніше. Найкраще, що я можу знайти, - це стаття:

http://www.drdobbs.com/cpp/copying-container-elements-from-the-c-li/240155771

Перевірка реалізацій libc ++ та libstdc ++ показує, що вони також безпечні.


9
Деякі підкріплення справді допоможуть тут.
chris

3
Це цікаво, я мушу визнати, що я ніколи не розглядав цю справу, але насправді це здається досить важким. Чи це також справедливо vec.insert(vec.end(), vec.begin(), vec.end());?
Матьє М.

2
@MatthieuM. Ні: Таблиця 100 говорить: "pre: i і j не є ітераторами в a".
Себастьян Редл

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

3
Чи є 23.2 / 11 у версії, яку ви використовуєте "Якщо не вказано інше (явно або шляхом визначення функції з точки зору інших функцій), виклик функції члена контейнера або передача контейнера як аргумент функції бібліотеки не приводить до недійсності ітераторів до або зміни значень об'єктів у цьому контейнері. " ? Але vector.push_backНЕ вказувати інакше. "Викликає перерозподіл, якщо новий розмір перевищує старий ємність." та (at reserve) "Перерозподіл недійсних усіх посилань, покажчиків та ітераторів, що посилаються на елементи в послідовності."
Ben Voigt

13

Стандарт гарантує безпеку навіть вашого першого прикладу. Цитування C ++ 11

[послідовність.reqmts]

3 У Таблицях 100 та 101 ... Xпозначає клас контейнера послідовності, aпозначає значення, Xщо містить елементи типу T, ... tпозначає значення і значення rst constX::value_type

16 Таблиця 101 ...

a.push_back(t) Вид повернення Вираз void Операційна семантика Додає копію t. Вимоги: T повинні бути CopyInsertableвключені X. Контейнер basic_string , deque, list,vector

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


7
Я не бачу, наскільки це гарантує безпеку.
jrok

4
@Angew: Це абсолютно недійсне t, питання лише в тому, до чи після виготовлення копії. Ваше останнє речення, безумовно, неправильне.
Бен Войгт

4
@BenVoigt Оскільки tвідповідає переліченим умовам, описана поведінка гарантована. Реалізація забороняє визнати попередньою умову недійсною, а потім використовувати її як привід не поводитись як зазначено.
bames53

8
@BenVoigt Клієнт не зобов’язаний підтримувати передумову протягом усього виклику; лише для того, щоб забезпечити його дотримання при ініціюванні дзвінка.
bames53

6
@BenVoigt Це хороший момент, але я вважаю, що функтор, який перейшов for_each, зобов’язаний не скасувати ітераторів. Я не можу придумати посилання на for_each, але я бачу на деяких алгоритмах текст на зразок "op і binary_op не може визнати недійсними ітератори чи підпункти".
bames53

7

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

Але, принаймні, це здається безпечним для Visual Studio 2010. Її реалізація push_backробить спеціальне поводження зі справою, коли ви відштовхуєте елемент у вектор. Код структурований так:

void push_back(const _Ty& _Val)
    {   // insert element at end
    if (_Inside(_STD addressof(_Val)))
        {   // push back an element
                    ...
        }
    else
        {   // push back a non-element
                    ...
        }
    }

8
Мені хотілося б знати, якщо специфікація вимагає, щоб це було безпечним.
Наваз

1
Відповідно до Стандарту, це не потрібно, щоб бути безпечним. Однак можна реалізувати це безпечно.
Бен Войгт

2
@BenVoigt Я б сказав, що це потрібно, щоб бути безпечним (див. Мою відповідь).
Ендже вже не пишається SO

2
@BenVoigt Під час передачі посилання вона діє.
Ендже вже не пишається SO

4
@Angew: Це недостатньо. Потрібно пройти посилання, яке залишається дійсним протягом тривалості дзвінка, а ця - ні.
Бен Войгт

3

Це не є гарантією від стандарту, але як інша точка даних v.push_back(v[0])є безпечною для LLVM's libc ++ .

std::vector::push_backдзвінки libc ++,__push_back_slow_path коли їй потрібно перерозподілити пам'ять:

void __push_back_slow_path(_Up& __x) {
  allocator_type& __a = this->__alloc();
  __split_buffer<value_type, allocator_type&> __v(__recommend(size() + 1), 
                                                  size(), 
                                                  __a);
  // Note that we construct a copy of __x before deallocating
  // the existing storage or moving existing elements.
  __alloc_traits::construct(__a, 
                            _VSTD::__to_raw_pointer(__v.__end_), 
                            _VSTD::forward<_Up>(__x));
  __v.__end_++;
  // Moving existing elements happens here:
  __swap_out_circular_buffer(__v);
  // When __v goes out of scope, __x will be invalid.
}

Копія повинна бути зроблена не тільки до розстановки наявного сховища, але до переходу з існуючих елементів. Я припускаю, що переміщення існуючих елементів робиться в __swap_out_circular_buffer, і в цьому випадку ця реалізація справді безпечна.
Ben Voigt

@BenVoigt: хороший момент, і ви дійсно правильні, що переміщення відбувається всередині __swap_out_circular_buffer. (Я додав кілька коментарів, щоб відзначити це.)
Nate Kohl

1

Перша версія, безумовно, НЕ безпечна:

Операції над ітераторами, отримані викликом стандартного контейнера бібліотеки або функції елемента рядка, можуть отримати доступ до базового контейнера, але не повинні змінювати його. [Примітка. Зокрема, операції з контейнерами, які визнають недійсними ітератори, суперечать операціям ітераторів, пов'язаних із цим контейнером. - кінцева примітка]

з розділу 17.6.5.9


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


1
Слід розуміти, що це примітка, а не правило, тому вона пояснює наслідок попереднього правила ... і наслідки для ідентичних посилань однакові.
Бен Войгт

5
Результат v[0]не є ітератором, також push_back()не приймає ітератор. Отже, з точки зору юриста з мови, ваш аргумент недійсний. Вибачте. Я знаю, що більшість ітераторів є покажчиками, і сенс відключення ітератора є майже таким же, як і для посилань, але частина стандарту, яку ви цитуєте, не має значення для ситуації, що знаходиться в цій ситуації.
cmaster - відновити моніку

-1. Це абсолютно нерелевантна цитата, і вона ніяк не відповідає на неї. Комітет каже x.push_back(x[0]), що БЕЗКОШТОВНО.
Наваз

0

Це повністю безпечно.

У вашому другому прикладі ви

v.reserve(v.size() + 1);

яка не потрібна, тому що якщо вектор вийде зі свого розміру, це буде означати значення reserve.

Вектор відповідає за цей матеріал, а не ви.


-1

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

Розділ 23.2.1 Загальні вимоги до контейнерів

16
  • a.push_back (t) Додає копію t. Потрібно: T має бути CopyInsertable у X.
  • a.push_back (rv) Додає копію rv. Потрібно: T має бути переміщеним в X.

Тому реалізація push_back повинна забезпечити вставлення копії v[0] . На противажному прикладі, припускаючи, що реалізація буде перерозподілена перед копіюванням, вона не гарантовано додає копію v[0]та як таке порушує характеристики.


2
push_backОднак також буде змінено розмір вектора, і при наївній реалізації це призведе до недійсності посилання до того, як відбувається копіювання. Тож якщо ви не зможете підкріпити це цитатою зі стандарту, я вважаю це неправильним.
Конрад Рудольф

4
Під "цим" ви маєте на увазі перший чи другий приклад? push_backскопіює значення у вектор; але (наскільки я бачу), що може статися після перерозподілу, в цей момент посилання, з якого вона намагається скопіювати, вже не дійсна.
Майк Сеймур

1
push_backотримує свій аргумент шляхом посилання .
bames53

1
@OlivierD: Потрібно було б (1) виділити новий простір (2) скопіювати новий елемент (3) перенести - сконструювати існуючі елементи (4) знищити переміщені з елементів (5) звільнити старе зберігання - у ТОМУ порядку - зробити так, щоб перша версія працювала.
Ben Voigt

1
@BenVoigt чому б інший контейнер вимагав, щоб тип був CopyInsertable, якщо він все-таки повністю ігнорує цю властивість?
OlivierD

-2

З 23.3.6.5/1: Causes reallocation if the new size is greater than the old capacity. If no reallocation happens, all the iterators and references before the insertion point remain valid.

Оскільки ми вставляємо в кінці, жодні посилання не будуть визнані недійсними, якщо вектор не буде змінено. Отже, якщо векторний, capacity() > size()то він гарантовано працює, інакше це гарантовано невизначена поведінка.


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

У питанні не згадуються ітератори чи безпека ітератора.
OlivierD

3
@OlivierD частина ітератора тут зайва: мені цікава referencesчастина цитати.
Марк Б

2
Це насправді гарантовано безпечно (див. Мою відповідь, семантику push_back).
Ендже вже не пишається SO
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.