Политики распространения Allocator в ваших новых современных контейнерах C ++ - PullRequest
0 голосов
/ 15 февраля 2019

В чем причина наличия этих признаков в контейнере (https://en.cppreference.com/w/cpp/memory/allocator_traits)

propagate_on_container_copy_assignment  Alloc::propagate_on_container_copy_assignment if present, otherwise std::false_type
propagate_on_container_move_assignment  Alloc::propagate_on_container_move_assignment if present, otherwise std::false_type
propagate_on_container_swap             Alloc::propagate_on_container_swap if present, otherwise std::false_type
is_always_equal(since C++17)            Alloc::is_always_equal if present, otherwise std::is_empty<Alloc>::type

Я понимаю, что реализация контейнера будет вести себя так или иначе при реализации присваивания и перестановки(и что обработка этого случая - ужасный код.) Я также понимаю, что иногда может потребоваться оставить контейнер move-from в состоянии, равном resizeble или что может быть вызвано хотя бы какое-то последнее освобождение, поэтомуРаспределитель нельзя оставлять недействительным. (Лично я считаю, что это слабый аргумент.)

Но вопрос в том, Почему эта информация уже не может быть частью обычной реализации, семантикисам пользовательский тип распределителя?

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

Таким же образом, вместо использования is_always_equal, можно фактически сделать всеприсвоение локатора ничего не делает.

(Кроме того, если is_always_equal истинно, можно сделать operator== для распределителей, возвращающих std::true_type, чтобы сигнализировать об этом.)

Мне кажется, что эти чертыпохоже, что переопределяет семантику, которую можно дать пользовательскому распределителю обычными средствами C ++.Кажется, это играет против общего программирования и современной философии C ++.

Единственная причина, я могу подумать об этом, может быть полезна для выполнения некоторой обратной совместимости со «старыми» контейнерами.

Если бы я сегодня написал новый контейнер и / или новый нетривиальный распределитель, могу ли я рассчитывать на семантику распределителя изабыть об этих чертах?

На мой взгляд, до тех пор, пока удаленный распределитель может "освобождать" состояние нулевого указателя (что в большинстве случаев означает ничего не делать), тогда это должно бытьотлично, и если resize выбрасывает, что тоже хорошо (верно), это просто означает, что распределитель больше не имеет доступа к своей куче.


РЕДАКТИРОВАТЬ: Впрактические термины, Могу ли я написать контейнеры просто так ?и делегировать сложность семантике пользовательских распределителей?:

templata<class Allocator>
struct my_container{
  Allocator alloc_;
  ...
  my_container& operator=(my_container const& other){
    alloc_ = other.alloc_; // if allocator is_always_equal equal this is ok, if allocator shouldn't propagate on copy, Alloc::operator=(Alloc const&) simply shouldn't do anything in the first place
    ... handle copy...
    return *this;
  }
  my_container& operator=(my_container&& other){
    alloc_ = std::move(other.alloc_); // if allocator shouldn't propagate on move then Alloc::operator=(Alloc&&) simply shouldn't do anything.
    ... handle move...
    return *this;
  }
  void swap(my_container& other){
     using std::swap;
     swap(alloc, other.alloc); //again, we assume that this does the correct thing (including not actually swapping anything if that is the desired criteria. (that would be the case equivalent to `propagate_on_container_swap==std::false_type`)
     ... handle swap...
  }
}

Я думаю, что единственное истинное требование к распределителю - это то, что распределитель, перемещенный из, должен быть в состоянии сделать это.

my_allocator a2(std::move(a1));
a1.deallocate(nullptr, 0); // should ok, so moved-from container is destructed (without exception)
a1.allocate(n); // well defined behavior, (including possibly throwing bad_alloc).

Ответы [ 2 ]

0 голосов
/ 24 февраля 2019

Николь Болас очень точный ответ.Я бы сказал так:

  • Распределитель - это дескриптор кучи. Это семантический тип значения, такой же, как указатель или intили string.Когда вы копируете распределитель, вы получаете копию его значения.Копии сравниваются равные.Это работает для распределителей точно так же, как это работает для указателей или int s или string s.

  • Одна вещь, которую вы можете сделать с распределителем, это передать еговокруг различных алгоритмов и структур данных, используя семантику чистой стоимости.STL не имеет много в этом отделе, но он имеет, например, allocate_shared.

  • Еще одна вещь, которую вы можете сделать сраспределитель передать его в контейнер STL.Вы передаете распределитель контейнеру во время строительства контейнера.В определенные моменты в течение своего жизненного цикла контейнер встретит других распределителей , и ему придется сделать выбор.


A<int> originalAlloc = ...;
std::vector<int, A<int>> johnny(originalAlloc);

A<int> strangeAlloc = ...;
std::vector<int, A<int>> pusher(strangeAlloc);

// pssst kid wanna try my allocator? it'll make you feel good
johnny = std::move(pusher);

На этом этапе johnny должен принять жесткое решение: «Я принимаю значения pusher элементов, что касается моего значения ; я должен также принять его распределитель?"

Способ, которым johnny принимает решение в C ++ 11 и более поздних версиях, - обратиться к allocator_traits<A<int>>::propagate_on_container_move_assignment и сделать то, что он говорит: если он говорит true, тогда мы примем strangeAllocи если он скажет false, мы будем придерживаться наших принципов и придерживаться нашего исходного распределителя.Использование нашего исходного распределителя означает, что нам, возможно, придется проделать кучу дополнительной работы, чтобы сделать копии всех элементов pusher (мы не можем просто украсть его указатель данных, потому что он указывает на кучу, связанную с strangeAllocа не куча, связанная с originalAlloc).

Суть в том, что решение придерживаться вашего текущего распределителя или принять новый - это решение, которое имеет смысл только в контексте контейнера .Вот почему признаки propagate_on_container_move_assignment (POCMA), а также POCCA и POCS имеют "контейнер" в названии.Речь идет о том, что происходит при назначении контейнера , а не при назначении allocator .Назначение распределителя следует семантике значения, потому что распределители являются семантическими типами значения.Period.

Итак, должны ли propagate_on_container_move_assignment (POCMA) и POCCA и POCS быть атрибутами типа container ?Должны ли мы иметь std::vector<int>, который беспорядочно принимает распределители, и std::stickyvector<int>, который всегда придерживается распределителя, с которым он был создан?Ну, наверное.

C ++ 17 вроде как делает вид, что мы сделали так и сделали, предоставив typedefs вроде std::pmr::vector<int>, которые очень похожи на std::stickyvector<int>;но под капотом std::pmr::vector<int> это просто typedef для std::vector<int, std::pmr::polymorphic_allocator<int>> и все еще выясняет, что делать, консультируясь с std::allocator_traits<std::pmr::polymorphic_allocator<int>>.

0 голосов
/ 21 февраля 2019

Я имею в виду, что назначение-копирование контейнера может попытаться скопировать-назначить исходный распределитель, и если это синтаксическое назначение-копия на самом деле не копирует, то, ну, это все равно, что сказать, что ваш контейнер не делаетt propagate_on_container_copy_assignment.

Концепция / именованное требование " CopyAssignable " означает нечто большее, чем просто возможность присвоить lvalue объекту того же типа, что иэто значениеОн также имеет семантическое значение: ожидается, что целевой объект будет эквивалентен по значению исходному объекту.Если ваш тип предоставляет оператор присваивания, ожидается, что этот оператор копирует объект.И почти все, что есть в стандартной библиотеке, которая позволяет назначать копии, требует этого.

Если вы задаете для стандартной библиотеки тип, который должен быть CopyAssignable, и у нее есть оператор назначения копирования, который не соблюдаетсемантическое значение этого понятия / именованного требования, неопределенные результаты поведения.

Распределитель имеет своего рода «значение».И копирование распределителя копирует это «значение».Вопрос, который распространяется при копировании / перемещении / замене, в основном задает этот вопрос: является ли значение распределителя частью значения контейнера?Этот вопрос возникает только в связи с контейнерами;при работе с распределителями вообще вопрос спорный.Распределитель имеет значение, и его копирование копирует это значение.Но что это означает относительно ранее выделенного хранилища - это совершенно отдельный вопрос.

Отсюда и черта.

Если бы я написал новый контейнер и /или новый нетривиальный распределитель сегодня могу ли я рассчитывать на семантику распределителя и забыть об этих чертах?

...

Могу ли я написать контейнеры просто так ?и делегировать сложность семантике пользовательских распределителей?:

Контейнер, который нарушает правила AllocatorAwareContainer , не является контейнером, поддерживающим распределитель, и вы не можете разумно передавать распределителик нему, которые следуют стандартной модели распределителя библиотеки.Точно так же распределитель, который нарушает правила Allocator , не может быть разумно передан AllocatorAwareContainer, поскольку эта модель требует, чтобы распределитель действительно был Allocator.Это включает синтаксические и семантические правила.

Если вы не предоставите значения для свойств propagate_on_*, будет использоваться значение по умолчанию false.Это означает, что он не будет пытаться распространять ваш распределитель, так что вы не столкнетесь с необходимостью того, что распределитель может быть назначен для копирования / перемещения или для замены.Однако это также означает, что поведение вашего распределителя копирования / перемещения / обмена никогда не будет использоваться, поэтому не имеет значения, какую семантику вы задаете этим операциям.Кроме того, без распространения, если два распределителя были неравны, это означает линейное перемещение / своп во времени.

Однако AllocatorAwareContainer по-прежнему не разрешено игнорировать эти свойства, так как по определению ондолжны реализовать их, чтобы взять на себя эту роль.Если распределитель определяет оператор присваивания копии, но делает его распространение при копировании ложным (что является абсолютно допустимым кодом), вы не можете вызывать оператор присвоения копии распределителя при назначении копирования контейнеру.

По сути, единственный способ выполнить эту работу - это жить в своей собственной вселенной, где вы используете свои «контейнеры» только с вашими «распределителями» и никогда не пытаетесь использовать какие-либо эквиваленты стандартной библиотеки с вашими «контейнерами / распределителями».


Исторический обзор также может быть полезен.

В исторических целях функции propagate_on_* имели историю изменений до C ++ 11, но это никогда не появилось, как вы предлагаете.

Самая ранняя статья по этому вопросу, которую я могу найти, это N2525 (PDF): специфичное для распределителя поведение Swap и Move * .Основная цель этого механизма состоит в том, чтобы позволить определенным классам итераторов с отслеживанием состояния иметь возможность выполнять операции перемещения и обмена в постоянное время.

Это было некоторое время включено в концептуальную версию, но однажды это былоудаленный из C ++ 0x, он вернулся к классу признаков с новым именем и более упрощенным интерфейсом (PDF) (да, интерфейс, который вы используете сейчас, - простая версия . Не за что;)).

Итак, во всех случаях было явное признание того, что необходимо проводить различие между существованием копирования / перемещения / обмена и смыслом этих операций суважение к контейнеру.


и что обработка этих дел - ужасный код.

Но это не так.В C ++ 17 вы просто используете if constexpr.В старой версии вы должны полагаться на SFINAE, но это просто означает написание таких функций:

template<typename Alloc>
std::enable_if_t<std::allocator_traits<Alloc>::propagate_on_container_copy_assignment::value> copy_assign_allocator(Alloc &dst, const Alloc &src)
{
  dst = src;
}

template<typename Alloc>
std::enable_if_t<!std::allocator_traits<Alloc>::propagate_on_container_copy_assignment::value> copy_assign_allocator(Alloc &dst, const Alloc &src) {}

Наряду с версиями для перемещения и обмена.Затем просто вызовите эту функцию, чтобы выполнить копирование / перемещение / обмен или не выполнять копирование / перемещение / обмен, в соответствии с поведением при распространении.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...