Викрадено ресурси з ключів std :: карта дозволено?


15

Чи добре в C ++ вкрасти ресурси з карти, яка мені вже не потрібна? Точніше, припустимо , що у мене є std::mapз std::stringключами , і я хочу , щоб побудувати вектор з нього шляхом крадіжки ресурсів mapи ключів з використанням std::move. Зауважте, що такий запис запису до клавіш пошкоджує внутрішню структуру даних (впорядкування ключів), mapале я не буду її використовувати згодом.

Питання : Чи можу я це зробити без проблем або це призведе до несподіваних помилок, наприклад, у деструкторі, mapтому що я отримав доступ до нього таким чином, який std::mapне призначений?

Ось приклад програми:

#include<map>
#include<string>
#include<vector>
#include<iostream>
using namespace std;
int main(int argc, char *argv[])
{
    std::vector<std::pair<std::string,double>> v;
    { // new scope to make clear that m is not needed 
      // after the resources were stolen
        std::map<std::string,double> m;
        m["aLongString"]=1.0;
        m["anotherLongString"]=2.0;
        //
        // now steal resources
        for (auto &p : m) {
            // according to my IDE, p has type 
            // std::pair<const class std::__cxx11::basic_string<char>, double>&
            cout<<"key before stealing: "<<p.first<<endl;
            v.emplace_back(make_pair(std::move(const_cast<string&>(p.first)),p.second));
            cout<<"key after stealing: "<<p.first<<endl;
        }
    }
    // now use v
    return 0;
}

Це дає вихід:

key before stealing: aLongString
key after stealing: 
key before stealing: anotherLongString
key after stealing: 

EDIT: Я хотів би зробити це для всього вмісту великої карти та зберегти динамічні асигнування шляхом крадіжки ресурсів.


3
Яка мета цієї «крадіжки»? Щоб видалити елемент з карти? Тоді чому б просто не зробити це (стерти елемент з карти)? Також зміна constзначення завжди є UB.
Якийсь програміст чувак

мабуть, це призведе до серйозних помилок!
rezaebrh

1
Не пряма відповідь на ваше запитання, але: Що робити, якщо ви не повернете вектор, а діапазон чи пару ітераторів? Це дозволить уникнути копіювання повністю. У будь-якому випадку вам потрібні орієнтири для відстеження прогресу оптимізації та профайлер для пошуку гарячих точок.
Ульріх Екхардт

1
@ ALX23z Чи є у вас джерела для цього твердження. Я не можу уявити, як копіювання покажчика дорожче, ніж копіювання цілого регіону пам'яті.
Себастьян Гофман

1
@SebastianHoffmann згадувалося про недавній CppCon не впевнений, на якому розмові, тхо. Справа в std::stringоптимізації коротких струн. Це означає, що існує певна нетривіальна логіка копіювання та переміщення, а не просто обмін вказівниками, а крім того, більшість часу переміщення передбачає копіювання - щоб не мати справу з досить довгими рядками. Різниця в статистиці в будь-якому випадку була невеликою, і загалом вона, безумовно, змінюється в залежності від того, який тип обробки рядків виконується.
ALX23z

Відповіді:


18

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

Ніколи не слід використовувати const_castдля видалення constзі змінної та зміни цієї змінної.

При цьому, C ++ 17 має рішення для вашої проблеми: std::map's extractfunction:

#include <map>
#include <string>
#include <vector>
#include <utility>

int main() {
  std::vector<std::pair<std::string, double>> v;
  std::map<std::string, double> m{{"aLongString", 1.0},
                                  {"anotherLongString", 2.0}};

  auto extracted_value = m.extract("aLongString");
  v.emplace_back(std::make_pair(std::move(extracted_value.key()),
                                std::move(extracted_value.mapped())));

  extracted_value = m.extract("anotherLongString");
  v.emplace_back(std::make_pair(std::move(extracted_value.key()),
                                std::move(extracted_value.mapped())));
}

І не варто using namespace std;. :)


Дякую, я спробую це! Але ти впевнений, що я не можу зробити так, як я? Я маю на увазі, що mapне буду скаржитися, якщо я не називаю його методи (що я не роблю), і, можливо, внутрішнє впорядкування не має важливого значення в його руйнівнику?
фінз

2
Ключі карти створені const. Вимкнення constоб'єкта - це миттєвий UB, незалежно від того, чи дійсно щось після цього отримує доступ до них.
HTNW

