Подтвердить что ты не робот

Безопасность std:: unordered_map:: merge()

При написании кода, ориентированного на С++ 17, я как бы ударил камень преткновения, определяющий исключение-безопасность операции, объединяющей два совместимых std:: unordered_maps. В текущем рабочем документе , §26.2.7, в таблице 91, в частности, говорится об условиях a.merge( a2 ):

Требуется: a.get_allocator() == a2.get_allocator().

Попытка извлечь каждый элемент в a2 и вставить его в a, используя хеш-функцию и предикат равенства ключей a. В контейнерах с уникальными ключами, если в элементе a есть элемент с ключом, эквивалентным ключу элемента из a2, то этот элемент не извлекается из a2.

Постусловия: указатели и ссылки на переданные элементы a2 относятся к тем же элементам, но как к членам a. Итераторы, относящиеся к переданным элементам, и все итераторы, относящиеся к a, будут недействительными, но итераторы к элементам, оставшимся в a2, останутся действительными.

Броски: ничего, если не будет выбрана хеш-функция или предикат равенства.

Стоит отметить, что эти условия сильно напоминают те, которые приведены для требований обычных ассоциативных контейнеров (std:: map), приведенных в п. 26.2.6, таблица 90, a.merge( a2 ):

Требуется: a.get_allocator() == a2.get_allocator().

Попытка извлечь каждый элемент в a2 и вставить его в a с помощью объекта сравнения a. В контейнерах с уникальными ключами, если в элементе a есть элемент с ключом, эквивалентным ключу элемента из a2, то этот элемент не извлекается из a2.

Постусловия: указатели и ссылки на переданные элементы a2 относятся к тем же элементам, но как к членам a. Итераторы, относящиеся к переданным элементам, будут продолжать ссылаться на свои элементы, но теперь они ведут себя как итераторы в a, а не в a2.

Броски: ничего, кроме объекта сравнения.

Мне нужно было объединить два std:: unordered_maps с тем же количеством элементов, которые я мог обеспечить, были бы уникальными для обоих контейнеров, что означает, что карта, содержащая объединенный результат, удвоит количество элементов, которые она ранее имела, и контейнер слит - из будет пустым. Это должно быть абсолютно безопасно благодаря С++ 17, правильно?

Это огромная победа в плане производительности... кроме того, у меня было это сомнительное сомнение. Как ни странно, в постусловном заявлении ничего не говорится о том, будет ли соблюден предыдущий максимальный коэффициент загрузки в объединенной карте, и хотя это кажется безопасным подразумеваемым предположением, оно, казалось, наивно противоречило заявлению об исключении-безопасности unordered_map. Если вы используете схему хеш-таблицы, в которой ведра являются смежными выделенными буферами, поддержание вашего коэффициента загрузки, по-видимому, подразумевает перезагрузку, что, по-видимому, подразумевает перераспределение буфера буфера.

Уже это казалось упражнением в экстремальном взгляде на пуповины, и было хорошей причиной, чтобы уйти достаточно хорошо в одиночку: более сложный хеш-стол можно было бы сделать как полностью основанную на node структуру, похожую на красно- черные деревья обычно лежат в основе std:: map, и в таком случае спецификация представляется разумной, так как пересмотр не предполагает выделения.

Возможно, в мою пользу, я поддался сомнению и вникнул в реализацию gcc-7.1 слияния. Это невероятно сложно, но, чтобы обобщить мои выводы, я обнаружил, что ведра, действительно, смежно распределены буферами, и повторное использование подразумевает перераспределение. Может быть, подумал я, была какая-то более глубокая магия, которую мне не хватало (я смотрел на источник почти целый день и все еще чувствовал, что у меня плохое понимание), который просто отключил повторное переключение во время слияния, а это означает, что все указанные условия будет поддерживаться, но вы можете получить неприятную регрессию производительности после достаточно большого слияния, поскольку ваша карта, вероятно, будет перегружена.

