Вам нужно перенести элемент между двумя ассоциативными контейнерами без копирования ключа и повторного выделения памяти. Какой механизм C++17 следует выбрать?
Используйте узлы-контейнеры — node_handle, получаемые через extract и передаваемые в другой контейнер через insert. Элемент извлекается вместе с внутренним узлом, поэтому его ключ и значение не копируются и не перемещаются, а память узла обычно переиспользуется.
До C++17 перенос элемента между ассоциативными контейнерами обычно выполняли через копирование или перемещение значения: сначала извлекали его содержимое, затем создавали новый узел в целевом контейнере. Это могло быть дорого для крупных объектов и лишало программу возможности сохранить сам выделенный узел.
Механизм node_handle появился для эффективного изменения принадлежности элементов, переноса узлов между совместимыми контейнерами и безопасного изменения ключа у узлов отображений.
Обычная операция вставки принимает значение элемента, поэтому может потребовать конструирования нового объекта и выделения памяти. Для контейнеров с дорогими ключами или значениями это приводит к лишним операциям и временным пикам потребления памяти.
При неверном использовании важно учитывать, что после extract элемент больше не принадлежит исходному контейнеру. Пустой node_handle нельзя разыменовывать, а вставка может завершиться неуспешно, например при конфликте ключей в std::map.
extract удаляет узел из контейнера и возвращает объект типа node_type. Сам узел сохраняет ключ, mapped-значение и связанную с ним память. Затем insert принимает этот узел и пытается включить его в целевой контейнер.
После extract размер source уменьшается. Если ключ уже существует в target, вставка в std::map не выполняется: узел возвращается через результат операции и остаётся доступным для повторного решения, например для другого контейнера.
У std::map ключ внутри обычного элемента доступен как const, но у непустого node_handle его можно изменить через node.key(). Это позволяет исправить ключ до вставки, не создавая новый элемент. После успешной вставки узел становится пустым.
Для безопасной передачи узла контейнеры должны иметь совместимый тип узла и совместимые аллокаторы. В частности, нельзя безоговорочно переносить узел между контейнерами с неравными аллокаторами: требования стандартной библиотеки к такой операции нужно проверить для конкретного контейнера и версии стандарта; для стандартного аллокатора обычно проблем нет.
Итератор на извлечённый элемент инвалидируется, однако ссылки и указатели на элемент сохраняют валидность, пока сам элемент существует в node_handle. После успешной вставки они продолжают ссылаться на тот же элемент, уже находящийся в целевом контейнере.
Сервис хранит конфигурации в std::map и периодически переносит отдельные записи из рабочего набора в архивный. Вариант с копированием прост, но копирует строки ключей и значения, а вариант с удалением и повторной вставкой дополнительно выделяет память для нового узла.
Можно было бы хранить значения в умных указателях и переносить только указатели, но это усложняет владение, ухудшает локальность данных и меняет модель хранения. node_handle сохраняет обычную структуру контейнера и переносит сам узел без копирования элемента, поэтому для совместимых контейнеров это наиболее прямое решение.
Практический результат — меньше аллокаций и операций над крупными объектами. При этом код должен явно обработать неуспешную вставку и не использовать исходный контейнерный итератор после extract.
Можно ли изменить ключ элемента после его извлечения?
Да. У непустого node_handle для ассоциативного контейнера ключ доступен через key(), и его можно изменить до вставки. Это безопасно, потому что узел в этот момент не участвует в упорядоченной структуре контейнера. Изменять ключ непосредственно у элемента, который уже находится в std::map, нельзя: это нарушило бы инварианты дерева.
Что происходит, если вставка узла в std::map не удалась?
Если в целевом контейнере уже есть эквивалентный ключ, элемент не заменяется. Результат insert содержит признак неуспеха и возвращает сам узел, поэтому его можно изменить и вставить повторно в другой контейнер. Узел не теряется автоматически.
Гарантирует ли node_handle отсутствие любой аллокации при вставке?
Для совместимых контейнеров с совместимыми аллокаторами перенос самого узла не требует повторного выделения памяти. Однако это не означает, что операция вообще не может выполнять дополнительные действия: контейнеру нужно проверить порядок или уникальность ключа, а пользовательские операции сравнения могут иметь собственную стоимость. Кроме того, несовместимые аллокаторы могут сделать перенос недопустимым или изменить применимые гарантии, поэтому аллокаторную совместимость нельзя игнорировать.