У цього методу є дві проблеми: (1) Оскільки я хочу витягти всі елементи, я не хочу витягувати за допомогою ключа (неефективного пошуку), а за допомогою ітератора. Я бачив, що це теж можливо, тому це добре. (2) Виправте мене, якщо я помиляюсь, але для вилучення всіх елементів буде величезна накладні витрати (для відновлення внутрішньої структури дерева при кожному видобутку)?
фінз

2
@phinz Як ви бачите на cppreference extract при використанні ітераторів, як аргумент амортизував постійну складність. Деякі накладні витрати неминучі, але це, мабуть, буде недостатньо значущим. Якщо у вас є особливі вимоги, які не охоплені цим, вам потрібно буде реалізувати власні, що mapвідповідають цим вимогам. Ці stdконтейнери призначені для загального призначення application.They спільного не оптимізовані для конкретних випадків застосування.
волоський горіх

@HTNW Ви впевнені, що ключі створені const? У цьому випадку ви можете, будь ласка, вказати, де моя аргументація неправильна.
фінз

4

Ваш код намагається модифікувати constоб'єкти, тому він має невизначену поведінку, як правильно вказує відповідь друкерман .

Деякі інші відповіді ( Фінза та Декі ) стверджують, що ключ не повинен зберігатися як constоб’єкт, оскільки ручка вузла, отримана в результаті вилучення вузлів з карти, дозволяє не constмати доступу до ключа. Цей висновок може здатися спочатку правдоподібним, але P0083R3 , документ, який запровадив extractфункціональні можливості), має спеціальний розділ з цієї теми, який обґрунтовує цей аргумент:

Побоювання

З цього приводу було висунуто кілька проблем. Ми звернемося до них тут.

Невизначена поведінка

Найважчою частиною цієї пропозиції з теоретичної точки зору є той факт, що видобутий елемент зберігає свій ключовий тип const. Це запобігає виїзду з нього або зміні його. Для вирішення цього питання ми надали функцію ключового аксесуара, яка забезпечує безконструктивний доступ до ключа в елементі, утримуваному ручкою вузла. Ця функція вимагає "магії" реалізації, щоб забезпечити її правильну роботу за наявності оптимізацій компілятора. Один із способів зробити це - об'єднанням pair<const key_type, mapped_type> та pair<key_type, mapped_type>. Перетворення між ними може бути здійснено безпечно, використовуючи методику, аналогічну тій, що використовується при std::launderвидобутку та повторному введенні.

Ми не відчуваємо, що це становить будь-яку технічну чи філософську проблему. Одна з причин , стандартна бібліотека існує, щоб написати непортабельний і чарівний код , який клієнт не може писати в портативних C ++ (наприклад <atomic>, <typeinfo>, <type_traits>і т.д.). Це просто ще один такий приклад. Все, що потрібно постачальникам компіляторів, щоб реалізувати цю магію, - це те, що вони не використовують невизначену поведінку в спілках для оптимізації цілей, і в даний час компілятори це вже обіцяють (настільки, якою тут скористаються).

Це дійсно накладає обмеження на клієнта, яке, якщо ці функції використовуються, std::pairне може бути спеціалізованим таким, яке pair<const key_type, mapped_type>має інший макет, ніж pair<key_type, mapped_type>. Ми вважаємо, що ймовірність того, що хтось насправді хоче цього зробити, фактично дорівнює нулю, і у формальній редакції ми обмежуємо будь-яку спеціалізацію цих пар.

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

(наголос мій)


Це насправді частина відповіді на початкове запитання, але я можу прийняти лише одну.
фінз

0

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

Цей відповідь стверджує , що

Іншими словами, ви отримуєте UB, якщо ви змінили спочатку об'єкт const, а інакше - ні.

Отже, рядок v.emplace_back(make_pair(std::move(const_cast<string&>(p.first)),p.second));у запитанні не призводить до UB тоді і лише тоді, коли stringоб’єкт p.firstне був створений як об'єкт const. Тепер зауважимо, що посилання проextract держави

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

Так що, якщо я , відповідний , продовжує жити в своєму місці зберігання. Але після видобутку мені дозволяють забрати ресурси, як у кодексі відповіді друкарня . Це означає, що, отже, і об'єкт не був створений як об'єкт const спочатку.extractnode_handleppmoveppstringp.first

Тому я думаю, що модифікація mapклавіш 's не призводить до UB, і з відповіді Декі , схоже, що і тепер пошкоджена структура дерева (тепер декілька тих же порожніх рядкових клавіш) mapне створює проблем у деструкторі. Таким чином, код у запитанні повинен добре працювати принаймні в C ++ 17, де extractіснує метод (і заява про покажчики залишаються дійсними утримуваннями).