Я перешел на практическую оценку, отражающую мой случай использования (который я бы представил, если это было возможно, извините), вместо того, чтобы просто допросить мою интерпретацию libstdС++:

#include <memory>        // for std::shared_ptr<>
#include <new>           // for std::bad_alloc
#include <utility>       // for std::move(), std::pair<>
#include <type_traits>   // for std::true_type
#include <unordered_map> // for std::unordered_map<>
#include <functional>    // for std::hash<>, std::equal_to<>
#include <string>        // for std::string
#include <iostream>      // for std::cout
#include <cstddef>       // for std::size_t

template<typename T>
class PrimedFailureAlloc
{
public:
  using value_type = T;
  using propagate_on_container_copy_assignment = std::true_type;
  using propagate_on_container_move_assignment = std::true_type;
  using propagate_on_container_swap = std::true_type;

  PrimedFailureAlloc() = default;

  template<typename U>
  PrimedFailureAlloc( const PrimedFailureAlloc<U>& source ) noexcept
    : m_triggered{ source.m_triggered }
  { }

  template<typename U>
  PrimedFailureAlloc( PrimedFailureAlloc<U>&& source ) noexcept
    : m_triggered{ std::move( source.m_triggered ) }
  { }

  T* allocate( std::size_t n )
  {
    if ( *m_triggered ) throw std::bad_alloc{};
    return static_cast<T*>( ::operator new( sizeof( T ) * n ) );
  }

  void deallocate( T* ptr, std::size_t n ) noexcept
  {
    ::operator delete( ptr );
  }

  bool operator==( const PrimedFailureAlloc& rhs ) noexcept
  {
    return m_triggered == rhs.m_triggered;
  }

  void trigger() noexcept { *m_triggered = true; }

private:
  template<typename U>
  friend class PrimedFailureAlloc;

  std::shared_ptr<bool> m_triggered{ new bool{ false } };
};

template<typename T>
bool operator!=( const PrimedFailureAlloc<T>& lhs,
                 const PrimedFailureAlloc<T>& rhs ) noexcept
{
  return !(lhs == rhs);
}

template< typename Key
        , typename T
        , typename Hash = std::hash<Key>
        , typename KeyEqual = std::equal_to<Key>
        >
using FailingMap = std::unordered_map<
  Key,
  T,
  Hash,
  KeyEqual,
  PrimedFailureAlloc<std::pair<const Key, T>>
>;

template<typename Key, typename T>
void printMap( const FailingMap<Key, T>& map )
{
  std::cout << "{\n";
  for ( const auto& [str, index] : map )
    std::cout << "  { " << str << ", " << index << " }\n";
  std::cout << "}\n";
}

int main()
{
  PrimedFailureAlloc<std::pair<const std::string, int>> a;
  FailingMap<std::string, int> m1{ a };
  FailingMap<std::string, int> m2{ a };

  m1.insert( { { "Purple", 0 }, { "Green", 3 }, { "Indigo", 16 } } );
  m2.insert( { { "Blue", 12 }, { "Red", 2 }, { "Violet", 5 } } );

  // m1.reserve( m1.size() + m2.size() );
  a.trigger();
  m1.merge( m2 );

  std::cout << "map :=\n";
  printMap( m1 );

  return 0;
}

Конечно, после компиляции этого кода под GCC-7.1 я получаю:

terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc
[1]    10944 abort      ./a.out

В то время как строка 98 (m1.reserve( m1.size() + m2.size() );) без комментирования приводит к ожидаемому результату:

map :=
{
  { Red, 2 }
  { Violet, 5 }
  { Purple, 0 }
  { Green, 3 }
  { Blue, 12 }
  { Indigo, 16 }
}

