Каков правильный способ выделения и использования блока неизученной памяти в С++?
Ответы, которые я получил по этому вопросу, до сих пор имеют два противоположных ответа: "это безопасно" и "это поведение undefined". Я решил переписать вопрос в целом, чтобы получить более четкие ответы, для меня и для тех, кто может приехать сюда через Google.
Кроме того, я удалил тег C
, и теперь этот вопрос задан как С++
Я делаю 8-байт-выровненную кучу памяти, которая будет использоваться в моей виртуальной машине. Самый очевидный подход, о котором я могу думать, - это выделить массив std::uint64_t
.
std::unique_ptr<std::uint64_t[]> block(new std::uint64_t[100]);
Предположим sizeof(float) == 4
и sizeof(double) == 8
. Я хочу сохранить float и double в block
и напечатать значение.
float* pf = reinterpret_cast<float*>(&block[0]);
double* pd = reinterpret_cast<double*>(&block[1]);
*pf = 1.1;
*pd = 2.2;
std::cout << *pf << std::endl;
std::cout << *pd << std::endl;
Я также хотел бы сохранить C-строку с надписью "привет".
char* pc = reinterpret_cast<char*>(&block[2]);
std::strcpy(pc, "hello\n");
std::cout << pc;
Теперь я хочу сохранить "Привет, мир!". который превышает 8 байтов, но я все еще могу использовать две последовательные ячейки.
char* pc2 = reinterpret_cast<char*>(&block[3]);
std::strcpy(pc2, "Hello, world\n");
std::cout << pc2;
Для целых чисел мне не нужен reinterpret_cast
.
block[5] = 1;
std::cout << block[5] << std::endl;
Я выделяю block
как массив std::uint64_t
для единственной цели выравнивания памяти. Я также не ожидаю, что в нем будет храниться больше 8 байтов. Тип блока может быть любым, если начальный адрес гарантированно выравнивается по 8 байт.
Некоторые люди уже ответили, что то, что я делаю, абсолютно безопасно, но некоторые другие сказали, что я определенно вызываю поведение undefined.
Я пишу правильный код, чтобы делать то, что намерен? Если нет, то каким образом?
Ответы
Ответ 1
Глобальные функции распределения
Чтобы выделить произвольный (нетипизированный) блок памяти, глобальные функции распределения (§3.7.4/2);
void* operator new(std::size_t);
void* operator new[](std::size_t);
Можно использовать для этого (§3.7.4.1/2).
§3.7.4.1/2
Функция распределения пытается выделить запрошенный объем памяти. Если он успешный, он должен вернуть адрес начала блока хранения, длина которого в байтах должна быть не меньше размера запрашиваемого. Нет ограничений на содержимое выделенного хранилища при возврате из функции распределения. Порядок, смежность и начальное значение хранилища, выделенные последовательными вызовами функции распределения, не определены. Возвращаемый указатель должен быть соответствующим образом выровнен так, чтобы его можно было преобразовать в указатель любого полного типа объекта с фундаментальным требованием к выравниванию (3.11), а затем использовать для доступа к объекту или массиву в выделенном хранилище (до тех пор, пока хранилище не будет явно освобождено вызов соответствующей функции освобождения).
И в 3.11 это должно сказать о фундаментальном требовании выравнивания;
§3.11/2
Фундаментальное выравнивание представлено выравниванием, меньшим или равным наибольшему выравниванию, поддерживаемому реализацией во всех контекстах, которое равно alignof(std::max_align_t)
.
Просто, чтобы быть уверенным в том, что функции распределения должны вести себя следующим образом:
§3.7.4/3
Любые функции распределения и/или освобождения, определенные в программе на С++, включая версии по умолчанию в библиотеке, должны соответствовать семантике, указанной в пунктах 3.7.4.1 и 3.7.4.2.
Цитаты из С++ WD n4527.
Предполагая, что 8-байтовое выравнивание меньше фундаментального выравнивания платформы (и похоже, что это так, но это можно проверить на целевой платформе с помощью static_assert(alignof(std::max_align_t) >= 8)
) - вы можете использовать глобальный ::operator new
для выделите требуемую память. После выделения память может быть сегментирована и использована с учетом требований к размеру и выравниванию.
Альтернативой здесь является std::aligned_storage
, и он сможет предоставить вам память, выровненную по любому требованию.
typename std::aligned_storage<sizeof(T), alignof(T)>::type buffer[100];
Из вопроса, я предполагаю, что размер и выравнивание T
будут равны 8.
Пример того, как мог выглядеть последний блок памяти (включая базовый RAII),
struct DataBlock {
const std::size_t element_count;
static constexpr std::size_t element_size = 8;
void * data = nullptr;
explicit DataBlock(size_t elements) : element_count(elements)
{
data = ::operator new(elements * element_size);
}
~DataBlock()
{
::operator delete(data);
}
DataBlock(DataBlock&) = delete; // no copy
DataBlock& operator=(DataBlock&) = delete; // no assign
// probably shouldn't move either
DataBlock(DataBlock&&) = delete;
DataBlock& operator=(DataBlock&&) = delete;
template <class T>
T* get_location(std::size_t index)
{
// https://stackoverflow.com/a/6449951/3747990
// C++ WD n4527 3.9.2/4
void* t = reinterpret_cast<void*>(reinterpret_cast<unsigned char*>(data) + index*element_size);
// 5.2.9/13
return static_cast<T*>(t);
// C++ WD n4527 5.2.10/7 would allow this to be condensed
//T* t = reinterpret_cast<T*>(reinterpret_cast<unsigned char*>(data) + index*element_size);
//return t;
}
};
// ....
DataBlock block(100);
Я построил более подробные примеры DataBlock
с подходящими шаблонами construct
и get
функций и т.д., живая демонстрация здесь и здесь с дополнительной проверкой ошибок и т.д..
Заметка о сглаживании
Похоже, что в исходном коде есть некоторые проблемы с псевдонимом (строго говоря); вы выделяете память одного типа и передаете ее другому типу.
Возможно, он работает так, как вы ожидаете, на своей целевой платформе, но вы не можете полагаться на него. Самый практичный комментарий, который я видел на этом:
"Undefined поведение имеет неприятный результат, как правило, делать то, что вы думаете, что он должен делать, пока это не будет" - hvd.
Код, который вы, вероятно, будете работать. Я думаю, что лучше использовать соответствующие глобальные функции распределения и быть уверенным, что при распределении и использовании требуемой памяти нет поведения undefined.
Сглаживание будет по-прежнему применяться; как только память будет выделена - сглаживание применимо в том, как оно используется. Как только у вас будет выделен произвольный блок памяти (как указано выше с глобальными функциями распределения) и время жизни объекта (§3.8/1), применяются правила сглаживания.
Как насчет std::allocator
?
В то время как std::allocator
предназначен для однородных контейнеров данных, а то, что вы ищете, сродни гетерогенным распределениям, реализация в вашем стандарте библиотека (с учетом Концепция Allocator) предлагает некоторые рекомендации по распределению необработанных памяти и соответствующей конструкции требуемых объектов.
Ответ 2
Обновление для нового вопроса:
Отличная новость заключается в простом и простом решении вашей реальной проблемы: выделите память с помощью new
(unsigned char[size]
). Память, выделенная с помощью new
, гарантируется в стандарте, который должен быть выровнен таким образом, который подходит для использования как любой тип, и вы можете безопасно псевдонизировать любой тип с помощью char*
.
Стандартная ссылка, 3.7.3.1/2, функции распределения:
Возвращаемый указатель должен быть соответствующим образом выровнен, чтобы он мог быть преобразуется в указатель любого полного типа объекта, а затем используется для доступ к объекту или массиву в выделенной памяти
Оригинальный ответ на исходный вопрос:
По крайней мере, в С++ 98/03 в 3.10/15 мы имеем следующее, что довольно четко делает его по-прежнему undefined поведение (поскольку вы получаете доступ к значению через тип, который не перечисляется в списке исключений)
Если программа пытается получить доступ к сохраненному значению объекта через l-значение другого, кроме одного из следующих типов, поведение undefined):
- динамический тип объекта,
- cvqualified версия динамического типа объекта,
- тип, который является подписанным или неподписанным типом, соответствующим динамическому типу объекта,
- тип, который является подписанным или неподписанным типом, соответствующим cvqualified версии динамического типа объекта,
- совокупность или тип объединения, который включает один из вышеупомянутых типов среди его членов (включая рекурсивно, член субагрегата или содержащегося объединения),
- тип, который является (возможно, cvqualified) типом базового класса динамического типа объекта,
- a char или неподписанный char тип.
Ответ 3
Много обсуждений здесь и даны некоторые ответы, которые немного неправильны, но, составляя хорошие моменты, я просто попытаюсь суммировать:
-
точно соответствует тексту стандарта (независимо от того, какая версия)... да, это поведение undefined. Обратите внимание, что в стандарте нет даже строгого псевдонимов - всего лишь набор правил для обеспечения его соблюдения независимо от того, какие реализации могут определить.
-
понимая причину правила "строгого сглаживания", он должен хорошо работать в любой реализации как долго, поскольку ни float
, ни double
не принимают более 64 бит.
-
стандарт не гарантирует вам ничего о размере float
или double
(намеренно) и что причина этого в том, что это ограничение в первую очередь.
-
вы можете обойти все это, гарантируя, что ваша "куча" - это выделенный объект (например, получить его с помощью malloc()
) и получить доступ к выровненным слотам через char *
и сдвинуть смещение на 3 бита.
-
вам все равно нужно убедиться, что все, что вы храните в таком слоте, не займет больше 64 бит. (что сложная часть, когда дело касается переносимости)
Вкратце: ваш код должен быть безопасным при любой "нормальной" реализации, если ограничения по размеру не являются проблемой (означает: ответ на вопрос в вашем названии, скорее всего, нет), НО это все еще undefined поведение (означает: ответ на ваш последний абзац да)
Ответ 4
pc
pf
и pd
- все разные типы, которые обращаются к памяти, указанной в block
как uint64_t
, поэтому, например, "pf
общие типы: float
и uint64_t
.
Можно было бы нарушить правило строгого псевдонира, которое когда-то было писать одним типом и читать с использованием другого, поскольку компилятор мог бы переупорядочить операции, думая, что нет общего доступа. Это не ваш случай, так как массив uint64_t
используется только для назначения, он точно такой же, как использование alloca
для выделения памяти.
Кстати, не существует проблемы со строгим правилом псевдонимов при литье любого типа в тип char и наоборот. Это общая схема, используемая для сериализации данных и десериализации.
Ответ 5
Я сделаю это коротко: весь ваш код работает с определенной семантикой, если вы выделяете блок с помощью
std::unique_ptr<char[], std::free>
mem(static_cast<char*>(std::malloc(800)));
Поскольку
- каждому типу разрешен псевдоним с помощью
char[]
и
-
malloc()
гарантированно вернет блок памяти, достаточно выровненный для всех типов (кроме, возможно, SIMD).
Мы передаем std::free
как пользовательский делектор, потому что мы использовали malloc()
, а не new[]
, поэтому вызов delete[]
, по умолчанию, будет undefined.
Если вы пурист, вы также можете использовать operator new
:
std::unique_ptr<char[]>
mem(static_cast<char*>(operator new[](800)));
Тогда нам не нужен пользовательский отладчик. Или
std::unique_ptr<char[]> mem(new char[800]);
чтобы избежать static_cast
от void*
до char*
. Но operator new
может быть заменен пользователем, поэтому я всегда немного опасаюсь его использовать. Ото; malloc
не может быть заменен (только в отношении платформы, например LD_PRELOAD
).
Ответ 6
Да, поскольку ячейки памяти, на которые указывает pf
, могут перекрываться в зависимости от размера float
и double
. Если бы они этого не сделали, результаты чтения *pd
и *pf
были бы четко определены, но не результаты чтения из block
или pc
.
Ответ 7
Поведение С++ и ЦП различны. Несмотря на то, что стандарт обеспечивает память, подходящую для любого объекта, правила и оптимизации, налагаемые процессором, делают выравнивание для любого заданного объекта "undefined" - массив с коротким аргументом должен быть согласован по 2 байта, но массив из 3-байтовой структуры может быть выровнено по 8 байт. Объединение всех возможных типов может быть создано и использовано между вашим хранилищем и использованием, чтобы не нарушать правила выравнивания.
union copyOut {
char Buffer[200]; // max string length
int16 shortVal;
int32 intVal;
int64 longIntVal;
float fltVal;
double doubleVal;
} copyTarget;
memcpy( copyTarget.Buffer, Block[n], sizeof( data ) ); // move from unaligned space into union
// use copyTarget member here.
Ответ 8
Если вы отметите это как вопрос на С++,
(1) зачем использовать uint64_t [], но не std::vector?
(2) с точки зрения управления памятью, в вашем коде отсутствует логика управления, которая должна отслеживать, какие блоки используются и которые являются бесплатными, а также отслеживание смежных блоков и, конечно же, методы выделения и выделения блоков.
(3) код показывает небезопасный способ использования памяти. Например, char * не является константой, и поэтому блок может быть потенциально записан и перезаписан следующий блок (ы). Reinterpret_cast считается опасным и должен быть абстрактным из логики памяти.
(4) код не показывает логику распределителя. В мире C функция malloc является нетипизированной и в мире С++ вводится оператор new. Вы должны рассмотреть что-то вроде нового оператора.