C++ПрограммированиеC++ Разработчик

Охарактеризуйте тип несовместимости, который не позволяет **std::pmr::vector<std::string>** использовать свой **std::pmr::polymorphic_allocator** для внутреннего хранения строк?

Проходите собеседования с ИИ помощником Hintsage

Ответ на вопрос

Несовместимость проистекает из типа-особенности std::uses_allocator, который оценивается как false для комбинации std::string и std::pmr::polymorphic_allocator. std::string жестко задает свой тип аллокатора как std::allocator<char>, в то время как std::pmr::vector предоставляет std::pmr::polymorphic_allocator<char>; это различные, несвязанные классы без неявного преобразования или наследственного отношения. Когда контейнер создает элементы, он запрашивает std::uses_allocator_v<T, Alloc>, чтобы определить, передавать ли аллокатор в качестве аргумента конструктора; поскольку эта проверка не проходит, вектор рассматривает std::string как не осведомленный об аллокаторе и вызывает его конструктор по умолчанию, который внутри использует глобальные new и delete, игнорируя ресурс памяти вектора.

static_assert(!std::uses_allocator_v<std::string, std::pmr::polymorphic_allocator<char>>); // std::pmr::vector не будет передавать свой аллокатор в std::string

Ситуация из жизни

Во время оптимизации движка расчета финансовых рисков мы реорганизовали горячую ветку, чтобы использовать std::pmr::monotonic_buffer_resource, основанный на памяти стека, чтобы устранить конкуренцию за кучу. Мы объявили std::pmr::vectorstd::string temp_symbols, ожидая, что все временные имена символов будут извлекаться из монотонного буфера, но профилирование производительности выявило неожиданные вызовы malloc внутри конструкторов std::string, указывая на то, что ресурс памяти полностью игнорировался.

Мы рассматривали возможность ручного создания каждой std::string с явным переданным std::pmr::polymorphic_allocator в ее конструктор, но это потребовало бы раскрытия деталей аллокации более высокоуровневой бизнес-логике и помешало бы использовать удобные модификаторы, такие как emplace_back. Другой подход заключался в создании пользовательского обертки строки, которая унаследовала бы от std::string и принимала бы полиморфный аллокатор, но это нарушило бы принцип подстановки Лисков и привело бы к рискам срезания объектов во время перераспределения контейнера. В конечном итоге мы заменили std::string на std::pmr::string (псевдоним для std::basic_string<char, std::char_traits<char>, std::pmr::polymorphic_allocator<char>>), который по своей сути задает allocator_type как полиморфный вариант. Это позволило вектору автоматически передавать свой аллокатор через протокол uses_allocator, устраняя все аллокации в куче на горячей ветке и сокращая задержку с микросекунд до сотен наносекунд.

Что часто упускают кандидаты

Как можно сделать совместимым пользовательский класс с std::pmr::polymorphic_allocator, если он выполняет внутреннюю динамическую аллокацию, учитывая, что простого принятия параметра аллокатора в его конструкторе недостаточно?

Класс должен явно указывать на свою осведомленность об аллокаторе, либо представляя публичный псевдоним типа allocator_type, который можно преобразовать из используемого аллокатора, либо предоставляя конструктор, чей первый параметр - это std::allocator_arg_t, а второй параметр - это тип аллокатора, в сочетании с специализацией std::uses_allocator<ClassName, Alloc>, чтобы наследоваться от std::true_type. Без этой явной рекламы std::pmr::vector предполагает, что класс не осведомлен об аллокаторе и создает его с помощью инициализации по умолчанию, что приводит к тому, что все внутренние аллокации обходят полиморфный ресурс памяти.

Почему std::allocator_traits<std::pmr::polymorphic_allocator<T>>::rebind_alloc<U> не разрешает несовместимость между std::pmr::vector и std::string?

Перепривязка создает std::pmr::polymorphic_allocator<U>, который остается несовместимым с std::allocator<U>, потому что это различные конкретные типы без отношения преобразования. Механизм std::uses_allocator требует, чтобы allocator_type элемента был тем же, что или преобразуемым из типа аллокатора контейнера, а не просто перепривязываемым к другому типу значения; поскольку std::string жестко задает std::allocator, перепривязка аллокатора контейнера не изменяет ожидаемый тип аллокатора элемента.

Какой конкретный риск жизненного цикла возникает при использовании std::pmr::monotonic_buffer_resource с std::pmr::string, и почему это обнаружение сложнее, чем с обычными аллокаторами?

Поскольку std::pmr::polymorphic_allocator является типо-устраиваемым и хранит указатель на базовый std::pmr::memory_resource, компилятор не может обеспечить ограничения жизненного цикла во время компиляции. Когда std::pmr::string, ссылающаяся на основанный на стеке monotonic_buffer_resource, перемещается или копируется в более долгоживущую область, указатель на ресурс памяти становится висячим; в отличие от std::allocator, который обычно использует глобальную кучу (всегда валидную), доступ к строке после разрушения буфера приводит к использованию после освобождения. Статические анализаторы с трудом обнаруживают это, потому что виртуальный интерфейс do_allocate/do_deallocate скрывает жизненный цикл подлежащего ресурса от типовой системы.