Является ли использование результата new char[] или malloc для приведения float * UB (строгим нарушением псевдонимов)?

Какой из них имеет UB (в частности, который нарушает строгое правило псевдонимов)?

void a() {
    std::vector<char> v(sizeof(float));
    float *f = reinterpret_cast<float *>(v.data());
    *f = 42;
}

void b() {
    char *a = new char[sizeof(float)];
    float *f = reinterpret_cast<float *>(a);
    *f = 42;
}

void c() {
    char *a = new char[sizeof(float)];
    float *f = new(a) float;
    *f = 42;
}

void d() {
    char *a = (char*)malloc(sizeof(float));
    float *f = reinterpret_cast<float *>(a);
    *f = 42;
}

void e() {
    char *a = (char*)operator new(sizeof(float));
    float *f = reinterpret_cast<float *>(a);
    *f = 42;
}

Я спрашиваю об этом из-за этого вопроса.

Я думаю, что d не имеет UB (иначе malloc был бы бесполезен в C++). И поэтому кажется логичным, что у b, c и e его тоже нет. Я где-то ошибся? Может b это УБ, а c нет?


person geza    schedule 26.10.2017    source источник
comment
псевдоним обычно является проблемой только тогда, когда вы продолжаете использовать оба указателя после псевдонима. Я бы не подумал, что какой-либо из них генерирует предупреждение.   -  person Garr Godfrey    schedule 26.10.2017
comment
IIRC, типы char освобождены от проблемы строгого псевдонима. Однако выравнивание все еще остается проблемой.   -  person molbdnilo    schedule 26.10.2017
comment
Я думаю, что @GarrGodfrey указывает, что все современные компиляторы имеют последовательно предсказуемое поведение в сценариях в OP. И эти строгие нарушения алиасинга действительно проявляются только тогда, когда в игру может вступить Load-Hit-Store. Но полагаться на это ни в коем случае нельзя.   -  person Frank    schedule 26.10.2017
comment
@molbdnilo: вы можете использовать char * для проверки любого значения. Обратное не обязательно верно.   -  person geza    schedule 26.10.2017
comment
связанные: stackoverflow.com/questions /46909105/   -  person W.F.    schedule 26.10.2017
comment
@geza О, верно. Итак, ИРИ.   -  person molbdnilo    schedule 26.10.2017
comment
Я считаю, что все они недействительны: все, кроме c(), недопустимо, потому что вам нужно использовать размещение new для создания объекта, а не только reinterpret_cast, и все от a() до c() недействительны из-за выравнивания. Хотя я не настолько уверен ни в том, ни в другом. Допустимой версией будет void f() { char *a = (char*)malloc(sizeof(float)); float *f = new(a) float; *f = 42; }; и malloc, и operator new из стандартной библиотеки гарантированно имеют достаточное выравнивание.   -  person Daniel H    schedule 26.10.2017
comment
иначе malloc был бы бесполезен в C++ malloc бесполезен. Std C++ сломан и бесполезен! (в строгом толковании, если отказаться от фактов, показывающих, что некоторые главы - фигня и отбрасываются)   -  person curiousguy    schedule 27.10.2017
comment
@curiousguy: Может быть, это языковой барьер, но я не совсем тебя понимаю. Вы имеете в виду, что malloc бесполезен или слишком резок? Перечитывая мой вопрос, да, я согласен с этим. Не бесполезный, но очень громоздкий в использовании. Может быть, мне следует удалить этот абзац, поскольку вопрос стоит сам по себе без него.   -  person geza    schedule 27.10.2017
comment
@geza, вы имеете в виду, что выражение char можно использовать для проверки любого значения. Выражение char может быть получено путем разыменования выражения char *, хотя есть и другие способы. Выражение char * нельзя использовать напрямую для проверки любого значения.   -  person M.M    schedule 27.10.2017
comment
@geza Либо вы отклоняете некоторые стандартные разделы как BS, либо вы должны признать, что исторически принятый код C / C ++, использующий malloc, недействителен. C/C++ мертв!   -  person curiousguy    schedule 27.10.2017


