"построение" тривиально-копируемого объекта с помощью memcpy

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

#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 объект, чье время жизни началось? (Если да, то когда именно это началось?)

3 ответов


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

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

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

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

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

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

проект стандарта C++14 в настоящее время говорит в 1.8 [интро.объект]:

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

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

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

и:

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

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

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

Propoal p0593: неявное создание объектов для низкоуровневой манипуляции объектами

предложение p0593 пытается решить эти вопросы, но AFAIK еще не был рассмотрен.

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

Он имеет некоторые мотивирующие примеры, которые похожи по своей природе, включая текущий std:: vector реализации, которая в настоящее время имеет неопределенное поведение.

Он предлагает следующие способы неявного создания объекта:

мы предлагаем, чтобы как минимум следующие операции были указаны как неявное создание объекты:

  • Создание массива char, unsigned char или std::byte неявно создает объекты внутри этого массива.

  • вызов malloc, calloc, realloc или любой функции с именем operator new или operator new[] неявно создает объекты в возвращаемом хранилище.

  • std:: allocator:: allocate также неявно создает объекты в возвращаемом хранилище; требования распределителя должны требовать других реализации распределителя делают то же самое.

  • вызов memmove ведет себя так, как если бы он

    • копирует исходное хранилище во временную область

    • неявно создает объекты в хранилище назначения, а затем

    • копирует временное хранилище в хранилище назначения.

    Это позволяет memmove сохранять типы тривиально-копируемых объекты или использоваться для переинтерпретации байтового представления одного объекта как представления другого объекта.

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

  • доступ к члену класса, который назначает член объединения, запускает неявное создание объекта в хранилище, занимаемом членом объединения. Обратите внимание, что это не совсем новое правило: разрешение уже существовало в [P0137R1] для случаев, когда доступ члена находится на левой стороне назначения, но теперь обобщается как часть этой новой структуры. Как поясняется ниже, это не позволяет каламбурить тип через союзы; скорее, это просто позволяет активному члену Союза быть измененным выражением доступа члена класса.

  • новая операция барьера (отличная от std::launder, которая не создает объекты) должна быть введена в стандарт библиотека с семантикой, эквивалентной memmove с тем же хранилищем источника и назначения. Как Соломенный человек, мы предлагаем:

    // Requires: [start, (char*)start + length) denotes a region of allocated
    // storage that is a subset of the region of storage reachable through start.
    // Effects: implicitly creates objects within the denoted region.
    void std::bless(void *start, size_t length);
    

в дополнение к вышесказанному в качестве неявно создаваемых объектов должен быть указан набор функций выделения и сопоставления памяти, не относящихся к stasndard, таких как mmap в системах POSIX и VirtualAlloc в системах Windows.

обратите внимание, что указатель reinterpret_cast не считается достаточным для инициировать неявное создание объекта.


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

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

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


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

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

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

#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 не оптимизирован для использования локального буфера для небольших строковых значений, просто указатель данных на внешний блок памяти. The memcpy() копирует внутренние данные из a как в buf. Теперь a.x и b->x обратитесь к тому же адресу памяти для string данные. Когда b->x присваивается новое значение, что блок памяти освобождается, но a.x все еще относится к нему. Когда a затем выходит из области видимости в конце main(), он пытается освободить тот же блок памяти снова. Возникает неопределенное поведение.

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

#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);
}