Оновлення

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


1
Вибачте, фінз, ваша відповідь неправильна. Я напишу відповідь, щоб пояснити це - це має щось спільне з профспілками і std::launder.
LF

-1

EDIT: Ця відповідь неправильна. Добрі коментарі вказували на помилки, але я її не видаляю, оскільки на неї посилалися в інших відповідях.

@druckermanly відповів на ваше перше запитання, в якому сказано, що зміна клавіш mapсилою порушує впорядкованість, на якій mapбудується внутрішня структура даних (Червоно-Чорне дерево). Але безпечно використовувати extractметод, оскільки він робить дві речі: перемістіть ключ із карти та видаліть його, щоб він зовсім не впливав на впорядкованість карти.

Інше питання, яке ви поставили, чи може це спричинити проблеми при деконструкції, не є проблемою. Коли карта деконструює, вона викличе деконструктор кожного з її елементів (mapped_types тощо), і moveметод забезпечує безпечне деконструювання класу після його переміщення. Тому не хвилюйтесь. Коротше кажучи, саме робота moveзабезпечує безпечне видалення або перепризначення нового класу "переміщеному" класу. Спеціально для stringцього moveметоду можна встановити свій покажчик nullptr, щоб він не видаляв фактичні дані, переміщені під час виклику деконструктора початкового класу.


Коментар нагадав мені точку, яку я оглядав, в основному він мав рацію, але є одна річ, з якою я повністю не згоден: const_castце, мабуть, не UB. constце лише обіцянка між компілятором і нами. об'єкти, відзначені як constі раніше, є об'єктом, таким же, як і ті, що не мають const, за їх типами та уявленнями у бінарній формі. Коли constвідкидається, він повинен поводитись так, ніби це нормальний клас, що змінюється. Що стосується move, Якщо ви хочете використовувати його, ви повинні пройти а, &а не const &так, як я бачу, що це не UB, він просто порушує обіцянку constі переміщує дані.

Я також зробив два експерименти, використовуючи MSVC 14.24.28314 та Clang 9.0.0 відповідно, і вони дали однаковий результат.

map<string, int> m;
m.insert({ "test", 2 });
m.insert({ "this should be behind the 'test' string.", 3 });
m.insert({ "and this should be in front of the 'test' string.", 1 });
string let_me_USE_IT = std::move(const_cast<string&>(m.find("test")->first));
cout << let_me_USE_IT << '\n';
for (auto const& i : m) {
    cout << i.first << ' ' << i.second << '\n';
}

вихід:

test
and this should be in front of the 'test' string. 1
 2
this should be behind the 'test' string. 3

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

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


2
"про те, чи може це викликати неприємності при деконструкції, не є проблемою." Технічно правильно, оскільки невизначена поведінка (зміна constзначення) сталася раніше. Однак аргумент " move[функція] гарантує безпеку деконструкції [об'єкта] класу після його переміщення" не втримується: Ви не можете безпечно переміщуватися з constоб'єкта / посилання, оскільки для цього потрібна модифікація, яка constперешкоджає. Ви можете спробувати подолати це обмеження, використовуючи const_cast, але в цьому випадку ви, в кращому випадку, заглиблюєтесь у певну поведінку, якщо не на UB.
hoffmale

@hoffmale Спасибі, я не помітив і зробив велику помилку. Якщо це не ви вказали на це, моя відповідь тут може ввести в оману когось іншого. Насправді я повинен сказати, що moveфункція бере &замість а const&, тому якщо хтось наполягає, щоб він перемістив ключ з карти, він повинен використовувати const_cast.
Декі

1
"об'єкти, зазначені як const, як і раніше є об'єктом, як і ті, що не мають const, з точки зору їх типів та подань у бінарній формі". Крім того, const дає можливість компілятору міркувати про код і кешувати значення, а не генерувати код для декількох читань (що може значно змінити продуктивність). Таким чином, UB, викликаний, const_castбуде неприємним. Це може працювати більшу частину часу, але порушувати ваш код тонко.
LF

Але тут я думаю, що ми можемо бути впевнені, що об’єкти не вкладаються в пам'ять лише для читання, оскільки після цього extractнам дозволяється рухатися від того самого об’єкта, правда? (див. мою відповідь)
фінз

1
Вибачте, аналіз у вашій відповіді невірний. Я напишу відповідь, щоб пояснити це - це щось стосується спілок іstd::launder
LF
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.