Я просто продолжаю ответ Kerrek SB в надежде объяснить проблему более подробно (и, следовательно, более убедительно). В принятой редакции 3 рассматриваемого документа (P0083R3) упоминается головоломка std::pair<const Key, Mapped>
против std::pair<Key, Mapped>
, и что «Преобразование между ними может быть безопасно осуществлено с использованием метода, аналогичного используемому * 1005». * при извлечении и повторной вставке. "
IOW, извлечение и повторная вставка защищены от оптимизаций, связанных с указанием типов, вызывая «магию реализации» (это явная формулировка в документе), чтобы разрешить любые возможные проблемы с наложением в самом коде контейнера, а также как пользователь Кодекс соблюдает указанные вами ограничения.
Возникает вопрос, почему это «волшебство» не может быть расширено, чтобы охватить также случаи, когда пользовательский код обращается к элементу Mapped
диссоциированного узла через указатель, который был получен, пока узел все еще принадлежал контейнеру. Причина этого заключается в том, что объем реализации такой «магии» значительно больше, чем объем реализации ограниченной магии, которая применяется только к извлечению и вставке узла.
Рассмотрим, например, тривиально эту функцию:
int f(std::pair<const int, int> &a, const std::pair<int, int> &b)
{
a.second = 5;
return b.second;
}
Согласно ограничениям на псевдонимы типов, реализация может предполагать, что a
и b
не могут находиться в одной и той же ячейке памяти. Поэтому в реализации также допускается, что a.second
и b.second
не находятся в одной и той же ячейке памяти, , даже если они имеют один и тот же тип . Следовательно, реализация имеет право на некоторые базовые свободы генерации кода, такие как выполнение загрузки b.second
перед сохранением до a.second
без необходимости фактического сравнения адресов a
и b
вначале.
Теперь предположим, что ограничения на соединение карт были не такими, как вы упомянули. Тогда можно будет сделать следующее:
int g()
{
std::map<int, int> m{{1, 1}};
auto &r = m[1];
auto node = m.extract(1);
return f(r, node.value());
}
Из-за ограничений типа наказания, очевидно, это UB. Подожди, я знаю, ты хочешь протестовать, потому что:
-
node_type
для std::map
не имеет value()
метода.
- Даже если это так,
node_type
должно std::launder
(или что-то примерно эквивалентное) значению.
Тем не менее, эти пункты не обеспечивают фактического средства правовой защиты. Что касается первого пункта, рассмотрим этот незначительный вариант:
int f(std::pair<const int, int> &a, const int &b)
{
a.second = 5;
return b;
}
int g()
{
std::map<int, int> m{{1, 1}};
auto &r = m[1];
auto node = m.extract(1);
return f(r, node.mapped());
}
Теперь оптимизация глазка в f
не дает компилятору достаточной информации, чтобы исключить алиасы. Однако, давайте предположим, что компилятор может встраивать как node.mapped()
(и, следовательно, компилятор может установить, что он возвращает ссылку на second
из std::pair<int, int>
), так и f
. Внезапно компилятор может снова почувствовать себя вправе опасной оптимизации.
А как же отмывание? Прежде всего, это здесь не применимо, потому что информация, касающаяся extract
и отмывания, сделанного в нем, может вообще отличаться от единицы перевода node_type::mapped()
. Это необходимо подчеркнуть: с жесткими ограничениями, которые были стандартизированы, отмывание может быть сделано во время извлечения, это не должно быть сделано при каждом вызове value()
, что также ясно является намерением, выраженным в Цитата я предоставил в начале. Основная проблема, однако, заключается в том, что отмывание не может предотвратить UB здесь, даже если это было сделано внутри node_type::mapped()
. Фактически, следующий код имеет неопределенное поведение (пример предполагает, что sizeof(int) <= sizeof(float)
):
float g()
{
float value = 0.0f; // deliberate separate initialization, see below
value = 3.14f;
int *intp = std::launder(reinterpret_cast<int *>(&value));
*intp = 1;
return value + *intp;
}
Это потому, что с использованием std::launder
вообще не дает пользователю права печатать punning . Вместо этого std::launder
позволяет повторно использовать память value
только путем установления зависимости продолжительности жизни между float
, который изначально находится в &value
, и int
, который живет там после std::launder
. На самом деле, что касается стандарта, value
и *intp
не могут быть живы одновременно точно, потому что они имеют несовместимые с указателем типы и одну и ту же ячейку памяти.
(Здесь std::launder
добивается, например, предотвращения переупорядочения от value = 3.14f;
до *intp = 1
. Проще говоря, компилятору не разрешается переупорядочивать записи после std::launder
и читать из отмытый указатель до std::launder
, если только он не может доказать, что области памяти на самом деле не перекрываются, и это действительно так, даже если речь идет о несовместимых с указателем типах. Я использовал отдельное назначение, чтобы я мог прояснить этот момент .)
В конечном итоге все сводится к тому, что для безопасной поддержки предполагаемого использования реализациям придется добавить дополнительную магию поверх того, что упомянуто в статье (и последнее в значительной степени уже реализовано). потому что это по крайней мере очень похоже на эффекты std::launder
). Это не только вызвало бы дополнительные усилия, но также могло бы иметь побочный эффект, заключающийся в предотвращении определенных оптимизаций в тех случаях, когда пользователь добровольно соблюдает установленные ограничения. Подобные суждения принимаются постоянно при стандартизации C ++ или почти во всем, когда эксперты пытаются сопоставить прогнозируемые затраты с определенными или, по крайней мере, вероятными выгодами. Если вам все еще нужно знать больше, вам, вероятно, придется обратиться к некоторым членам CWG напрямую, потому что именно здесь был сделан запрос на применение этих ограничений, как указано в документе, связанном выше.
Надеюсь, это поможет немного прояснить ситуацию, даже если вы все еще не согласны с принятым решением.
В качестве последнего замечания я настоятельно рекомендую вам посмотреть некоторые из замечательных выступлений на C ++ UB, если вы еще этого не сделали, например, Неопределенное поведение - это удивительно от Петра Падлевского или Garbage В, Мусор Out ... Чендлер Кэррут.