обработка памяти, возвращаемой оператором new (sizeof (T) * N) как массив

В C можно выделять динамические массивы с помощью malloc(sizeof(T) * N), а затем использовать арифметику указателей для получения элементов со смещением i в этом динамическом массиве.

В C ++ можно сделать то же самое, используя operator new() так же, как malloc(), а затем разместить new (например, можно увидеть решение для пункта 13 в книге Херба Саттера «Исключительный C ++: 47 инженерных головоломок, проблем программирования и решений») . Если у вас его нет, краткое изложение решения этого вопроса будет следующим:

T* storage = operator new(sizeof(T)*size);

// insert element    
T* p = storage + i;
new (p) T(element);

// get element
T* element = storage[i];

Для меня это выглядело законным, поскольку я прошу кусок памяти с достаточным объемом памяти для хранения N выровненных элементов размером = sizeof(T). Поскольку sizeof(T) должен возвращать размер элемента, который выровнен, и они размещаются один за другим в блоке памяти, использование арифметики указателей здесь нормально.

Однако затем мне указали на такие ссылки: http://eel.is/c++draft/expr.add#4 или http://eel.is/c++draft/intro.object#def:object и утверждая, что в C ++ operator new() не возвращает объект массива, поэтому арифметика указателя над тем, что он вернул, и использование его в качестве массива является неопределенным поведением в отличие от ANSI C.

Я не настолько хорош в таких низкоуровневых вещах, и я действительно пытаюсь понять, читая это: https://www.ibm.com/developerworks/library/pa-dalign/ или это: http://jrruethe.github.io/blog/2015/08/23/Placement-new/, но я до сих пор не могу понять, был ли Саттер просто просто неправильно?

Я понимаю, что alignas имеет смысл в таких конструкциях, как:

alignas(double) char array[sizeof(double)];

(c) http://georgeflanagin.com/alignas.php

Если кажется, что массив не находится в пределах double (возможно, после char в структуре, запущенной на 2-байтовом процессоре чтения).

Но это другое дело - я запросил память из кучи / свободного хранилища, особенно запросил оператор new для возврата памяти, которая будет содержать элементы, выровненные по sizeof(T).

Подводя итог, если это был TL; DR:

  • Можно ли использовать malloc() для динамических массивов в C ++?
  • Можно ли использовать operator new() и новое размещение для динамических массивов в более старом C ++, в котором нет ключевого слова alignas?
  • Не определено ли поведение арифметики указателя при использовании в памяти, возвращаемой operator new()?
  • Саттер советует код, который может сломаться на какой-нибудь старинной машине?

Извините, если это глупо.


