Кража ресурсов из ключей std :: map разрешена?

15

В C ++ нормально ли красть ресурсы с карты, которая мне больше не нужна? Точнее, предположим, что у меня есть ключи std::mapс std::stringключами, и я хочу создать из него вектор путем кражи ресурсов mapс использованием ключей s 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: 

РЕДАКТИРОВАТЬ: Я хотел бы сделать это для всего содержимого большой карты и сохранить динамическое распределение путем кражи этого ресурса.

phinz
источник
3
Какова цель этого "воровства"? Удалить элемент с карты? Тогда почему бы просто не сделать это (стереть элемент с карты)? Кроме того, изменение constзначения всегда UB.
Какой-то программист чувак
видимо это приведет к серьезным ошибкам!
Резабб
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;. :)

druckermanly
источник
Спасибо, я попробую это! Но ты уверен, что я не могу сделать так, как сделал я? Я имею в виду, что mapне будет жаловаться, если я не буду вызывать его методы (что я не делаю), и, возможно, внутренний порядок не важен в его деструкторе?
Финз
2
Ключи карты созданы const. Мутирование constобъекта происходит мгновенно, независимо от того, получает ли он что-либо на самом деле впоследствии.
HTNW
У этого метода есть две проблемы: (1) Поскольку я хочу извлечь все элементы, я не хочу извлекать ключом (неэффективный поиск), но итератором. Я видел, что это тоже возможно, так что это нормально. (2) Поправьте меня, если я ошибаюсь, но для извлечения всех элементов будут огромные накладные расходы (для перебалансировки внутренней древовидной структуры при каждом извлечении)?
Финз
2
@phinz Как вы можете видеть на cppreference extract при использовании итераторов в качестве аргумента, амортизируется постоянная сложность. Некоторые накладные расходы неизбежны, но, вероятно, они не будут достаточно значительными, чтобы иметь значение. Если у вас есть особые требования, не охватываемые этим, то вам нужно будет реализовать свои собственные mapтребования, удовлетворяющие этим требованиям. Эти stdконтейнеры предназначены для общего назначения application.They общего не оптимизированы для конкретных случаев применения.
грецкий орех
@HTNW Вы уверены, что ключи созданы const? В этом случае не могли бы вы указать, где мои аргументы неверны.
phinz
4

Ваш код пытается изменить constобъекты, поэтому он имеет неопределенное поведение, как правильно указывает ответ druckermanly .

В некоторых других ответах ( Финса и Дьючи ) утверждается, что ключ не должен храниться как constобъект, поскольку дескриптор узла, полученный в результате извлечения узлов из карты, допускает отсутствие constдоступа к ключу. На первый взгляд этот вывод может показаться правдоподобным, но P0083R3 , статья, которая представила extractфункциональные возможности), имеет специальный раздел на эту тему, который лишает законной силы этот аргумент:

Обеспокоенность

Несколько проблем были подняты об этой конструкции. Мы обратимся к ним здесь.

Неопределенное поведение

Наиболее сложной частью этого предложения с теоретической точки зрения является тот факт, что извлеченный элемент сохраняет свой тип константного ключа. Это предотвращает выход из него или его изменение. Чтобы решить эту проблему, мы обеспечили ключевую функцию доступа, которая обеспечивает неконстантный доступ к ключу в элементе , принадлежащий ручкой узла. Эта функция требует реализации "магии", чтобы гарантировать ее правильную работу при наличии оптимизаций компилятора. Один из способов сделать это - объединение 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>. Мы чувствуем, что вероятность того, что кто-то действительно захочет это сделать, фактически равна нулю, и в формальной формулировке мы ограничиваем любую специализацию этих пар.

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

(акцент мой)

