"создание" объекта с возможностью копирования с возможностью memcpy

В С++ этот код правильный?

#include <cstdlib>
#include <cstring>

struct T   // trivially copyable type
{
    int x, y;
};

int main()
{
    void *buf = std::malloc( sizeof(T) );
    if ( !buf ) return 0;

    T a{};
    std::memcpy(buf, &a, sizeof a);
    T *b = static_cast<T *>(buf);

    b->x = b->y;

    free(buf);
}

Другими словами, существует *b объект, чье жизненное время началось? (Если так, когда это началось точно?)

Ответ 1

Это неуказано, которое поддерживается N3751: время жизни объекта, низкоуровневое программирование и memcpy, который говорит, среди прочего:

В настоящее время стандарты С++ не говорят о том, следует ли использовать memcpy для копии байтов объекта-объекта представляют собой концептуальное назначение или строительство объекта. Разница имеет значение для семантики программный анализ и инструменты преобразования, а также оптимизаторы, время отслеживания объекта. В этой статье предполагается, что

  • использование memcpy для копирования байтов двух разных объектов двух разных тривиальных скопируемых таблиц (но в остальном одинакового размера) допускается

  • такое использование распознается как инициализация или, в более общем смысле, как (концептуально) построение объекта.

Признание в качестве построения объекта будет поддерживать двоичный ввод-вывод, в то время как позволяя анализы и оптимизаторы на протяжении всей жизни.

Я не могу найти какие-либо протоколы встреч, на которых обсуждался этот документ, поэтому кажется, что это все еще проблема.

Стандарт проекта С++ 14 в настоящее время говорит в 1.8 [intro.object]:

[...] Объект создается определением (3.1), посредством нового выражения (5.3.4) или реализацией (12.2), когда это необходимо. [...]

который у нас нет с malloc, а случаи, описанные в стандарте для копирования тривиальных типов с возможностью копирования, относятся только к уже существующим объектам в разделе 3.9 [basic.types]:

Для любого объекта (кроме подобъекта базового класса) тривиально тип копирования T, независимо от того, имеет ли объект допустимое значение типа T, базовые байты (1.7), составляющие объект, могут быть скопированы в массив char или unsigned char.42 Если содержимое массива char или unsigned char копируется обратно в объект, объект должен впоследствии сохраняют свое первоначальное значение [...]

и

Для любого тривиально-скопируемого типа T, если два указателя на T указывают на различные T-объекты obj1 и obj2, где ни obj1, ни obj2 не являются subobject базового класса, если базовые байты (1.7), составляющие obj1, являются скопированные в obj2,43 obj2, впоследствии должны иметь то же значение, что и obj1. [...]

в основном это предложение, так что это не должно удивлять.

dyp указывает на увлекательную дискуссию по этой теме из списка рассылки ub: [ub] Тип punning, чтобы избежать копирования.

Ответ 2

Из быстрый поиск.

"... время жизни начинается, когда правильно выровненное хранилище для объекта распределяется и заканчивается, когда хранилище освобождается или повторно используется другим объектом."

Итак, я бы сказал, по этому определению, срок жизни начинается с выделения и заканчивается бесплатным.

Ответ 3

Правильно ли этот код?

Ну, это обычно "работает", но только для тривиальных типов.

Я знаю, что вы не просили об этом, но давайте использовать пример с нетривиальным типом:

#include <cstdlib>
#include <cstring>
#include <string>

struct T   // trivially copyable type
{
    std::string x, y;
};

int main()
{
    void *buf = std::malloc( sizeof(T) );
    if ( !buf ) return 0;

    T a{};
    a.x = "test";

    std::memcpy(buf, &a, sizeof a);    
    T *b = static_cast<T *>(buf);

    b->x = b->y;

    free(buf);
}

После построения a, a.x присваивается значение. Предположим, что std::string не оптимизирован для использования локального буфера для небольших строковых значений, а только для указателя данных на внешний блок памяти. memcpy() копирует внутренние данные a as-is в buf. Теперь a.x и b->x относятся к одному и тому же адресу памяти для данных string. Когда b->x присваивается новое значение, этот блок памяти освобождается, но a.x все еще ссылается на него. Когда a затем выходит из области видимости в конце main(), он пытается снова освободить тот же блок памяти. Undefined.

Если вы хотите быть "правильным", правильным способом создания объекта в существующий блок памяти является использование вместо этого оператора размещение-новый, например:

#include <cstdlib>
#include <cstring>

struct T   // does not have to be trivially copyable
{
    // any members
};

int main()
{
    void *buf = std::malloc( sizeof(T) );
    if ( !buf ) return 0;

    T *b = new(buf) T; // <- placement-new
    // calls the T() constructor, which in turn calls
    // all member constructors...

    // b is a valid self-contained object,
    // use as needed...

    b->~T(); // <-- no placement-delete, must call the destructor explicitly
    free(buf);
}