person xor256    schedule 23.11.2018    source источник
comment
Можно ли использовать malloc () для динамических массивов в C? - вы хотели написать C ++?   -  person Swordfish    schedule 23.11.2018
comment
да. И я хотел понять, в чем разница между malloc в C и C ++ в этом случае. Поскольку я давно видел, что оператор new () реализован в терминах malloc () в некоторых заголовках C ++ в какой-то версии GCC, поэтому оператор new () будет просто эквивалентен malloc (), поэтому могу ли я использовать над ним арифметику указателя в C ++ без выравнивания? Я совершенно запуталась. :(   -  person xor256    schedule 23.11.2018
comment
@ xor256 Я полагаю, что реализация может использовать неопределенное поведение, если она дает больше гарантий для себя. Итак, использует ли GCC что-то в своей библиотечной реализации (даже при условии, что она не содержит ошибок и придерживается стандартов) ничего не говорит вам о том, определено ли это поведение в стандарте.   -  person    schedule 23.11.2018
comment
Вы можете поместить сюда весь код? Потому что представленный вами код даже не компилируется (по крайней мере, в первой строке отсутствует приведение).   -  person geza    schedule 23.11.2018
comment
Для арифметики указателя вам не нужен массив, единственное, что вы должны быть осторожны, чтобы избежать неопределенного поведения, - это не перемещаться за конец указателя (один за вашим выделенным размером).   -  person yyny    schedule 23.11.2018
comment
Мне не удается найти то, что, по моему мнению, является релевантным сообщением. Однако я думаю, что это нормально, потому что я почти уверен, что в стандарте говорится, что один объект рассматривается как массив из одного элемента по отношению к арифметике указателя.   -  person Galik    schedule 23.11.2018
comment
@Galik В нем говорится, что в eel.is/c++draft/expr. добавить # footnote-85. Я не думаю, что указатель считается указывающим на один объект. В выделенной памяти не было построено ни одного объекта.   -  person    schedule 23.11.2018
comment
@eukaryota Да, мне кажется, я неправильно понял вопрос. Неужели вы не думаете, что выражение (possibly-hypothetical) из стандартной формулировки может быть извлечено некоторую выгоду?   -  person Galik    schedule 23.11.2018
comment
Да, как я вижу, в этом коде действительно есть UB. Но, на мой взгляд, исправлять нужно стандарт, а не код Херба. Было бы интересно узнать, почему у нас есть такое ограничивающее правило для арифметики указателей.   -  person geza    schedule 23.11.2018
comment
@geza Может быть, это потому, что определение object было недавно изменено на более ограничительное? Раньше даже неинициализированная память была объектом.   -  person Galik    schedule 23.11.2018
comment
@Galik Я действительно не знаю, но в моем любительском чтении possibly-hypothetical относится к hypothetical x[n] после фактического массива, и квалификационный if из 4.2 также не использует его.   -  person    schedule 23.11.2018
comment
@eukaryota Я думаю, это означает для целей арифметики, пока память выделена и теоретически может стать объектом, тогда арифметика работает. Но трудно быть уверенным   -  person Galik    schedule 23.11.2018
comment
@Galik: Насколько я помню, в C ++ 98 было то же правило, и оно означало то же самое (в том смысле, что код Херба никогда не был четко определен).   -  person geza    schedule 23.11.2018


Ответы (4)


Стандарты C ++ содержат открытую проблему, которая базовое представление объектов - это не «массив», а «последовательность» unsigned char объектов. Тем не менее, все рассматривают его как массив (что и задумано), поэтому можно безопасно писать такой код:

char* storage = static_cast<char*>(operator new(sizeof(T)*size));
// ...
char* p = storage + sizeof(T)*i;  // precondition: 0 <= i < size
new (p) T(element);

пока void* operator new(size_t) возвращает правильно выровненное значение. Использование смещений, умноженных на sizeof, для сохранения выравнивания безопасно.

В C ++ 17 есть макрос STDCPP_DEFAULT_NEW_ALIGNMENT < / a>, который определяет максимальное безопасное выравнивание для "нормального" void* operator new(size_t), и void* operator new(std::size_t size, std::align_val_t alignment) следует использовать, если требуется большее выравнивание.

В более ранних версиях C ++ такого различия нет, а это означает, что void* operator new(size_t) необходимо реализовать таким образом, чтобы это было совместимо с выравниванием любого объекта.

Что касается возможности выполнять арифметические операции с указателями непосредственно на T*, я не уверен, что это требуется в соответствии с требованиями стандарта. Однако сложно реализовать модель памяти C ++ таким образом, чтобы она не работала.

person Kit.    schedule 23.11.2018
comment
CWG 1701 не имеет ничего общего с рассматриваемой проблемой. CWG 1701 посвящена представлению объектов. Проблема с распределением функций в том, что они не создают объекты. Как здесь должно помочь решение вопроса? - person Language Lawyer; 10.01.2019
comment
все рассматривают это как массив (который предназначен) Откуда это, что предполагается? Примечание к проблеме: Был поднят дополнительный вопрос о том, уместно ли ссылаться на составляющие байты объекта как на сами «объекты». Как это должно быть массивом, когда его элементы не должны быть объектами? - person Language Lawyer; 10.01.2019
comment
@LanguageLawyer, это неверно, что функции распределения не создают объекты. См. стандарт. Предназначен авторами языка, что следует из того, как они (и все остальные) используют такие конструкции; если сомневаетесь, вы можете спросить их напрямую, их электронные письма не являются секретными. - person Kit.; 10.01.2019
comment
@Набор. См. стандарт. Он не говорит, что объект создан, он говорит, что его время жизни началось. См. стандарт при создании объекта. - person Language Lawyer; 10.01.2019
comment
@LanguageLawyer, то при создании объекта оказывается соломинка. Семантически нет никакой разницы, создает ли функция распределения или обращается к массиву байтов. - person Kit.; 10.01.2019
comment
@Набор. Нет, это не соломинка. Это предназначено. Ваше (очень популярное среди людей) неправильное толкование правила начала срока службы, согласно которому мириады объектов волшебным образом появляются в хранилище подходящего размера и выравнивания, явно противоречит нескольким правилам, например, когда объекты в течение своей жизни могут иметь один и тот же адрес, что показывает, что такие толкование не предназначалось Комитетом. - person Language Lawyer; 10.01.2019
comment
@LanguageLawyer, нет, это ваша интерпретация, что они появляются волшебным образом. В стандарте четко указано, что их срок службы начинается с момента получения хранилища с правильным выравниванием и размером. Если вы считаете, что это противоречит чему-то еще в стандарте, отправьте отчет о дефекте. - person Kit.; 11.01.2019
comment
@Набор. В стандарте четко указано, что их время жизни начинается с момента получения хранилища с правильным выравниванием и размером. Ага. Когда объект создается, первым делом получается хранилище для него. И если нет непустой инициализации, запускается время жизни создаваемого объекта. Это правильная интерпретация правила. - person Language Lawyer; 11.01.2019
comment
@Набор. В любом случае, здесь - это предложение члена комитета, который поддерживает статус-кво, что только malloc не является достаточно для создания объекта. Вы сказали , что функции распределения не создают объекты - это неправда. Предназначено авторами языка. Как видим, не предполагалось. - person Language Lawyer; 11.01.2019
comment
@LanguageLawyer, C ++ был создан и развивается как язык, одним из самых сильных аргументов которого является способность работать с объектами POD, не созданными языковыми конструкциями (от аппаратных регистров до данных в файлах, отображаемых в память, на объекты, созданные в том же процессе кодом, написанным на другом языке). Если бы Комитет однажды решил запретить эту способность, такое глупое решение вызвало бы массовый протест в отрасли, который невозможно было бы пропустить. - person Kit.; 12.01.2019
comment
@Набор. Если бы объекты волшебным образом появились в любом подходящем хранилище, тогда с кодом в сообщении OP не было бы проблем, потому что там появился бы массив беззнаковых символов, охватывающий всю часть выделенного хранилища. Но это не так. См. Предложение в ответе с самым высоким рейтингом. - person Language Lawyer; 12.01.2019
comment
@LanguageLawyer, код в сообщении OP не имеет проблем, за исключением тех, которые, возможно, относятся к STDCPP_DEFAULT_NEW_ALIGNMENT. Что вызывает проблемы с этим кодом, так это интерпретация, что объекты в текущем C ++ могут начать свое время жизни только в результате создания объектов языковых конструкций. Хотя изменение языка таким образом, чтобы эта интерпретация стала правильной, может показаться хорошей идеей, это может без необходимости нарушить большой объем существующего кода, особенно в автономных реализациях, не дав взамен ничего полезного. - person Kit.; 12.01.2019

Проблема арифметики указателя на выделенную память, как в вашем примере:

T* storage = static_cast<T*>(operator new(sizeof(T)*size));
// ...
T* p = storage + i;  // precondition: 0 <= i < size
new (p) T(element);

технически неопределенное поведение известно давно. Это означает, что std::vector не может быть реализован с четко определенным поведением только как библиотека, но требует дополнительных гарантий от реализации, помимо тех, которые содержатся в стандарте.

Комитет по стандартам определенно не намеревался сделать std::vector невыполнимым. Саттер, конечно, прав в том, что такой код предназначен для четкого определения. Формулировка стандарта должна это отражать.

P0593 - это предложение, которое, если оно будет принято в стандарте, может решить эту проблему. А пока можно продолжать писать код, подобный приведенному выше; ни один крупный компилятор не будет рассматривать его как UB.

Изменить: Как указано в комментариях, я должен был сказать, что, когда я сказал, что storage + i будет четко определен в P0593, я предполагал, что элементы storage[0], storage[1], ..., storage[i-1] уже были был построен. Хотя я не уверен, что понимаю P0593 достаточно хорошо, чтобы сделать вывод, что он также не охватывает случай, когда эти элементы не были уже созданы.

person Brian Bi    schedule 23.11.2018
comment
Хм, а почему тут P0593 актуален? T может быть любого типа. Думаю, это предложение не решит эту проблему. - person geza; 23.11.2018
comment
std::vector не является невыполнимым. Возможно, это невозможно реализовать в пользовательском коде. Стандартная библиотека не может быть реализована в переносимом коде, тем более в коде, написанном пользователем. Это одна из причин, по которой он поставляется с компилятором - он может использовать известное поведение этого компилятора и целевой ОС. - person Pete Becker; 23.11.2018
comment
@PeteBecker Вот что я имел в виду. std::vector не должен был быть невыполнимым в пользовательском коде. - person Brian Bi; 23.11.2018
comment
@geza Я сделал предположение (которое, вероятно, мне следовало заявить), что storage[0], storage[1], ..., storage[i-1] уже построены. В этом случае P0593 подразумевает, что объект массива с i элементами создается неявно, а storage + i находится за концом и, следовательно, четко определен. P0593 указывает, что он предназначен для работы с массивами любого типа. - person Brian Bi; 23.11.2018
comment
Я имел в виду, что P0593 касается типов, которые автор называет неявными типами времени жизни. Таким образом, это предложение не касается всех типов, оно не может быть общим решением этой проблемы. Но и в этом я не уверен на 100% :) - person geza; 23.11.2018
comment
Но в чем причина УБ? Это не арифметика указателей, а этот std :: bless what is? Размещение новое? Можете ли вы свести это к элементарным блокам? - person xor256; 23.11.2018
comment
А как насчет арифметики (uint8_t *)storage? Разве это не было бы четко определенным, позволяющим четко определить vector реализацию? - person HolyBlackCat; 23.11.2018
comment
@HolyBlackCat: Нет. Насколько я знаю, вам нужно reinterpret_cast это uintptr_t и выполнять арифметические операции там. Это, конечно, поведение, определяемое реализацией - person geza; 23.11.2018
comment
@geza Он говорит, что тип массива элемента any является неявным типом времени жизни (независимо от того, является ли тип элемента неявным типом времени жизни). Потому что, если у вас уже есть группа объектов типа T, выстроенных в линию в памяти, то для создания массива T не требуется никакого дополнительного кода. - person Brian Bi; 23.11.2018
comment
Спасибо, это действительно имеет смысл! А теперь пора в 35-й раз перечитать это предложение :) - person geza; 23.11.2018
comment
Я не уверен, следует ли мне создавать новый вопрос, позвольте мне задать его здесь. Предположим, мы не используем значение, возвращаемое new. Можем ли мы использовать storage или p для доступа к элементам? Или мы должны сначала std::launder это сделать? - person Evg; 02.12.2019
comment
@Evg Хороший вопрос. Я думаю, что std::launder требуется - иначе исходный указатель останется недопустимым значением указателя, поскольку в стандарте нет положения, чтобы он автоматически начинал указывать на вновь созданный объект. Но я не уверен в этом. - person Brian Bi; 02.12.2019
comment
Хотя я не уверен, что понимаю P0593 достаточно хорошо, чтобы сделать вывод, что он также не охватывает случай, когда эти элементы еще не были созданы. В противном случае было бы невозможно сделать это: buf_end_size = newbuf + sizeof(T) * size();. Здесь арифметика указателя используется для получения указателя, который переходит в конец массива объектов, которые еще не существуют. Я ошибся? - person skypjack; 09.06.2021

