Обработка памяти, возвращаемой оператором 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 и заявленные что в операторе 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)];

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

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

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

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

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

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

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, который определяет максимальное безопасное выравнивание для "нормального" 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++ таким образом, чтобы она не работала.

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

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

Ко всем широко используемым в последнее время 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). Конечно, на разных машинах с разным объемом памяти и разметкой один и тот же буквенный адрес может получить доступ к областям разного назначения / статуса. / существование, но зачем вам использовать буквенные адреса, если вы не пишете код драйвера для конкретного оборудования?...:)

Вы можете сделать это с "старомодным" 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[]-функции, которые объединяют эти два шага и тем самым освобождают память, которой они не владеют.

Другие вопросы по тегам