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

Зачем разрешать `propagate_on_container_swap == false` в Allocators, если это может привести к поведению undefined?

Примечание: Первоначально заданный Matt Mcnabb как comment on Почему замена стандартных контейнеров библиотек может быть проблематичной в С++ 11 (с участием распределителей)?.


Стандарт (N3797) говорит, что если progagate_on_container_swap внутри Allocator std::false_type, он даст поведение undefined если задействованные два распределителя не сравниваются с равными.

  • Почему Стандарт допускает такую ​​конструкцию, когда она кажется более чем опасной?

23.2.1p9 Общие требования к контейнерам [container.requirements.general]

Если   allocator_traits<allocator_type>::propagate_on_container_swap::value  true, то распределители a и b также должны быть обменены   используя неквалифицированный вызов не-члену swap. В противном случае они должны   не меняются местами, а поведение undefined, если a.get_allocator() == b.get_allocator().

4b9b3361

Ответ 1

Я могу вспомнить несколько реальных сценариев, когда конструкция, разрешенная Стандартом, имеет смысл и требуется, однако; Сначала я попытаюсь ответить на этот вопрос с более широкой точки зрения, не затрагивая какой-либо конкретной проблемы.


ОБЪЯСНЕНИЕ

Распределители - это магические вещи, ответственные за распределение, построение, разрушение и освобождение памяти и объектов. Так как С++ 11, когда вступили в действие распределители состояния, распределитель может делать гораздо больше, чем раньше, но все сводится к ранее упомянутым четырем операциям.

Абонент имеет множество требований, один из которых - a1 == a2 (где a1 и a2 - распределители того же типа) должен давать true только, если память, выделенная можно отменить другой [1].

Вышеуказанное требование operator== означает, что два распределителя, сравнивающих одинаковые, могут делать что-то по-другому, если они все еще имеют взаимопонимание о распределении памяти.

Вышеприведенное означает, что стандарт позволяет propagate_on_container_* быть равным std::false_type; мы могли бы захотеть изменить содержимое двух контейнеров, у которых распределители имеют одинаковое поведение при освобождении, но оставим другое поведение (не связанное с управлением основной памятью).


[1] как указано в [allocator.requirements]p2 (таблица 28)


ИСТОРИЯ (SILLY)

Представьте, что у нас есть Allocator с именем Watericator, он собирает воду по требуемому распределению и передает ее в запрошенный контейнер.

Watericator - это Allocator с сохранением состояния, и при построении нашего экземпляра мы можем выбрать два режима;

  • использует Эрика, который забирает воду в пресную воду spring, а также измеряет (и сообщает) уровень и чистоту воды.

  • используйте Адама, который использует выталкивание на заднем дворе и не заботится о регистрации. Адам намного быстрее, чем Эрик.


Независимо от того, где вода приходит от нас, всегда распоряжаться ею таким же образом; путем полива наших растений. Даже если у нас есть один экземпляр, где Эрик снабжает нас водой (памятью), а другой, где Adam использует краны, оба обойщика сравниваются с равными до operator==.

Выделение, сделанное одним, может быть освобождено другим.


Вышеупомянутое может быть глупым similie, но представьте, что у нас есть распределитель, который регистрирует каждое распределение, и мы используем его в контейнере где-то в нашем коде, который нас интересует; мы позже хотим вывести элементы из этого контейнера в другой, но мы больше не заинтересованы во всех этих журналах.

Без разделителей с состоянием и возможность отключить propagate_on_container_*, мы будем вынуждены либо 1) скопировать каждый задействованный элемент 2) застрять с записью (более не требуемой).

Ответ 2

Это не так, что стандарт позволяет propagate_on_container_swap вызывать поведение Undefined, но стандарт предоставляет Undefined поведение через это значение!


Простым примером является рассмотрение распределенного распределителя, который выделяет память из локального пула и который пул удаляется, когда распределитель выходит из области видимости:

template <typename T>
class scoped_allocator;

Теперь давайте использовать его:

int main() {
    using scoped = scoped_allocator<int>;

    scoped outer_alloc;
    std::vector<int, scoped> outer{outer_alloc};

    outer.push_back(3);

    {
        scoped inner_alloc;
        std::vector<int, scoped> inner{inner_alloc};

        inner.push_back(5);

        swap(outer, inner); // Undefined Behavior: loading...
    }

    // inner_allocator is dead, but "outer" refers to its memory
}