LF
источник
На самом деле это часть ответа на первоначальный вопрос, но я могу принять только один.
Финз
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ключей не приводит к UB, и из ответа Деучи , кажется, что также теперь поврежденная древовидная структура (теперь несколько одинаковых пустых строковых ключей) mapне создает проблем в деструкторе. Таким образом, код в вопросе должен нормально работать, по крайней мере, в C ++ 17, где extractметод существует (и утверждение о том, что указатели остаются действительными, сохраняется).

Обновить

Теперь я считаю, что этот ответ неверен. Я не удаляю его, потому что на него ссылаются другие ответы.

phinz
источник
1
Извините, Финз, ваш ответ неверный. Я напишу ответ, чтобы объяснить это - это как-то связано с профсоюзами и std::launder.
LF
-1

РЕДАКТИРОВАТЬ: Этот ответ неверен. Добрые комментарии указали на ошибки, но я не удаляю их, потому что на них есть ссылки в других ответах.

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

Другой вопрос, который вы задали относительно того, не вызовет ли это проблемы при деконструкции, не является проблемой. Когда карта деконструируется, она вызывает деконструктор каждого из своих элементов (mapped_types и т. Д.), И moveметод гарантирует, что безопасно деконструировать класс после его перемещения. Так что не волнуйся. В двух словах, именно эта операция moveгарантирует, что можно безопасно удалить или переназначить какое-то новое значение «перемещенному» классу. Специально для string, moveметод может установить свой указатель на символ nullptr, поэтому он не удаляет фактические данные, которые были перемещены, когда был вызван деконструктор исходного класса.


Комментарий напомнил мне пункт, который я пропустил, в основном он был прав, но есть одна вещь, с которой я не полностью согласен: const_castэто, вероятно, не UB. constэто просто обещание между компилятором и нами. объекты, помеченные как constвсе еще являются объектами, такими же, как те, с которыми они не имеют const, с точки зрения их типов и представлений в двоичной форме. Когда constобъект отбрасывается, он должен вести себя так, как будто это обычный изменяемый класс. Что касается move, если вы хотите использовать его, вы должны передать &вместо a 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Функции и так далее полагаться на правильность их упорядоченности внутренней структуры данных, и что является причиной , почему мы должны держаться подальше от мысли о том, заглядывая внутрь.

Deuchie
источник
2
«Что касается того, вызовет ли это проблемы при деконструкции, это не проблема». Технически правильно, поскольку неопределенное поведение (изменение constзначения) произошло раньше. Однако аргумент « move[функция] гарантирует, что безопасно деконструировать [объект] класса после того, как он был перемещен» не выполняется: вы не можете безопасно перемещаться из constобъекта / ссылки, так как это требует модификации, которая constпредотвращает. Вы можете попытаться обойти это ограничение с помощью const_cast, но в этот момент вы в лучшем случае углубляетесь в поведение, специфичное для реализации, если не UB.
hoffmale
@hoffmale Спасибо, я упустил из виду и сделал большую ошибку. Если это не вы указали это, мой ответ может ввести в заблуждение кого-то другого. На самом деле я должен сказать, что moveфункция принимает &вместо a const&, поэтому, если кто-то настаивает на том, что он перемещает ключ с карты, он должен использовать const_cast.
Deuchie
1
«объекты, помеченные как const, по-прежнему являются объектами, такими же, как объекты, не имеющие const, с точки зрения их типов и представлений в двоичной форме». Нет. Объекты const могут быть помещены в постоянную память. Кроме того, const позволяет компилятору рассуждать о коде и кэшировать значение вместо генерации кода для множественного чтения (что может иметь большое значение с точки зрения производительности). Так что UB, вызванный, const_castбудет неприятным. Это может работать большую часть времени, но неуклюже нарушает ваш код.
LF
Но здесь я думаю, что мы можем быть уверены, что объекты не помещаются в постоянную память, потому что после extractэтого нам разрешается двигаться от одного и того же объекта, верно? (см. мой ответ)
phinz
1
Извините, анализ в вашем ответе неверен. Я напишу ответ, чтобы объяснить это - это как-то связано с профсоюзами иstd::launder
Л.Ф.