Понимая, что С++ 17 по-прежнему является стандартом проекта, который еще не был доработан, и что реализация gcc является экспериментальной, я полагаю, что мой вопрос будет следующим:

  • Неужели я серьезно неверно истолковал безопасность операции слияния, как указано в стандарте? Существуют ли лучшие методы использования std::unordered_map::merge(), которые я пропустил? Должен ли я быть неявным ответственным за обеспечение распределения ведер? Будет ли использование std::unordered_map::reserve() действительно переносимым, когда clang, MSVC и Intel наконец поддерживают С++ 17? Я имею в виду, что моя программа прерывается, когда невозможно исключить объединение без исключения, после некоторого понятия придерживайтесь перечисленных постусловий...
  • Это на самом деле дефект в стандарте? Сходство формулировки между неупорядоченными ассоциативными контейнерами и обычными ассоциативными контейнерами могло бы позволить непреднамеренным гарантиям ускользнуть, если текст был, скажем, скопирован.
  • Это просто ошибка gcc?

Стоит отметить, что я проверял gcc-трекер перед этой записью и, казалось, не обнаружил никаких открытых ошибок, соответствующих моему описанию, и, кроме того, я проверил стандартный отчет о дефектах С++ и, похоже, выглядел пустым (правда, выполнение текстового поиска этого сайта усугубляется, и я, возможно, был менее тщательным). Последнее неудивительно, поскольку стандартные дефекты и их обходные пути или последствия обычно отмечаются в исходном коде gcc, и во время моего изучения я не обнаружил таких обозначений. Я попытался скомпилировать свой пример кода в моей последней проверке clang (старше недели), но компилятор отключился, поэтому я больше не проводил экзамен и не консультировался с libС++.

4b9b3361

Ответ 1

Это просто дефект в стандарте, т.е. ваша возможность 2.

LWG только что переместил LWG issue 2977 в статус "Готов", который ударит по ошибочному предложению Throws.

Ответ 2

Чтобы понять, правильна ли формулировка в стандарте или нет, вам нужно изучить основные функции.
Слияние состоит из двух операций.

  • указывает указатели на добавленные элементы. Поскольку они уже выделены, здесь не предусмотрено выделение.
  • в случае достижения границ для количества элементов в ведре, вызывается rehash().

Вызов rehash() - это точка, где вы ожидаете, что будет выбрано исключение. Сол, чтобы изучить его безопасность исключения.

23.2.5.1 Гарантии безопасности исключений [unord.req.except]
1 Для неупорядоченных ассоциативных контейнеров никакая функция clear() не генерирует исключение. erase (k) не бросает исключение, если это исключение выбрано контейнерами Hash или Pred object (если есть).
2 Для неупорядоченных ассоциативных контейнеров, если исключение вызывается любой операцией, отличной от контейнеров хеш-функции из функции вставки или emplace, вставляющей один элемент, вставка не имеет эффект.
3 Для неупорядоченных ассоциативных контейнеров никакая функция swap не выдает исключение, если это исключение не выбрасывается путем обмена контейнеров Hash или Pred object (если есть).
4 Для неупорядоченных ассоциативных контейнеров, если исключение выбрано из функции rehash(), отличной от с помощью хэш-функции контейнеров или функции сравнения функция rehash() не имеет эффекта.

Как вы можете видеть, для функции rehash() определено, что она ничего не делает, если исключение выбрано внутри, кроме как для хеша или функции сравнения. То есть, на мой взгляд, идеально соответствует определению для слияния:

Броски: ничего, если не будет выбрана хеш-функция или предикат равенства.

Я понимаю, что, когда нет места для увеличения базовой структуры данных для списка ведра, она сохраняется в ее первоначальной форме. Это может привести к незначительному неэффективному доступу к элементам, поскольку в отдельных ведрах может быть больше элементов, чем определено. То же самое может произойти во время вставки.

Где проблема в вашем случае? Возможно, в реализации функции rehash(), которая бросает туда, где она не должна.

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Я не эксперт по этой теме. Это то, что я нашел. Поэтому не стесняйтесь исправить меня, если я ошибаюсь.