Ко всем широко используемым современным posix-совместимым системам, то есть Windows, Linux (и Android и т. Д.), И MacOSX применимо следующее:

Можно ли использовать malloc () для динамических массивов в C ++?

Да, это так. Использование reinterpret_cast для преобразования результирующего void* в желаемый тип указателя является наилучшей практикой, и в результате получается динамически распределенный массив, подобный этому: type *array = reinterpret_cast<type*>(malloc(sizeof(type)*array_size); Будьте осторожны, в этом случае конструкторы не вызываются для элементов массива, поэтому это по-прежнему неинициализированное хранилище, независимо от того, что type. Деструкторы не вызываются, когда free используется для освобождения


Можно ли использовать оператор new () и размещение new для динамических массивов в более старом C ++, в котором нет ключевого слова alignas?

Да, но вам нужно знать о выравнивании в случае размещения new, если вы вводите в него пользовательские местоположения (то есть те, которые не поступают из malloc / new). Обычный оператор new, а также malloc предоставят собственные области памяти, выровненные по словам (по крайней мере, всякий раз, когда размер выделения> = wordize). Этот факт, а также тот факт, что макеты и размеры структуры определены таким образом, чтобы выравнивание учитывалось должным образом, вам не нужно беспокоиться о выравнивании массивов dyn, если используется malloc или new. Можно заметить, что размер слова иногда значительно меньше, чем самый большой встроенный тип данных (который обычно равен long double), но он должен быть выровнен таким же образом, поскольку выравнивание касается не размера данных, а разрядности адресов на шине памяти для разных размеров доступа.


Не определено ли поведение арифметики указателя при использовании в памяти, возвращаемой оператором new ()?

Нет, пока вы соблюдаете границы памяти процесса - с этой точки зрения new в основном работает так же, как malloc, более того, new фактически вызывает malloc в подавляющем большинстве реализаций, чтобы получить требуемую область. На самом деле арифметика указателей как таковая никогда не бывает недействительной. Однако результат арифметического выражения, оценивающего указатель, может указывать на место за пределами разрешенных областей, но это не ошибка арифметики указателя, а ошибочное выражение.


Саттер советует код, который может сломаться на какой-нибудь старинной машине?

Я так не думаю, если используется правильный компилятор. (не компилируйте инструкции avr или mov с 128-битной памятью в двоичный файл, предназначенный для работы на 80386) Конечно, на разных машинах с разными размерами памяти и разметкой один и тот же буквальный адрес может иметь доступ к областям с разным назначением / статусом / Существование, но зачем вам использовать буквальные адреса, если вы не пишете код драйвера для конкретного оборудования? ... :)

