Является ли memcpy тривиально копируемой конструкцией или присваиванием типа?

Допустим, у вас есть объект типа T и правильно выровненный буфер памяти alignas(T) unsigned char[sizeof(T)]. Если вы используете std::memcpy для копирования из объекта типа T в массив unsigned char, считается ли это построением копии или присваиванием копии?

Если тип легко копируется, но не является стандартным, вполне возможно, что такой класс:

struct Meow
{
    int x;
protected: // different access-specifier means not standard-layout
    int y;
};

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

struct Meow_internal
{
private:
    ptrdiff_t x_offset;
    ptrdiff_t y_offset;
    unsigned char buffer[sizeof(int) * 2 + ANY_CONSTANT];
};

Компилятор может хранить x и y Meow в буфере в любой части buffer, возможно, даже со случайным смещением в пределах buffer, если они правильно выровнены и не перекрываются. Смещение x и y может даже произвольно изменяться для каждой конструкции, если компилятор пожелает. (x может идти после y, если компилятор желает, потому что стандарт требует, чтобы члены одного и того же спецификатора доступа шли по порядку, а x и y имеют разные спецификаторы доступа.)

Это отвечало бы требованиям возможности тривиального копирования; a memcpy скопирует скрытые поля смещения, так что новая копия будет работать. Но некоторые вещи не работали. Например, удерживание указателя на x через memcpy приведет к поломке:

Meow a;
a.x = 2;
a.y = 4;
int *px = &a.x;

Meow b;
b.x = 3;
b.y = 9;
std::memcpy(&a, &b, sizeof(a));

++*px; // kaboom

Однако действительно ли компилятору разрешено реализовать таким образом тривиально копируемый класс? Разыменование px должно быть неопределенным, только если время жизни a.x закончилось. Есть это? Соответствующие части проекта стандарта N3797 не очень ясны по этому вопросу. Это раздел [basic.life] / 1:

время жизни объекта - это свойство объекта во время выполнения. Говорят, что объект имеет нетривиальную инициализацию, если он относится к классу или агрегатному типу и он или один из его членов инициализируется конструктором, отличным от тривиального конструктора по умолчанию. [Примечание: инициализация тривиальным конструктором копирования / перемещения является нетривиальной инициализацией. - конец примечания] Время жизни объекта типа T начинается, когда:

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

Время жизни объекта типа T заканчивается, когда:

  • если T - это тип класса с нетривиальным деструктором ([class.dtor]), запускается вызов деструктора, или
  • хранилище, которое занимает объект, повторно используется или освобождается.

А это [basic.types] / 3:

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

Тогда возникает вопрос, является ли перезапись тривиально копируемого экземпляра класса «копирующей конструкцией» или «копированием-присваиванием»? Ответ на вопрос, кажется, решает, является ли Meow_internal допустимым способом для компилятора реализовать тривиально копируемый класс Meow.

Если memcpy - это «копирующая конструкция», то ответ таков, что Meow_internal допустимо, потому что копирующая конструкция повторно использует память. Если memcpy - «копирование-присваивание», то ответ таков, что Meow_internal не является допустимой реализацией, потому что присвоение не делает недействительными указатели на созданные экземпляры членов класса. Если memcpy и то и другое, я понятия не имею, каков ответ.


