Подтвердить что ты не робот

Каков правильный способ выделения и использования блока неизученной памяти в С++?

Ответы, которые я получил по этому вопросу, до сих пор имеют два противоположных ответа: "это безопасно" и "это поведение 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.

Я пишу правильный код, чтобы делать то, что намерен? Если нет, то каким образом?

4b9b3361

Ответ 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. Вы должны рассмотреть что-то вроде нового оператора.