person Géza Török    schedule 23.11.2018
comment
Но почему в ответной ссылке @Brian для того же примера, что и у Саттерса (за исключением деталей реализации) самодельного вектора, говорится, что: На практике этот код работает с рядом существующих реализаций, но в соответствии с C ++ В объектной модели неопределенное поведение возникает в точках #a, #b, #c, #d и #e, потому что они пытаются выполнить арифметику указателя в области выделенной памяти, которая не содержит объект массива. (c) open-std.org/jtc1 /sc22/wg21/docs/papers/2018/p0593r2.html - person xor256; 23.11.2018
comment
Что именно вы имеете в виду под widely used posix-based systems ?. Вы основываете свои ответы на гарантии стандартизации или внедрения? - person ; 23.11.2018
comment
@eukaryota ни один из них, но чистый опыт. Применимо как минимум к последним версиям Windows, Linux (и Android ofc) и Mac OSX. - person Géza Török; 24.11.2018
comment
@ xor256 Это просто неправда ... каждое выделение в конечном итоге приведет к вызову malloc, а реализация malloc обеспечивается библиотекой C платформы, которая должна сама решать эти проблемы, иначе вы бы не возможность использовать структурные массивы в C либо ... - person Géza Török; 24.11.2018
comment
Ой, народ ... вы никогда не видели ассемблерного кода? Указатели - это простые целые числа без знака, безумный страх использовать их в арифметике ... - person Géza Török; 24.11.2018
comment
@ GézaTörök это не страх перед указателями или арифметикой над ними, это страх сложности C ++, он все время меняется и не может понять, что написано в стандарте (как и в случае с налоговыми правилами). Проблема должна возникнуть, по крайней мере, для разработчиков компилятора, поскольку он существует, поэтому, хотя с точки зрения пользователя C ++ ответы на мой исходный вопрос полностью удовлетворяют, с точки зрения любопытного человека я все еще не понимаю, что проблема в том (может быть, абстрактная машина C ++ не может работать с памятью, представленной картофелем фри с перцем?) - person xor256; 25.11.2018
comment
@ xor256 Я все еще думаю, что это слишком остро. Старые добрые классы хранения, к которым каждый из нас давно привык в эпоху C, точно так же работают и в C ++. Поэтому все, что касается указателей, работает точно так же. Единственная сложная часть - это создание / разрушение объекта, но размещение нового / удаления одного объекта дает возможность технически вызвать конструктор и деструктор как функцию. Конечно, вы можете выстрелить себе в ногу, если попытаетесь, но у нас все еще есть необходимый набор инструментов, и им можно надежно воспользоваться. - person Géza Török; 25.11.2018
comment
@ xor256 Так что я тоже не очень понимаю, в чем проблема на самом деле ... - person Géza Török; 25.11.2018
comment
@ GézaTörök: Проблема в том, что компиляторы предполагают, что если два указателя или lvalue не могут идентифицировать одно и то же хранилище, операции над ними могут быть безопасно переупорядочены относительно друг друга. Тот факт, что указатель формируется из другого с использованием арифметики указателей, не всегда достаточно, чтобы убедить некоторые компиляторы в том, что они могут идентифицировать одно и то же хранилище, если они думают, что правила типизации объектов запрещают указателям доступ к одному и тому же хранилищу. - person supercat; 08.03.2020
comment
@supercat Я думаю, вы говорите об очень редких случаях для версий компилятора до введения строгого алиасинга. Если бы такое неверное толкование было обычным явлением, это, например, сделало бы reinterpret_cast совершенно бесполезным. - person Géza Török; 10.03.2020
comment
@ GézaTörök: Напротив, проблема не в предустановленных версиях компилятора, а в компиляторах, которые - вместо того, чтобы интерпретировать строгие правила псевдонима как просто указание на то, что компиляторы могут предположить, что на первый взгляд не имеет отношения указатели не являются псевдонимами - вместо этого они интерпретируют правила как приглашение игнорировать очевидные отношения между указателями. Стандарт не указывает, что с учетом float *p; компилятор должен рассматривать *(uint32_t*)p += 0x08000000; как потенциальный доступ к любому объекту типа float, который может быть идентифицирован p, потому что авторы Стандарта ... - person supercat; 10.03.2020
comment
... считал очевидным, что такие конструкции должны обрабатываться задокументированным образом, характерным для окружающей среды, в ситуациях, когда это было бы полезно и практично, независимо от того, требуется ли это в стандарте или нет. Вопрос о том, когда поддерживать такие конструкции, был оставлен в качестве проблемы качества реализации, о чем, по мнению авторов Стандарта, рынок мог судить лучше, чем Комитет. Что касается компиляторов, за которые люди действительно платили бы, я думаю, они были правы, но объединение gcc с Linux защитило его от рыночных сил. - person supercat; 10.03.2020

