В C можно выделять динамические массивы с помощью malloc(sizeof(T) * N), а затем использовать арифметику указателей для получения элементов со смещением i в этом динамическом массиве.
В C++ можно сделать то же самое, используя operator new() так же, как malloc(), а затем разместить новый (например, решение для пункта 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()?Извините, если это глупо.
да. И я хотел понять, в чем разница между malloc в C и C++ в этом случае. Поскольку я давно видел, что оператор new () реализован в терминах malloc () в некоторых заголовках C++ в какой-то версии GCC, поэтому оператор new () будет просто эквивалентен malloc (), поэтому могу ли я использовать над ним арифметику указателя в C++ без выравнивания? Я совершенно запуталась. :(
@ xor256 Я полагаю, что реализация может использовать неопределенное поведение, если она дает больше гарантий для себя. Поэтому то, использует ли GCC что-то в своей реализации библиотеки (даже при условии, что она не содержит ошибок и придерживается стандарта), ничего не говорит вам о том, определено ли это поведение в стандарте.
Вы можете поместить сюда весь код? Потому что представленный вами код даже не компилируется (по крайней мере, в первой строке отсутствует приведение).
Для арифметики указателя вам не нужен массив, единственное, что вы должны быть осторожны, чтобы избежать неопределенного поведения, - это не перемещаться за конец указателя (один за вашим выделенным размером).
Мне не удается найти то, что, по моему мнению, является релевантным сообщением. Однако я думаю, что это нормально, потому что я почти уверен, что в стандарте говорится, что один объект рассматривается как массив из одного элемента по отношению к арифметике указателей.
@Galik Там написано, что в eel.is/c++draft/expr.add#footnote-85. Я не думаю, что указатель считается указывающим на один объект. В выделенной памяти не было построено ни одного объекта.
@eukaryota Да, мне кажется, я неправильно понял вопрос. Неужели вы не думаете, что выражение (possibly-hypothetical) можно извлечь из стандартной формулировки?
Да, как я понимаю, в этом коде действительно есть UB. Но, на мой взгляд, исправлять нужно стандарт, а не код Херба. Было бы интересно узнать, почему у нас есть такое ограничивающее правило для арифметики указателей.
@geza Может быть, это потому, что определение объект было недавно изменено на более ограничительное? Раньше даже неинициализированная память была объектом.
@Galik Я точно не знаю, но в моем любительском чтении possibly-hypothetical относится к hypothetical x[n] после фактического массива, и соответствующий if из 4.2 также не использует его.
@eukaryota Я думаю, это означает в целях арифметики, пока память выделена и теоретически может стать объектом, тогда арифметика работает. Но трудно быть уверенным
@Galik: Насколько я помню, в C++ 98 было то же правило, и оно означало то же самое (в том смысле, что код Херба никогда не был четко определен).





Проблема арифметики указателя на выделенную память, как в вашем примере:
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 достаточно хорошо, чтобы сделать вывод, что он также не охватывает случай, когда эти элементы не имел уже созданы.
Хм, а почему тут P0593 актуален? T может быть любого типа. Думаю, это предложение не решит эту проблему.
std::vector не является нереализуемым. Возможно, это нереализуемый в коде пользователя. Стандартная библиотека не может быть реализована в переносимом коде, тем более в коде, написанном пользователем. Это одна из причин, по которой он поставляется с компилятором - он может использовать известное поведение этого компилятора и целевой ОС.
@PeteBecker Вот что я имел в виду. std::vector не должен был быть реализован в пользовательском коде.
@geza Я сделал предположение (которое, вероятно, мне следовало заявить), что все storage[0], storage[1], ..., storage[i-1] уже созданы. В этом случае P0593 подразумевает, что объект массива с элементами i создается неявно, а storage + i находится за концом и, следовательно, четко определен. P0593 указывает, что он предназначен для работы с массивами любого типа.
Я имел в виду, что P0593 касается типов, которые автор называет «неявными типами времени жизни». Таким образом, это предложение не касается всех типов, оно не может быть общим решением этой проблемы. Но и в этом я не уверен на 100% :)
Но в чем причина УБ? Это не арифметика указателей, а этот std :: bless what is? Размещение новое? Можете ли вы свести это к элементарным блокам?
А как насчет арифметики на (uint8_t *)storage? Разве это не было бы четко определенным, позволяющим четко определить реализацию vector?
@HolyBlackCat: Нет. Насколько я знаю, вам нужно подключить reinterpret_cast к uintptr_t и выполнять там арифметические операции. Это, конечно, поведение, определяемое реализацией
@geza Он говорит, что тип массива типа элемента любой является неявным типом времени жизни (независимо от того, является ли тип элемента неявным типом времени жизни). Потому что, если у вас уже есть группа объектов типа T, выстроенных в память, то для создания массива T не требуется никакого дополнительного кода.
Спасибо, это действительно имеет смысл! А теперь пора в 35-й раз перечитать это предложение :)
Я не уверен, следует ли мне создавать новый вопрос, позвольте мне задать его здесь. Предположим, мы не используем значение, возвращаемое new. Можем ли мы использовать storage или p для доступа к элементам? Или надо сначала его std::launder?
@Evg Хороший вопрос. Я думаю, что требуется std::launder - в противном случае исходный указатель останется недопустимым значением указателя, поскольку в стандарте нет положения, чтобы он автоматически начинал указывать на вновь созданный объект. Но я не уверен в этом.
buf_end_size = newbuf + sizeof(T) * size();. Здесь арифметика указателя используется для получения указателя, который переходит в конец массива объектов, которые еще не существуют. Я ошибся?
Вы можете сделать это с помощью «старомодного» 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[], которые объединяют эти два шага и тем самым освобождают память, которой они не владеют.
Но могу ли я использовать в вашем примере оператор new () вместо malloc ()?
Насколько я понимаю, новое размещение массива может потребовать неуказанных накладных расходов на память. Таким образом, определение поведения в этом коде зависит от реализации. См. stackoverflow.com/questions/8720425/…
@eukaryota обратите внимание, что в примерах Sutter и open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0593r2.html не используется размещение массива new. Они используют новое размещение только для каждого элемента, который они создают внутри блока памяти.
@ xor256 Да, в этом ответе я конкретно имею в виду пример кода. Новое размещение без массива не может иметь эти накладные расходы.
Ко всем широко используемым современным posix-совместимым системам, то есть к Windows, Linux (и Android и т. д.), И MacOSX применимо следующее:
Is it possible to use malloc() for dynamic arrays in C++?
Да, это. Использование reinterpret_cast для преобразования результирующего void* в желаемый тип указателя является наилучшей практикой, и в результате получается динамически распределенный массив, подобный этому: type *array = reinterpret_cast<type*>(malloc(sizeof(type)*array_size);Будьте осторожны, в этом случае конструкторы не вызываются для элементов массива, поэтому это все еще неинициализированное хранилище, независимо от того, что такое type. Деструкторы не вызываются, когда free используется для освобождения памяти.
Is it possible to use operator new() and placement new for dynamic arrays in older C++ which has no alignas keyword?
Да, но вам нужно знать о выравнивании в случае размещения new, если вы вводите его в пользовательские местоположения (то есть те, которые не поступают из malloc / new). Обычный оператор new, так же как и malloc, предоставит собственные области памяти, выровненные по словам (по крайней мере, всякий раз, когда размер выделения> = wordize). Этот факт, а также тот факт, что макеты и размеры структуры определены таким образом, чтобы выравнивание учитывалось должным образом, вам не нужно беспокоиться о выравнивании массивов dyn, если используется malloc или new.
Можно заметить, что размер слова иногда значительно меньше, чем самый большой встроенный тип данных (обычно это long double), но он должен быть выровнен таким же образом, поскольку выравнивание касается не размера данных, а разрядности адресов на шина памяти для разных размеров доступа.
Is pointer arithmetic undefined behavior when used over memory returned by operator new()?
Нет, пока вы соблюдаете границы памяти процесса - с этой точки зрения new в основном работает так же, как malloc, более того, new фактически вызывает malloc в подавляющем большинстве реализаций, чтобы получить требуемую область.
На самом деле арифметика указателей как таковая никогда не бывает недействительной. Однако результат арифметического выражения, оценивающего указатель, может указывать на место за пределами разрешенных областей, но это не ошибка арифметики указателя, а ошибочное выражение.
Is Sutter advising code which might break on some antique machine?
Я так не думаю, если используется правильный компилятор. (не компилируйте инструкции avr или mov с 128-битной памятью в двоичный файл, предназначенный для работы на 80386) Конечно, на разных машинах с разным объемом памяти и разметкой один и тот же буквальный адрес может иметь доступ к областям с разным назначением / статусом / существованием, но зачем вам использовать буквальные адреса, если вы не пишете код драйвера для определенного оборудования? ... :)
Но почему в ответной ссылке @Brian для того же примера, что и Саттерс (за исключением деталей реализации) самодельного вектора, говорится, что: «На практике этот код работает с рядом существующих реализаций, но в соответствии с В объектной модели C++ неопределенное поведение возникает в точках #a, #b, #c, #d и #e, потому что они пытаются выполнить арифметические операции с указателями в области выделенной памяти, которая не содержит объект массива. "? (c) open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0593r2.html
Что именно вы имеете в виду под widely used posix-based systems ?. Вы основываете свои ответы на гарантии стандартизации или внедрения?
@eukaryota ни один из них, но чистый опыт. Применимо как минимум к последним версиям Windows, Linux (и Android ofc) и Mac OSX.
@ xor256 Это просто неправда ... каждое выделение в конечном итоге приведет к вызову malloc, а реализация malloc обеспечивается библиотекой платформы C, которая должен сама решает эти проблемы, иначе вы не сможете использовать массивы структур в C либо ...
Ой, народ ... вы никогда не видели ассемблерного кода? Указатели - это простые целые числа без знака, безумный страх использовать их в арифметике ...
@ GézaTörök - это не страх перед указателями или арифметикой над ними, это страх сложности C++, он все время меняется и не может понять, что написано в стандарте (как и в налоговых правилах). Проблема должна возникнуть, по крайней мере, для разработчиков компилятора, поскольку он существует, поэтому, хотя с точки зрения пользователя C++ ответы на мой исходный вопрос полностью удовлетворяют, с точки зрения любопытного человека я все еще не понимаю, что проблема в том (может быть, абстрактная машина C++ не может работать с памятью, представленной картофелем фри с перцем?)
@ xor256 Я все еще думаю, что это слишком остро. Старые добрые классы хранения, к которым каждый из нас давно привык в эпоху C, точно так же работают и в C++. Следовательно, все, что касается указателей, работает точно так же. Единственная сложная часть - это создание / разрушение объекта, но размещение нового / удаления одного объекта дает возможность технически вызвать конструктор и деструктор как функцию. Конечно, вы можете выстрелить себе в ногу, если попытаетесь, но у нас все еще есть необходимый набор инструментов, и им можно надежно воспользоваться.
@ xor256 Так что я тоже не очень понимаю, в чем проблема на самом деле ...
@ GézaTörök: Проблема в том, что компиляторы предполагают, что если два указателя или lvalue «не могут» идентифицировать одно и то же хранилище, операции над ними могут быть безопасно переупорядочены относительно друг друга. Тот факт, что указатель формируется из другого указателя с использованием арифметики указателя, не всегда достаточен, чтобы убедить некоторые компиляторы в том, что они могут идентифицировать одно и то же хранилище, если они думают, что правила типизации объектов запрещают указателям доступ к одному и тому же хранилищу.
@supercat Я думаю, вы говорите об очень редких случаях для версий компилятора до введения строгого алиасинга. Если бы такое неверное толкование было обычным явлением, это, например, сделало бы reinterpret_cast совершенно бесполезным.
@ GézaTörök: Напротив, проблема не в предустановленных версиях компилятора, а в компиляторах, которые - вместо того, чтобы интерпретировать правила «строгого псевдонима», просто говоря, что компиляторы могут предположить, что указатели на первый взгляд несвязанный не имеют псевдонимов - вместо этого интерпретируйте правила как приглашение игнорировать очевидные отношения между указателями. Стандарт не указывает, что для данного float *p; компилятор должен рассматривать *(uint32_t*)p += 0x08000000; как потенциальный доступ к любому объекту типа float, который может быть идентифицирован p, потому что авторы Стандарта ...
... считал очевидным, что такие конструкции должны обрабатываться «документированным образом, характерным для окружающей среды» в ситуациях, когда это было бы полезно и практично, независимо от того, требуется ли это в стандарте или нет. Вопрос о том, когда поддерживать такие конструкции, был оставлен как проблема качества реализации, о чем, по мнению авторов Стандарта, рынок мог судить лучше, чем Комитет. Что касается компиляторов, за которые люди действительно будут платить, я думаю, они были правы, но объединение gcc с Linux защитило его от рыночных сил.
Стандарты 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, который определяет максимально безопасное выравнивание для «нормального» 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++ так, чтобы она не работала.
CWG 1701 не имеет ничего общего с рассматриваемой проблемой. CWG 1701 посвящена представлению объектов. Проблема с распределением функций в том, что они не создают объекты. Как здесь должно помочь решение вопроса?
@LanguageLawyer, нет правда, что функции распределения не создают объекты. См. стандарт. Предназначен авторами языка, что следует из того, как они (и все остальные) используют такие конструкции; если сомневаетесь, вы можете спросить их напрямую, их электронные письма не являются секретными.
@Набор. См. Стандарт. Он не говорит, что объект создан, он говорит, что его время жизни началось. См. Стандарт, когда объект создается.
@LanguageLawyer, то «когда объект создается» - соломинка. Семантически нет никакой разницы, «создает» ли функция распределения или «ссылается» на массив байтов.
@Набор. Нет, это не соломинка. Это предназначено. Ваше (очень популярное среди людей) неправильное толкование правила начала срока службы, согласно которому мириады объектов волшебным образом появляются в хранилище соответствующего размера и выравнивания, явно противоречит нескольким правилам, например, когда объекты в течение своего времени жизни могут иметь один и тот же адрес, что показывает, что такие толкование не предназначалось Комитетом.
@LanguageLawyer, нет, это ваша интерпретация того, что они появляются «волшебным образом». В стандарте четко указано, что их срок службы начинается с момента получения хранилища с правильным выравниванием и размером. Если вы считаете, что это противоречит чему-то еще в стандарте, отправьте отчет о дефекте.
@Набор. В стандарте четко указано, что их срок службы начинается с момента получения хранилища с правильным выравниванием и размером. Ага. Когда объект создается, первое, что для него получается хранилище. И если нет непустой инициализации, запускается время жизни создаваемого объекта. Это правильная интерпретация правила.
@Набор. В любом случае, здесь - это предложение члена Комитета, говорящее «это поддерживает статус-кво, что одного malloc недостаточно для создания объекта». Вы сказали "неверно, что функции распределения не создают объекты. Предназначено авторами языка". Как видим, не предполагалось.
@LanguageLawyer, C++ был создан и развивается как язык, одним из самых сильных аргументов которого является способность работать с объектами POD не создается языковыми конструкциями (от аппаратных регистров до данных в файлах, отображаемых в память, до объектов, созданных в том же процессе с помощью кода, написанного на другом языке). язык). Если бы Комитет однажды решил запретить эту способность, такое глупое решение вызвало бы массовый протест в отрасли, который невозможно было бы пропустить.
@Набор. Если бы объекты волшебным образом появились в любом подходящем хранилище, тогда с кодом в сообщении OP не было бы проблем, потому что там появился бы массив беззнаковых символов, охватывающий всю часть выделенного хранилища. Но это не так. См. Предложение в ответе с самым высоким рейтингом.
@LanguageLawyer, код в сообщении OP не имеет проблем, за исключением тех, которые, возможно, относятся к STDCPP_DEFAULT_NEW_ALIGNMENT. Что имеет проблемы с этим кодом, так это интерпретация, что объекты в текущем C++ могут начать свое время жизни только в результате создания объекта языковые конструкции. Хотя изменение языка таким образом, чтобы эта интерпретация стала правильной, может показаться хорошей идеей, это может излишне нарушить большой объем существующего кода, особенно в автономных реализациях, не дав взамен ничего полезного.