person Myria    schedule 03.10.2014    source источник
comment
Если вы используете memcpy, то это не конструкция или назначение.   -  person M.M    schedule 03.10.2014
comment
Надеюсь, TC напишет ответ, IDK, каков статус объектов, созданных с использованием memcpy вместо конструктора :)   -  person M.M    schedule 03.10.2014
comment
На машинах с sizeof (int) = 4 тогда sizeof (Meow) обычно равно 8. В то время как sizeof (Meow_internal) не менее 16. Никто не будет использовать такой компилятор из-за дополнительного использования памяти.   -  person brian beuning    schedule 03.10.2014
comment
@brianbeuning, но будет ли он соответствовать стандарту?   -  person M.M    schedule 03.10.2014
comment
У меня сейчас нет доступа к Стандарту, но в нескольких черновиках [basic.types] / 3 содержится около двух объектов типа T. Кажется, это лучше подходит для примера кода, чем цитата о символьных массивах IMHO.   -  person dyp    schedule 03.10.2014
comment
Поскольку вы можете memcpy что-то, что не T, в T - что определенно считается повторным использованием хранилища и завершает время жизни объекта T - я не вижу причин, по которым memcpy включение T в T также не считается повторным использованием. И я согласен с @brianbeuning, что обсуждать стандартное соответствие гипотетического компилятора, который ни один здравомыслящий человек никогда бы не написал или не использовал, бессмысленно.   -  person T.C.    schedule 03.10.2014
comment
Я думаю, Meow_internal нарушает [basic.life] / 7, если ваш компилятор не изменяет указатель px, если мы заменим memcpy на new((void*)&a) Meow(b);. (Хотя это может быть тонко: px указывает на неполный объект; нужно было сделать вывод из других источников, что впоследствии он должен указывать на объект того же типа и т. Д. Но я думаю, что это намерение Стандарта.)   -  person dyp    schedule 03.10.2014
comment
@ T.C. Причина, по которой я задаю этот вопрос, заключается в том, что если Meow_internal является незаконной реализацией, это означает, что нет технической основы для ограничения Стандарта, что offsetof требует структуры стандартного макета. Можно было бы формально доказать, что возможность простого копирования будет достаточно для поддержки offsetof, и оправдать изменение Стандартом его определений в результате.   -  person Myria    schedule 03.10.2014
comment
@dyp Я сомневаюсь, что это сломается. px не указывает на объект типа T; он указывает на подобъект, и, насколько я могу судить, нет никакой гарантии, что при повторном использовании хранилища указатели объекта на его подобъекты останутся действительными (конечно, он также повторно использует хранилище *px, но нет гарантии, что это повторное использование также удовлетворяет другим требованиям [basic.life] / 7).   -  person T.C.    schedule 03.10.2014
comment
@dyp Компилятор не может настроить указатели для вас за вашей спиной, потому что вы можете reinterpret_cast<std::uintptr_t>(px), выполнить XOR полученного беззнакового целого числа со случайным числом, полученным от /dev/urandom, установить px в nullptr, а затем выполнить memcpy. После завершения memcpy используйте reinterpret_cast<int *>(encrypted_uintptr) для восстановления исходного значения указателя (допустимо [expr.reinterpret.cast] / 5). Компилятор не может узнать, что вы скрыли указатель. (Однако это не будет указатель безопасно извлеченный с помощью [basic.stc.dynamic.safety] / 3).   -  person Myria    schedule 03.10.2014
comment
Один из способов ужесточить это - объявить, что memcpy аннулирует все указатели, указывающие на перезаписанную память, за исключением указателей на начало области, если цель или место назначения содержат нестандартные классы макета.   -  person Lie Ryan    schedule 03.10.2014
comment
@LieRyan Если один из членов нестандартного макета, но тривиально копируемого класса является char или unsigned char, и вы сохраняете указатель на него, ясно, что some элемент массива резервного хранилища будет сравнивать равный этому указателю. Так что говорить, что указатели недействительны, неверно. Возможно, тогда будет правильнее сказать, что их можно использовать ограниченным образом, как в [basic.life] / 5?   -  person Myria    schedule 03.10.2014
comment
Интересно подумать, почему offsetof`` would interact with this. It is supposed to work with standard-layout classes, so either offsetof` нереализуем или ваш гипотетический компилятор нарушает что-то еще. Обратите внимание, что offsetof - это макрос, который оценивает смещение члена в байтах с учетом имени класса (не экземпляра), подразумевая, что гипотетический компилятор OP не может полностью реализовать стандарт, потому что offsetof было бы невозможно . cplusplus.com/reference/cstddef/offsetof   -  person Ben    schedule 30.04.2015
comment
@Ben Class Meow не является стандартным макетом, поэтому offsetof не требуется для работы с ним. Однако цель приведенного выше упражнения - указать на то, что мне кажется глупым в Стандарте. Идея состоит в том, чтобы показать, что совместимая реализация компилятора, в которой тривиально копируемый (т. Е. memcpy-совместимый) класс не обязательно offsetof-совместим, либо противоречит, либо настолько абсурдна, что никогда не будет реализована. Таким образом, было бы оправдано изменить Стандарт, указав, что offsetof разрешено для тривиально копируемых типов, а не только для типов стандартного макета.   -  person Myria    schedule 30.04.2015
comment
Конечно. Это интересный безумный угловой случай. Я хочу сказать, что существование offsetof, похоже, подразумевает, что смещение члена должно быть одинаковым от экземпляра к экземпляру, что нарушает ваш пример и делает ваш гипотетический компилятор неявно несовместимым, я думаю. Вы бы согласились?   -  person Ben    schedule 01.05.2015
comment
Возможно, это не совсем четко определено в Стандарте. Рассмотрим N3751 и соответствующее обсуждение в списке рассылки UB.   -  person dyp    schedule 01.05.2015
comment
@dyp: если бы стандарт распознал, что любая живая область хранения, которая не содержит объект нетривиального типа, содержит все объекты тривиального типа, которые могут поместиться в него, даже если такие объекты не всегда будут доступны, это исправит множество угловых случаев, хотя они не всегда могут быть доступны, по-прежнему допускают оптимизацию на основе типов. Представление о том, что компилятор должен волшебным образом читать мысли программиста, чтобы обрабатывать memcpy осмысленно, является следствием неработающей абстракции, когда время жизни тривиальных объектов начинается и заканчивается отдельно от их хранилища.   -  person supercat    schedule 19.09.2020
comment
Если, например, union foo содержит элементы структуры s1 и s2 с общей начальной последовательностью, такая модель проясняет, что будет доступно, если код прочитает lvalue foo.s2.commonMember после записи foo.s1.commonMember. Запись foo.s1.commonMember может сделать foo.s2 недоступным, но разрешение lvalue foo.s2 сделает доступным этот уже существующий член, не завершая время жизни foo.s1 и не делая его недоступным.   -  person supercat    schedule 19.09.2020


Ответы (2)


Мне ясно, что использование std::memcpy не приводит ни к построению, ни к присваиванию. Это не конструкция, поскольку конструктор не вызывается. Это также не присвоение, поскольку оператор присваивания не будет вызываться. Учитывая, что тривиально копируемый объект имеет тривиальные деструкторы, конструкторы (копирование / перемещение) и операторы присваивания (копирование / перемещение), вопрос довольно спорный.

Похоже, вы процитировали 2 из 3.9 [basic.types]. На 3 он гласит:

Для любого тривиально копируемого типа T, если два указателя на T указывают на разные T объекты obj1 и obj2, где ни obj1, ни obj2 не являются подобъектами базового класса, если базовые байты (1.7), составляющие obj1, копируются в _10 _, 41 obj2 впоследствии будет иметь то же значение, что и obj1. [Пример:
T* t1p;
T* t2p;
// при условии, что t2p указывает на инициализированный объект ...
std::memcpy(t1p, t2p, sizeof(T));
// в этот момент каждый подобъект тривиально копируемого типа в *t1p содержит
// то же значение, что и соответствующий подобъект в *t2p
- конечный пример]
41) Используя, например, библиотечные функции (17.6.1.2) std::memcpy или std::memmove.

Ясно, что стандарт, предназначенный для того, чтобы *t1p можно было использовать любым *t2p способом.

Продолжая до 4:

объектное представление объекта типа T - это последовательность N беззнаковых char объектов, занятых объектом типа T, где N равно sizeof(T). представление значения объекта - это набор битов, содержащих значение типа T. Для тривиально копируемых типов представление значения - это набор битов в представлении объекта, который определяет значение, которое является одним дискретным элементом набора значений, определяемого реализацией. 42
42) Намерение состоит в том, чтобы модель памяти C ++ была совместима с моделью памяти языка программирования C ISO / IEC 9899

Использование слова the перед обоими определенными терминами означает, что любой данный тип имеет только одно представление объекта. и данный объект имеет только одно представление значения. Ваш гипотетический внутренний тип морфинга не должен существовать. В сноске проясняется, что намерение состоит в том, чтобы тривиально копируемые типы имели компоновку памяти, совместимую с C. Ожидается, что даже объект с нестандартной компоновкой, копирующий его, по-прежнему позволит его использовать.

person jxh    schedule 08.05.2015
comment
Given that a trivially copyable object has trivial destructors, constructors, and assignment operators Только конструкторы копирования и перемещения должны быть тривиальными. Тривиально копируемые типы могут иметь нетривиальные «нормальные» конструкторы. Возможно, вы думаете о POD, у которых не может быть конструкторов, но которые являются более строгим надмножеством тривиально копируемых. - person underscore_d; 15.07.2016
comment
@jxh Что ж, я поясняю, что, хотя вы сказали «тривиальные конструкторы», не указав какие, только конструкторы copy и move должны быть тривиальными для тривиально копируемого статуса. Нетривиальные конструкторы "нормального" (признаюсь, я не уверен, есть ли для этого официальный термин), т.е. подписи без копирования / перемещения разрешены для тривиально копируемых типов. Это агрегированные и, следовательно, типы POD, которые не могут иметь никаких нетривиальных конструкторов. Вам решать, отредактировать ли вы это в своем ответе, но я думаю, что это можно было бы улучшить. - person underscore_d; 15.07.2016
comment
@jxh круто, и хороший улов о том, как операции присваивания следуют тому же шаблону; Я сразу заговорил о конструкторах и упустил из виду задание! что странно, поскольку я интенсивно использую присвоение преобразования для тривиально копируемых типов. Если бы я мог быть педантичным, я бы снял скобки, так как некоторые люди могли бы интерпретировать это как «включая», а не «только» :) - person underscore_d; 16.07.2016

В том же черновике вы также найдете следующий текст, следующий непосредственно за цитируемым вами текстом:

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

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

person celtschk    schedule 08.05.2015
comment
Это означало бы, что Meow_internal не соответствует стандарту реализации Meow. Я согласен с этой интерпретацией. Однако следствием этого является то, что различие в Стандарте между тривиально копируемым и стандартным макетом немного размыто. Насколько я могу судить, offsetof "должен" концептуально работать с тривиально копируемыми типами в дополнение к типам стандартной компоновки, иначе реализация явно несовместима по другим причинам. - person Myria; 15.05.2015