Вы можете сделать это с помощью «старомодного» malloc, который дает вам блок памяти, который выполняет наиболее жесткое выравнивание на соответствующей платформе (например, для long long double). Таким образом, вы сможете поместить любой объект в такой буфер без нарушения каких-либо требований к выравниванию.

Учитывая это, вы можете использовать новое размещение для массивов вашего типа на основе такого блока памяти:

struct MyType {
    MyType() {
        cout << "in constructor of MyType" << endl;
    }
    ~MyType() {
        cout << "in destructor of MyType" << endl;
    }
    int x;
    int y;
};

int main() {

    char* buffer = (char*)malloc(sizeof(MyType)*3);
    MyType *mt = new (buffer)MyType[3];

    for (int i=0; i<3; i++)  {
        mt[i].~MyType();
    }
    free(mt);
}

Обратите внимание, что - как всегда с размещением new - вам придется позаботиться о явном вызове деструкторов и освобождении памяти на отдельном шаге; Вы не должны использовать delete или delete[]-функции, которые объединяют эти два шага и тем самым освобождают память, которой они не владеют.

person Stephan Lechner    schedule 23.11.2018
comment
Но могу ли я использовать в вашем примере оператор new () вместо malloc ()? - person xor256; 23.11.2018
comment
Насколько я понимаю, новое размещение массива может потребовать неуказанных накладных расходов на память. Таким образом, определение поведения в этом коде зависит от реализации. См. stackoverflow.com/questions/8720425/ - person ; 23.11.2018
comment
@eukaryota, обратите внимание, что и Саттер, и open-std .org / jtc1 / sc22 / wg21 / docs / paper / 2018 / p0593r2.html примеры, чтобы не использовать размещение массива new. Они используют новое размещение только для каждого элемента, который они создают внутри блока памяти. - person xor256; 23.11.2018
comment
@ xor256 Да, в этом ответе я конкретно имею в виду пример кода. Новое размещение без массива не может иметь эти накладные расходы. - person ; 23.11.2018