Memcpy/memmove члену объединения, устанавливает ли это 'активный' член?
Важное пояснение: некоторые комментаторы, похоже, думают, что я копирую из профсоюза. Посмотрите внимательно на memcpy
, он копирует с адреса простого старого uint32_t
, который не содержится в объединении. Кроме того, я копирую (через memcpy
) конкретный член объединения (u.a16
или &u.x_in_a_union
, а не непосредственно ко всему самому объединению (&u
)
С++ довольно жестко относится к объединениям - вы должны читать от члена только в том случае, если это был последний элемент, который был написан для:
9.5. Unions [class.union] [[С++ 11]] В объединении не более одного нестатического элемента данных могут быть активны в любое время, то есть значение не более одного из нестатические элементы данных могут быть сохранены в объединении в любое время.
(Конечно, компилятор не отслеживает, какой член активен. Это зависит от разработчика, чтобы убедиться, что они сами отслеживают это)
Обновление: этот следующий блок кода является основным вопросом, непосредственно отражающим текст в заголовке вопроса. Если этот код в порядке, у меня есть продолжение относительно других типов, но теперь я понимаю, что этот первый блок кода интересен сам.
#include <cstdint>
uint32_t x = 0x12345678;
union {
double whatever;
uint32_t x_in_a_union; // same type as x
} u;
u.whatever = 3.14;
u.x_in_a_union = x; // surely this is OK, despite involving the inactive member?
std::cout << u.x_in_a_union;
u.whatever = 3.14; // make the double 'active' again
memcpy(&u.x_in_a_union, &x); // same types, so should be OK?
std::cout << u.x_in_a_union; // OK here? What the active member?
Блок кода непосредственно над этим, вероятно, является главной проблемой в комментариях и ответах. Оглядываясь назад, мне не нужно было смешивать типы в этом вопросе! В принципе, u.a = b
совпадает с memcpy(&u.a,&b, sizeof(b))
, если типы идентичны?
Во-первых, относительно простой memcpy
, позволяющий нам читать uint32_t
как массив uint16_t
:
#include <cstdint> # to ensure we have standard versions of these two types
uint32_t x = 0x12345678;
uint16_t a16[2];
static_assert(sizeof(x) == sizeof(a16), "");
std:: memcpy(a16, &x, sizeof(x));
Точное поведение зависит от контентоспособности вашей платформы, и вы должны остерегаться ловушек и т.д. Но в целом это согласуется с этим (я думаю, что отзывы заслуживают!), Что, с осторожностью, чтобы избежать проблемных значений, приведенный выше код может быть абсолютно стандартным жалобой в правильном контексте на правильной платформе.
(Если у вас есть проблема с указанным выше кодом, пожалуйста, прокомментируйте или отредактируйте вопрос соответственно. Я хочу быть уверенным, что у нас есть не противоречивая версия выше, прежде чем перейти к "интересному" коду ниже.)
Если и только если оба блока кода выше не являются UB, я хотел бы объединить их следующим образом:
uint32_t x = 0x12345678;
union {
double whatever;
uint16_t a16[2];
} u;
u.whatever = 3.14; // sets the 'active' member
static_assert(sizeof(u.a16) == sizeof(x)); //any other checks I should do?
std:: memcpy(u.a16, &x, sizeof(x));
// what is the 'active member' of u now, after the memcpy?
cout << u.a16[0] << ' ' << u.a16[1] << endl; // i.e. is this OK?
Какой член объединения, u.whatever
или u.a16
, является "активным членом"?
Наконец, моя собственная догадка заключается в том, что причина, по которой мы в этом заботимся, на практике заключается в том, что оптимизирующий компилятор может не заметить, что произошел
memcpy
и, следовательно, сделать ложные предположения (но допустимые допущения по стандарту) о том, какой член активен и какие типы данных являются "активными", что приводит к ошибкам в области псевдонимов. Компилятор может изменить порядок
memcpy
странными способами. Является ли это подходящим резюме того, почему мы заботимся об этом?
Ответы
Ответ 1
Мое чтение стандарта заключается в том, что std::memcpy
является безопасным, когда тип тривиально копируется.
Из 9 классов мы можем видеть, что union
являются типами классов, и к ним применимо тривиально скопированное.
Объединение - это класс, определенный с помощью объединения классов классов; он содержит только один элемент данных за раз (9.5).
Тривиально-скопируемый класс - это класс, который:
- не имеет нетривиальных конструкторов копирования (12.8),
- не имеет нетривиальных конструкторов перемещения (12.8),
- не имеет нетривиальных операторов присваивания копий (13.5.3, 12.8),
- не имеет нетривиальных операторов присваивания перемещения (13.5.3, 12.8) и
- имеет тривиальный деструктор (12.4).
Точное значение тривиально-скопируемого приведено в 3.9. Типы:
Для любого объекта (кроме субобъекта базового класса) тривиально-скопируемого типа T
, независимо от того, имеет ли объект допустимое значение типа T
, базовые байты (1.7), составляющие объект, могут быть скопированы в массив char
или unsigned char
. Если содержимое массива char
или unsigned char
будет скопировано обратно в объект, объект впоследствии сохранит свое исходное значение.
Для любого тривиально-скопируемого типа T
, если два указателя на T
указывают на различные T
объекты obj1
и obj2
, где ни obj1
, ни obj2
не является подобъектом базового класса, если базовые байты (1.7), составляющие obj1
, скопированы в obj2
, obj2
впоследствии будет иметь то же значение, что и obj1
.
Стандарт также дает явный пример того и другого.
Итак, если вы копируете весь союз, ответ будет однозначно да, активный член будет "скопирован" вместе с данными. (Это важно, потому что это означает, что std::memcpy
следует рассматривать как допустимое средство для изменения активного элемента объединения, поскольку его использование явно разрешено для всего объединения копирования.)
Теперь вы вместо этого копируете член союза. Стандарт, как представляется, не требует какого-либо конкретного метода назначения члену профсоюза (и, следовательно, его активации). Все, что он делает, это указать (9.5), что
[Примечание. В общем, нужно использовать явный класс деструктора и назначить новые операторы для изменения активного члена объединения. - конечная нота]
о котором он говорит, конечно, потому что С++ 11 допускает объекты нетривиального типа в объединениях. Обратите внимание на "в целом" на фронте, что совершенно ясно указывает на то, что в определенных случаях допустимы другие способы изменения активного члена; мы уже знаем, что это так, потому что назначение явно разрешено. Конечно, запрет на использование std::memcpy
, где его использование в противном случае было бы правильным.
Итак, мой ответ - да, это безопасно, и да, он меняет активный член.
Ответ 2
Не более одного члена объединения может быть активным и активным в течение его жизни.
В стандарте С++ 14 (§ 9.3 или 9.5 в черновике) все члены нестатического объединения распределяются так, как если бы они были единственным членом struct
и имели один и тот же адрес. Это не начинается с жизни, но существует нетривиальный конструктор по умолчанию (который может иметь только один член объединения). Существует специальное правило, которое назначает члену профсоюза, активирует его, даже если вы не могли нормально это сделать с объектом, чье жизненное время еще не началось. Если союз тривиален, он и его члены не могут не беспокоиться о нетривиальных деструкторах. В противном случае вам нужно беспокоиться о том, когда закончится время жизни активного члена. Из стандарта (§ 3.8.5):
Программа может завершить время жизни любого объекта, повторно используя хранилище, которое занимает объект, или явно вызвав деструктор для объекта типа класса с нетривиальным деструктором. [... I] f нет явного вызова деструктора или если выражение удаления не используется для освобождения хранилища, деструктор не должен быть неявно вызван, и любая программа, зависящая от побочных эффектов, создаваемых деструктором, undefined.
В общем случае безопаснее явно называть деструктор действующего в данный момент члена и активировать другой член с размещением new
. Стандарт дает пример:
u.m.~M();
new (&u.n) N;
Во время компиляции вы можете проверить, нужна ли первая строка с помощью std::is_trivially_destructible
. При строгом чтении стандарта вы можете начать только время жизни члена профсоюза, инициализируя объединение, присваивая ему или размещение new
, но как только вы это сделаете, вы можете безопасно скопировать объект, который можно копировать с возможностью копирования, поверх другого, используя memcpy()
. (§ 3.9.3, 3.8.8)
Для тривиально-копируемых типов представление значения представляет собой набор бит в представлении объекта, который определяет значение, а интерпретация объекта T представляет собой последовательность объектов sizeof(T)
unsigned char
. Функция memcpy()
копирует это представление объекта. Все члены нестатического объединения имеют один и тот же адрес, и вы можете использовать этот адрес в качестве void*
для хранения после его выделения и до начала жизни объектов (§ 3.8.6), поэтому вы можете передать его на memcpy()
когда член неактивен. Если союз является стандартным макетом, адрес самого союза совпадает с адресом его первого нестатического элемента, и поэтому все они. (Если нет, то это взаимно обратимо с static_cast
.)
Если тип has_unique_object_representations
, он тривиально-копируемый, и никакие два разных значения не имеют одинакового представления объекта; то есть никакие биты не заполняются.
Если тип is_pod
(Обычные старые данные), то он тривиально-копируемый и имеет стандартный макет, поэтому его адрес также совпадает с адресом его первого нестатического элемента.
В C у нас есть гарантия, что мы можем прочитать неактивные члены объединения совместимого типа с последним написанным. В С++ мы этого не делаем. Существует несколько особых случаев, когда он работает, например указатели, содержащие адреса объектов того же типа, подписанные и неподписанные интегральные типы одинаковой ширины и структуры, совместимые с макетами. Тем не менее, типы, которые вы использовали в вашем примере, имеют некоторые дополнительные гарантии: если они вообще существуют, uint16_t
и uint32_t
имеют точную ширину и отсутствие заполнения, каждое представление объекта является уникальным значением, а все элементы массива смежны в памяти, поэтому любое представление объекта uint32_t
также является допустимым объектным представлением некоторого uint16_t[2]
, хотя это представление объекта технически undefined. То, что вы получаете, зависит от сущности. (Если вы действительно хотите безопасно разбить 32 бита, вы можете использовать бит-сдвиги и битмаски.)
Чтобы обобщить, если исходный объект is_pod
, то его можно скопировать строго по его представлению объекта и поместить поверх другого совместимого с макета объекта по новому адресу, а если целевой объект имеет одинаковый размер и has_unique_object_representations
, он также тривиально копируется и не будет выбрасывать ни один из битов, однако может быть представление ловушки. Если ваш союз не является тривиальным, вам нужно удалить активный член (только один член нетривиального объединения может иметь нетривиальный конструктор по умолчанию, и он будет активным по умолчанию) и используйте размещение new
, чтобы сделать целевого участника.
Всякий раз, когда вы копируете массивы на C или С++, вы всегда хотите проверить переполнение буфера. В этом случае вы приняли мое предложение и использовали static_assert()
. Это не требует дополнительных затрат времени. Вы также можете использовать memcpy_s()
: memcpy_s( &u, sizeof(u), &u32, sizeof(u32) );
будет работать, если источником и получателем являются POD (тривиально-копируемый со стандартным макетом), и если союз имеет стандартный макет. Он никогда не будет переполнять или ниспровергать союз. Он выложит все оставшиеся байты объединения с нулями, что может сделать много ошибок, которые вы беспокоитесь о видимых и воспроизводимых.
Ответ 3
[class.union]/5:
В объединении нестатический член данных активен, если его имя относится к объекту, чье жизненное время началось и не закончилось ([basic.life]). Не более чем один из нестатических элементов данных объекта типа объединения может быть активным в любое время, то есть значение не более одного из нестатических элементов данных может быть сохранено в объединении в любое время.
Не более одного члена объединения может быть активным в любой момент времени.
Активным членом является тот, чье жизненное время началось и не закончилось.
Таким образом, если вы закончите срок жизни члена вашего объединения, он больше не активен.
Если у вас нет активных членов, что приводит к тому, что время жизни другого члена союза будет хорошо определено под стандартом и заставляет его активироваться.
Союз выделил хранилище, достаточное для всех его членов. Все они распределены так, как если бы они были одни, а они взаимно обратимы. [class.union]/2
.
[basic.life]/6
До того, как время жизни объекта запустилось, но после того, как хранилище, которое будет занимать объект, было выделено 40 или, после того как время жизни объекта закончилось и перед хранилищем, которое было занято объектом повторно используемый или выпущенный, может использоваться любой указатель, который представляет адрес места хранения, в котором находится или находится объект, но только ограниченным образом. Строку или разрушение объекта см. В разделе [class.cdtor]. В противном случае такой указатель относится к выделенному хранилищу ([basic.stc.dynamic.deallocation]) и с помощью указателя, как если бы указатель имел тип void *, четко определен.
Таким образом, вы можете взять указатель на член объединения и рассматривать его как указатель на выделенное хранилище. Такой указатель может быть использован для построения объекта там, если такая конструкция является законной.
Размещение new - это допустимый способ построения объекта. memcpy
тривиально-скопируемых типов (включая типы POD) является допустимым способом построения там объекта.
Но, построение объекта существует только в том случае, если оно не нарушает правило наличия одного активного члена объединения.
Если вы назначаете член союза при определенных условиях [class.union]/6
, он сначала заканчивает время жизни текущего активного члена, а затем запускает время жизни назначенного члена. Таким образом, ваш u.u32_in_a_union = 0xaaaabbbb;
является законным, даже если в объединении есть другой член (и он активирует u32_in_a_union
).
Это не относится к размещению new или memcpy
, в спецификации объединения нет явного "времени жизни активного члена". Мы должны искать в другом месте:
[basic.life]/5
Программа может завершить время жизни любого объекта за счет повторного использования хранилища, которое занимает объект, или путем явного вызова деструктора для объекта типа класса с нетривиальным деструктором.
Вопрос: начинается ли жизнь другого члена союза "повторное использование хранилища", тем самым заканчивая время жизни других членов профсоюза? На практике, очевидно (они являются взаимопревращаемыми указателями, они имеют один и тот же адрес и т.д.). [class.union]/2
.
Поэтому я бы сказал, что да.
Таким образом, создание другого объекта с помощью указателя void*
(размещение new или memcpy
, если оно является законным для этого типа) заканчивает время жизни альтернативных членов union
(если они есть) (не вызывая их деструктор, но обычно это нормально), и делает объект-указатель активным и живым сразу.
Право на начало жизненного цикла double
или массив int16_t
или аналогичный с помощью memcpy
для хранения.
Законность копирования массива из двух uint16_t
поверх uint32_t
или наоборот. Я оставлю другим спорить. По-видимому, это законно на С++ 17. Но этот объект, являющийся союзом, не имеет ничего общего с этой законностью.
Этот ответ основан на обсуждении с @Lorehead ниже их ответа. Я чувствовал, что должен дать ответ, который прямо направлен на то, что я считаю суть проблемы.
Ответ 4
Слон в комнате: союзы вообще не поддерживаются в полном строгом C++, "языке", который вы получаете, когда пытаетесь применить все стандартные условия неудачной попытки формализовать интуицию C++ называется стандартом.
Это потому что:
- lvalue относится к объекту,
- членский доступ (
x.m
) является обычным значением для любого класса или объединения,
- все члены живого класса или профсоюза могут быть назначены в любое время доступом участника,
- в соответствии со строгими правилами жизни в объединении может быть только один объект-член,
- понятие lvalue, относящееся к скоро создаваемому объекту, не определено в стандарте.
Таким образом, простое использование союза, как:
union {
char c;
int i;
} u;
u.i = 1;
не имеет определенного поведения, потому что результат оценки u.i
не может ссылаться ни на один объект int
, так как на момент оценки такого объекта нет.
Комитет C++ не справился со своей задачей.
На самом деле никто не использует полный строгий C++ для каких-либо целей, людям необходимо отклонить целые части стандарта или составить целые воображаемые предложения, вдохновленные письменным текстом, или вернуться от текста к намерению, которое они представляют, а затем повторно формализовать намерение, чтобы понять это.
Разные люди отбрасывают разные части и заканчивают с совершенно разными формализмами.
Мое предложение состоит в том, чтобы отклонить правила жизни и иметь объект по любому адресу, который может содержать такой объект. Это решает всю проблему, и никто никогда не выдвигал обоснованного возражения против подхода (смутные утверждения, что "это нарушает все инварианты" не является обоснованным возражением). Наличие объекта по любому действительному адресу просто создает бесконечное количество потенциальных объектов (в частности, всех типов указателей, int*
, int**
, int***
...), но их нельзя использовать для чтения, поскольку не было записано допустимое значение.
Обратите внимание, что без этого ослабления правила жизни или определения lvalues у вас не будет даже нетривиального "правила строгого алиасинга", так как это правило не будет применяться к четко определенной программе без этого правила. Как интерпретируется в настоящее время, "строгое правило псевдонимов" бесполезно. (Также это так плохо написано, что никто не знает, что это значит.)
Или, может быть, кто-то скажет мне, что для понимания строгого правила наложения имен lvalue int
относится к объекту, просто другого типа. Это было бы настолько удивительно и глупо, что даже если вы будете так последовательно интерпретировать стандарт, я все равно скажу, что он нарушен.