Ответы (3)


Преамбула: хранилище и объекты — это разные понятия в C++. Хранилище относится к пространству в памяти, а объекты – это сущности с продолжительностью жизни, которые могут создаваться и уничтожаться в пределах части хранилища. Хранилище может быть повторно использовано для размещения нескольких объектов с течением времени. Все объекты требуют хранения, но может быть хранилище без объектов.


c правильно. Placement-new — это один из допустимых методов создания объекта в хранилище (C++14 [intro.object]/1), даже если в этом хранилище уже существовали объекты. Старые объекты неявно уничтожаются при повторном использовании хранилища, и это совершенно нормально, если у них нет нетривиальных деструкторов ([basic.life]/4). new(a) float; создает объект типа float и динамической длительности хранения в существующем хранилище ([expr.new]/1).

d и e не определены из-за упущения в текущих правилах объектной модели: эффект доступа к памяти через выражение glvalue определяется только тогда, когда это выражение ссылается на объект; а не когда выражение относится к хранилищу, не содержащему объектов. (Примечание: просьба не оставлять неконструктивных замечаний по поводу очевидной неадекватности существующих определений).

Это не означает, что «malloc бесполезен»; эффект malloc и operator new заключается в получении хранилища. Затем вы можете создавать объекты в хранилище и использовать эти объекты. На самом деле именно так работают стандартные распределители и выражение new.

a и b являются строгими нарушениями псевдонимов: значение gl типа float используется для доступа к объектам несовместимого типа char. ([базовый.lval]/10)


Есть предложение, которое сделает все случаи четко определены (кроме выравнивания a, упомянутого ниже): в соответствии с этим предложением использование *f неявно создает объект этого типа в местоположении с некоторыми оговорками.


Примечание. В случаях с b по e проблем с выравниванием не возникает, потому что new-expression и ::operator new гарантированно выделяют память, правильно выровненную для любого типа ([new.delete. сингл]/1).

Однако в случае std::vector<char>, несмотря на то, что стандарт указывает, что ::operator new вызывается для получения памяти, стандарт не требует, чтобы первый элемент вектора помещался в первый байт этой памяти; например вектор может решить выделить 3 дополнительных байта впереди и использовать их для некоторой бухгалтерии.

person M.M    schedule 26.10.2017
comment
@Т.С. спасибо, обновил мой ответ в соответствии с вашим комментарием. Существуют ли какие-либо существующие реализации, которые добавляют отступы? - person M.M; 27.10.2017
comment
Я так не думаю (реализация может предположительно спрятать размер/емкость в выделенном блоке или что-то в этом роде, но вряд ли это будет выгодно), но мы находимся в стране языковых юристов :) - person T.C.; 27.10.2017
comment
a и b также допустимы в соответствии с правилами P0593R1 (по модулю теоретической проблемы выравнивания с a). Объект float возник бы как раз вовремя, чтобы запись стала четко определенной. - person T.C.; 27.10.2017
comment
@Т.С. Я не совсем понимаю это из предложения: в нем говорится, что объекты могут возникать там, где не было реальных объектов, но в случаях (a), (b) хранилище уже включает реальные char объекты. Пример в разделе 1.2 предложения работает только в пространстве, выделенном внешним API (и, следовательно, может быть хранилищем без реальных объектов). Но пример в 2.3 допускает появление int там, где были настоящие char, мне кажется, что потребуются дополнительные разъяснения. (Например, если вы поместите int в буфер short, сможете ли вы по-прежнему получить доступ к short?) - person M.M; 27.10.2017
comment
Предлагаемое правило, которое есть в 2.2, довольно простое. Объекты неявного жизненного цикла возникают по мере необходимости, чтобы придать программе определенное поведение. В вашем примере int/short вы сможете писать в shorts (возникая новые объекты short непосредственно перед записью), но не читать из них (поскольку любой новый объект short будет иметь неопределенные значения). - person T.C.; 27.10.2017

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

Я считаю, что все они, за исключением c(), содержат строгие нарушения псевдонимов, как формально определено стандартом.

