Могу ли я использовать memcpy для записи в несколько соседних подкомпонентов стандартного макета?

Отказ от ответственности:. Это пытается развернуть более серьезную проблему, поэтому, пожалуйста, не зацикливайтесь на том, действительно ли этот пример имеет смысл на практике.

И да, если вы хотите копировать объекты, используйте/предоставьте конструктор-копию. (Но обратите внимание, что даже пример не копирует целый объект, он пытается разбить некоторую память на несколько соседних (Q.2) целых чисел.)


Учитывая С++ Стандартная компоновка struct, могу ли я использовать memcpy для записи сразу нескольким (смежным) под-объектам

Полный пример: (https://ideone.com/1lP2Gd https://ideone.com/YXspBk)

#include <vector>
#include <iostream>
#include <assert.h>
#include <inttypes.h>
#include <stddef.h>
#include <memory.h>

struct MyStandardLayout {
    char mem_a;
    int16_t num_1;
    int32_t num_2;
    int64_t num_3;
    char mem_z;

    MyStandardLayout()
    : mem_a('a')
    , num_1(1 + (1 << 14))
    , num_2(1 + (1 << 30))
    , num_3(1LL + (1LL << 62))
    , mem_z('z')
    { }

    void print() const {
        std::cout << 
            "MySL Obj: " <<
            mem_a << " / " <<
            num_1 << " / " <<
            num_2 << " / " <<
            num_3 << " / " <<
            mem_z << "\n";
    }
};

void ZeroInts(MyStandardLayout* pObj) {
    const size_t first = offsetof(MyStandardLayout, num_1);
    const size_t third = offsetof(MyStandardLayout, num_3);
    std::cout << "ofs(1st) =  " << first << "\n";
    std::cout << "ofs(3rd) =  " << third << "\n";
    assert(third > first);
    const size_t delta = third - first;
    std::cout << "delta =  " << delta << "\n";
    const size_t sizeAll = delta + sizeof(MyStandardLayout::num_3);
    std::cout << "sizeAll =  " << sizeAll << "\n";

    std::vector<char> buf( sizeAll, 0 );
    memcpy(&pObj->num_1, &buf[0], sizeAll);
}

int main()
{
    MyStandardLayout obj;
    obj.print();
    ZeroInts(&obj);
    obj.print();

    return 0;
}

Учитывая формулировку в С++ Standard:

9.2 Члены класса

...

13 Нестационарные члены данных (неединичного) класса с одним и тем же контролем доступа (раздел 11) распределяются так, что последующие члены более высокие адреса внутри объекта класса. (...) Требования к выравниванию реализации могут приводить к двум смежные члены не должны быть распределены сразу друг за другом; (...)

Я бы сделал вывод, что гарантировано, что num_1 to num_3 имеют увеличивающиеся адреса и являются смежными по модулю дополнением.

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

  • memcpy должно быть разрешено записывать сразу несколько "объектов памяти" таким образом, то есть

    7.21.2.1 Функция memcpy

    2 Функция memcpy копирует n символов из объекта, на который указывает s2 в объект, на который указывает s1.

    Итак, для меня вопрос здесь по. это то, является ли целевой диапазон, который мы здесь здесь, можно рассматривать как "объект" в соответствии со стандартом C или С++. Примечание. Массив символов (часть), объявленный и определенный как таковой, можно считать считанным "объектом" для целей memcpy, потому что я уверен, что мне разрешено копировать с одного часть массива char в другую часть (другого) массива char.

    Итак, тогда вопрос был бы, если бы было законно переинтерпретировать диапазон памяти трех членов как "концептуальный" (?) char массив.

  • Вычисление sizeAll является законным, то есть использование offsetof является законным, как показано.

  • Запись в дополнение между членами является законной.

Сохраняются ли эти свойства? Я пропустил что-нибудь еще?

Ответ 1

Поместите это как частичный ответ. memcpy(&num_1, buf, sizeAll):

Примечание: ответ Джеймса намного более краткий и окончательный.

Я спросил:

  • memcpy должно быть разрешено записывать сразу несколько "объектов памяти" таким образом, то есть

    • Вызов memcpy с целевым адресом num_1 и размером, превышающим размер объекта num_1, является законным.
    • [С++ (14) Standard] [2], AFAICT, описывает описание memcpy в [C99 Standard] [3], и в нем говорится:

    7.21.2.1 Функция memcpy

         

    2 Функция memcpy копирует n символов из объекта, на который указывает s2   в объект, на который указывает s1.

    Итак, для меня вопрос здесь по. это то, является ли целевой диапазон, который мы здесь здесь, можно рассматривать как "объект" в соответствии с C или С++ Стандартный.

Мышление и поиск немного больше, я нашел в стандарте C:

§ 6.2.6. Представления типов

§ 6.2.6.1 Общие сведения

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

Итак, по крайней мере, подразумевается, что "объект" = > "непрерывная последовательность байтов".

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

Тогда, как указано в Q, §9.2/13 Стандарта С++ (и § 1.8/5), похоже, гарантируют, что мы имеем непрерывную последовательность байтов (включая дополнение).

Тогда в §3.9/3 говорится:

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

T* t1p;
T* t2p;       
     // provided that t2p points to an initialized object ...         
std::memcpy(t1p, t2p, sizeof(T));  
     // at this point, every subobject of trivially copyable type in *t1p contains        
     // the same value as the corresponding subobject in *t2p

-end пример]

