обработка памяти, возвращаемой оператором 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 выровненных элементов size = sizeof (T). Так как sizeof (T) должен возвращать размер элемента, который выровнен, и они располагаются один за другим в куске памяти, здесь хорошо использовать арифметику указателей.

Однако тогда мне указали ссылки вроде: http://eel.is/c++draft/expr.add#4 или же http://eel.is/c++draft/intro.object#def:object и утверждение, что в операторе C ++ 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)];

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

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

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

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

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

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

8

Решение

Стандарты 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 ++ таким образом, чтобы она не работала.

3

Другие решения

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

T* storage = 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 достаточно хорошо, чтобы сделать вывод, что он также не будет охватывать случай, когда эти элементы не имел уже был построен.

7

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

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

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


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

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


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

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


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

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

1

Вы можете сделать это с «старомодным» 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);
}

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

0