Я основываюсь на разделе 1.8.1 документа стандарт

... Объект создается определением (3.1), новым выражением (5.3.4) или реализацией (12.2), когда это необходимо. ...

reinterpret_cast<>ing память не подпадает ни под один из этих случаев.

person Frank    schedule 26.10.2017
comment
reinterpret_cast никогда не создает новый объект, но это не означает, что результирующий указатель или ссылка всегда небезопасны для использования. Если результат имеет тип, которому разрешено использовать псевдоним исходного объекта, все в порядке. Например, вы можете интерпретировать unsigned int как int. Объект int никогда не создавался, но использование результата приведения четко определено. Вы законно используете int, несмотря на то, что int никогда не создавалось. Вопрос здесь в том, можете ли вы использовать массив символов как float. Псевдонимы типов - person François Andrieux; 26.10.2017
comment
Я считаю, что ваш вывод верен, но я не верю, что предоставленный вами отрывок объясняет, почему. - person François Andrieux; 26.10.2017
comment
Ну, я собирался указать на 3.10.10, но это применимо только в том случае, если вы можете установить, что в данный момент в этом месте памяти нет объекта float. Так что я все еще считаю, что основной причиной является 1.8.1. - person Frank; 26.10.2017
comment
@FrançoisAndrieux В новейшем стандарте С++ не говорится, что подписанные и неподписанные могут иметь псевдонимы, но похожие значения имеют одинаковое представление. Так что можно ожидать, что в ближайшем будущем компиляторы перестанут считать указатель на unsigned int псевдонимом указателя на int. - person Oliv; 26.10.2017

Из cppreference:

Псевдоним типа

Всякий раз, когда делается попытка прочитать или изменить сохраненное значение объекта типа DynamicType через значение gl типа AliasedType, поведение не определено, если не выполняется одно из следующих условий:

  • AliasedType и DynamicType похожи.
  • AliasedType — это подписанный или неподписанный вариант DynamicType (возможно, с указанием cv).
  • AliasedType — это std::byte, (начиная с C++17)char или unsigned char: это позволяет исследовать объектное представление любого объекта в виде массива байтов.

Неформально два типа похожи, если после удаления cv-квалификации на каждом уровне (но исключая все, что находится внутри функционального типа) они относятся к одному и тому же типу.

Например: [...некоторые примеры...]

Также cppreference:

glvalue — это выражение, оценка которого определяет идентичность объекта, битового поля или функции;

Вышеизложенное относится ко всем примерам, кроме (c). Типы не являются ни похожими, ни подписанными/беззнаковыми вариантами. Также AliasedType (тип, к которому вы приводите) не является ни char, ни unsigned char, ни std::byte. Следовательно, все они (кроме c) демонстрируют неопределенное поведение.

Отказ от ответственности: Прежде всего, cppreference не является официальной ссылкой, а только стандартом. Во-вторых, к сожалению, я даже не уверен на 100%, верна ли моя интерпретация того, что я прочитал на cppreference.

person 463035818_is_not_a_number    schedule 26.10.2017
comment
В случае c нет функции reinterpret_cast. - person François Andrieux; 26.10.2017
comment
@FrançoisAndrieux new(a) float; не glvalue? Я не уверен... Я не упоминаю reinterpret_cast - person 463035818_is_not_a_number; 26.10.2017
comment
@ tobi303 tobi303 См. Мой ответ на этот вопрос, раздел 1.8.1 устанавливает, что новое выражение создает объект, делая его glvalue. - person Frank; 26.10.2017
comment
Вы упоминаете reinterpret_cast в URL-адресе в первой строке своего ответа, а первая цитата блока цитирования - это документация. - person François Andrieux; 26.10.2017
comment
@FrançoisAndrieux там объясняется строгое использование псевдонимов, и я думаю, что объяснение носит более общий характер и относится не только к переосмыслению приведения. - person 463035818_is_not_a_number; 26.10.2017
comment
В (c) new(a) float; создает объект, динамический тип которого равен float. - person M.M; 27.10.2017