Таким образом, это явно допускает применение memcpy к целым объектам типов тривиально копируемого типа.

В этом примере три элемента содержат "тривиально скопируемый под-объект", и, действительно, я думаю, что их объединение в фактическом подобъекте отдельного типа все равно будет выполнять точно такой же макет памяти для явного объекта, как и для трех членов

struct MyStandardLayout_Flat {
    char mem_a;
    int16_t num_1;
    int32_t num_2;
    int64_t num_3;
    char mem_z;
};

struct MyStandardLayout_Sub {
    int16_t num_1;
    int32_t num_2;
    int64_t num_3;
};

struct MyStandardLayout_Composite {
    char mem_a;
    // Note that the padding here is different from the padding in MyStandardLayout_Flat, but that doesn't change how num_* are layed out.
    MyStandardLayout_Sub nums;
    char mem_z;
};

Макет памяти nums в _Composite и три члена _Flat должны быть полностью изложены, поскольку применяются те же основные правила.

Итак, в заключении, учитывая, что "вспомогательный объект" num_1 to num_3 будет представлен эквивалентной последовательной последовательностью байтов как полный субобъект с возможностью копирования, I:

  • очень и очень трудно представить себе реализацию или оптимизатор, который разбивает этот
  • Можно сказать, что это может быть:
    • читайте как Undefined Поведение, iff, мы заключаем, что С++ §3.9/3 подразумевает, что разрешено обрабатывать только (полные) объекты Тривиально копируемого типа таким образом, на memcpy или заключить из C99§6.2.6.1/2 и общую спецификацию memcpy 7.21.2.1, что смежная последовательность num_ * bytes не содержит "действительный объект" для целей memcopy.
    • читайте как Определено Поведение, iff, мы заключаем, что С++ §3.9/3 не нормативно ограничивает применимость memcpy для других типов или диапазонов памяти и заключает, что определение memcpy (и "объектный термин" ) в стандарте C99 позволяет обрабатывать смежные переменные как целевой объект с непрерывным байтом.

Ответ 2

§8.5

(6.2) - если T является (возможно, cv-квалифицированным) классом неединичного класса, каждый нестатический член данных и каждый базовый класс подобъект инициализируется нулем, а заполнение инициализируется нулевыми битами;

Теперь стандарт фактически не говорит, что эти нулевые биты будут записываться, но я не могу думать о архитектуре, которая имеет этот уровень детализации в разрешениях доступа к памяти (и мы не хотим этого делать).

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

Ответ 3

правомерно интерпретировать диапазон памяти трех членов как "концептуальный" (?) char массив

Нет, произвольные подмножества элементов объектов сами по себе не являются объектом любого рода. Если вы не можете взять sizeof что-то, это не вещь. Аналогично, как было предложено ссылкой, которую вы предоставили, если вы не можете идентифицировать предмет std::is_standard_layout, это не вещь.

Аналогично будет

size_t n = (char*)&num_3 - (char*)&num_1;

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

Тем не менее, я думаю, что вы находитесь на безопасной территории, даже если стандарт не является явным. Если MyStandardLayout является стандартным макетом, то разумно предположить, что его подмножество, даже если оно не имеет имени и не является идентифицируемым типом.

Но я бы этого не сделал. Назначение абсолютно безопасно и потенциально быстрее, чем memcpy. Если подмножество имеет смысл и имеет много членов, я бы подумал о том, чтобы сделать его явной структурой и использовать присваивание вместо memcpy, воспользовавшись конструктором экземпляров по умолчанию, предоставленным компилятором.