可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Important clarification: some commenters seem to think that I am copying from a union. Look carefully at the memcpy
, it copies from the address of a plain old uint32_t
, which is not contained within a union. Also, I am copying (via memcpy
) to a specific member of a union (u.a16
or &u.x_in_a_union
, not directly to the entire union itself (&u
)
C++ is quite strict about unions - you should read from a member only if that was the last member that was written to:
9.5 Unions [class.union] [[c++11]] In a union, at most one of the non-static data members can be active at any time, that is, the value of at most one of the non-static data members can be stored in a union at any time.
(Of course, the compiler doesn't track which member is active. It's up to the developer to ensure they track this themselves)
Update: This following block of code is the main question, directly reflecting the text in the question title. If this code is OK, I have a follow up regarding other types, but I now realize that this first block of code is interesting itself.
#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's the active member?
The block of code immediately above this is probably the main issue in the comments and answers. In hindsight, I didn't need to mix types in this question! Basically, is u.a = b
the same as memcpy(&u.a,&b, sizeof(b))
, assuming the types are identical?
First, a relatively simple memcpy
allowing us to read a uint32_t
as an array of 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));
The precise behaviour depends on the endianness of your platform, and you must beware of trap representations and so on. But it is generally agreed here (I think? Feedback appreciated!) that, with care to avoid problematic values, the above code can be perfectly standards-complaint in the right context on the right platform.
(If you have a problem with the above code, please comment or edit the question accordingly. I want to be sure we have a non-controversial version of the above before proceeding to the "interesting" code below.)
If, and only if, both blocks of code above are not-UB, then I would like to combine them as follows:
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?
Which member of the union, u.whatever
or u.a16
, is the 'active member'?
Finally, my own guess is that the reason why we care about this, in practice, is that an optimizing compiler might fail to notice that the memcpy
happened and therefore make false assumptions (but allowable assumptions, by the standard) about which member is active and which data types are 'active', therefore leading to mistakes around aliasing. The compiler might reorder the memcpy
in strange ways. Is this an appropriate summary of why we care about this?
回答1:
My reading of the standard is that std::memcpy
is safe whenever the type is trivially copyable.
From 9 Classes, we can see that union
s are class types and so trivially copyable applies to them.
A union is a class defined with the class-key union; it holds only one data member at a time (9.5).
A trivially copyable class is a class that:
- has no non-trivial copy constructors (12.8),
- has no non-trivial move constructors (12.8),
- has no non-trivial copy assignment operators (13.5.3, 12.8),
- has no non-trivial move assignment operators (13.5.3, 12.8), and
- has a trivial destructor (12.4).
The exact meaning of trivially copyable is given in 3.9 Types:
For any object (other than a base-class subobject) of trivially copyable type T
, whether or not the object holds a valid value of type T
, the underlying bytes (1.7) making up the object can be copied into an array of char
or unsigned char
. If the content of the array of char
or unsigned char
is copied back into the object, the object shall subsequently hold its original value.
For any trivially copyable type T
, if two pointers to T
point to distinct T
objects obj1
and obj2
, where neither obj1
nor obj2
is a base-class subobject, if the underlying bytes (1.7) making up obj1
are copied into obj2
, obj2
shall subsequently hold the same value as obj1
.
The standard also gives an explicit example of both.
So, if you were copying the entire union, the answer would be unequivocally yes, the active member will be "copied" along with the data. (This is relevant because it indicates that std::memcpy
must be regarded as a valid means of changing the active element of a union, since using it is explicitly allowed for whole union copying.)
Now, you are instead copying into a member of the union. The standard doesn't appear to require any particular method of assigning to a union member (and hence making it active). All it does is specify (9.5) that
[ Note: In general, one must use explicit destructor class and placement new operators to change the active member of a union. — end note]
which it says, of course, because C++11 allows objects of non-trivial type in unions. Note the "in general" on the front, which quite clearly indicates that other methods of changing the active member are permissible in specific cases; we already know this to be the case because assignment is clearly permitted. Certainly there is no prohibition on using std::memcpy
, where its use would otherwise be valid.
So my answer is yes, this is safe, and yes, it changes the active member.
回答2:
At most one member of a union can be active, and it is active during its lifetime.
In the C++14 standard (§ 9.3, or 9.5 in the draft), all non-static union members are allocated as if they were the sole member of a struct
, and share the same address. This does not begin the lifetime, but a non-trivial default constructor (which only one union member can have) does. There is a special rule that assigning to a union member activates it, even though you could not normally do this to an object whose lifetime has not begun. If the union is trivial, it and its members have no non-trivial destructors to worry about. Otherwise, you need to worry about when the lifetime of the active member ends. From the standard (§ 3.8.5):
A program may end the lifetime of any object by reusing the storage which the object occupies or by explicitly calling the destructor for an object of a class type with a non-trivial destructor. [... I]f there is no explicit call to the destructor or if a delete-expression is not used to release the storage, the destructor shall not be implicitly called and any program that depends on the side effects produced by the destructor has undefined behavior.
It is safer in general to explicitly call the destructor of the currently-active member, and make another member active with placement new
. The standard gives the example:
u.m.~M();
new (&u.n) N;
You can check at compile time whether the first line is necessary with std::is_trivially_destructible
. By a strict reading of the standard, you can only begin the lifetime of a union member by initializing the union, assigning to it, or placement new
, but once you have, you can safely copy a trivially-copyable object over another using memcpy()
. (§ 3.9.3, 3.8.8)
For trivially-copyable types, the value representation is a set of bits in the object representation that determines the value, and the object interpretation of T is a sequence of sizeof(T)
unsigned char
objects. The memcpy()
function copies this object representation. All non-static union members have the same address, and you can use that address as a void*
to storage after it has been allocated and before the object’s lifetime begins (§ 3.8.6), so you can pass it to memcpy()
when the member is inactive. If the union is a standard-layout union, the address of the union itself is the same as the address of its first non-static member, and therefore all of them. (If not, it is interconvertible with static_cast
.)
If a type has_unique_object_representations
, it is trivially-copyable, and no two distinct values share the same object representation; that is, no bits are padding.
If a type is_pod
(Plain Old Data), then it is trivially-copyable and has standard layout, so its address is also the same as the address of its first non-static member.
In C, we have a guarantee that we can read inactive union members of a compatible type to the last one written. In C++, we do not. There are a few special cases where it works, such as pointers containing addresses of objects of the same type, signed and unsigned integral types of the same width, and layout-compatible structures. However, the types you used in your example have some extra guarantees: if they exist at all, uint16_t
and uint32_t
have exact widths and no padding, each object representation is a unique value, and all array elements are contiguous in memory, so any object representation of a uint32_t
is also a valid object representation of some uint16_t[2]
even though this object representation is technically undefined. What you get depends on endianness. (If you actually want to split up 32 bits safely, you can use bit shifts and bitmasks.)
To generalize, if the source object is_pod
, then it can be copied strictly by its object representation and laid over another layout-compatible object at the new address, and if the destination object is the same size and has_unique_object_representations
, it is trivially-copyable as well and will not throw away any of the bits—however, there might be a trap representation. If your union is not trivial, you need to delete the active member (only one member of a non-trivial union can have a non-trivial default constructor, and it will be active by default) and use placement new
to make the target member active.
Whenever you copy arrays in C or C++, you always want to check for buffer overflow. In this case, you took my suggestion and used static_assert()
. This has no run-time overhead. You can also use memcpy_s()
: memcpy_s( &u, sizeof(u), &u32, sizeof(u32) );
will work if the source and destination are POD (trivially-copyable with standard layout) and if the union has standard layout. It will never overflow or underflow a union. It will pad out any remaining bytes of the union with zeroes, which can make a lot of the bugs you’re worried about visible and reproducible.
回答3:
[class.union]/5:
In a union, a non-static data member is active if its name refers to an object whose lifetime has begun and has not ended ([basic.life]). At most one of the non-static data members of an object of union type can be active at any time, that is, the value of at most one of the non-static data members can be stored in a union at any time.
At most one member of a union can be active at any one time.
An active member is one whose lifetime has begun and not ended.
Thus, if you end the lifetime of a member of your union, it is no longer active.
If you have no active members, causing the lifetime of another member of the union to begin is well-defined under the standard, and causes it to become active.
The union has allocated storage sufficient for all of its members. They all are allocated as if they where alone, and they are pointer-interconvertible. [class.union]/2
.
[basic.life]/6
Before the lifetime of an object has started but after the storage which the object will occupy has been allocated40 or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that represents the address of the storage location where the object will be or was located may be used but only in limited ways. For an object under construction or destruction, see [class.cdtor]. Otherwise, such a pointer refers to allocated storage ([basic.stc.dynamic.deallocation]), and using the pointer as if the pointer were of type void*, is well-defined.
So you can take a pointer to a union member and treat it as a pointer to allocated storage. Such a pointer may be used to construct an object there, if such a construction is legal.
Placement new is a valid way to construct an object there. memcpy
of trivially copyable types (including POD types) is a valid way to construct an object there.
But, constructing an object there is only valid if it does not violate the rule of there being one active member of the union.
If you assign to a member of a union under certain conditions [class.union]/6
it first ends the lifetime of the currently active member, then starts the lifetime of the assigned-to member. So your u.u32_in_a_union = 0xaaaabbbb;
is legal even if there is another member active in the union (and it makes u32_in_a_union
active).
This isn't the case with placement new or memcpy
, there is no explicit "the lifetime of the active member end" in the union specification. We must look elsewhere:
[basic.life]/5
A program may end the lifetime of any object by reusing the storage which the object occupies or by explicitly calling the destructor for an object of a class type with a non-trivial destructor.
The question is, is starting the lifetime of a different member of the union "reusing the storage", thus ending the other union members lifetime? In practice, obviously (they are pointer-interconvertable, they share the same address, etc). [class.union]/2
.
So I would argue yes.
So creating another object through a void*
pointer (placement new, or memcpy
if legal for the type) ends the lifetime of the alternative members of the union
(if any) (not calling their destructor, but that is usually ok), and makes the pointed-to object active and alive, at once.
It is legal to begin the lifetime of a double
or an array of int16_t
or similar via memcpy
over storage.
The legality of copying an array of two uint16_t
over an uint32_t
or vice versa I will leave to others to argue. Apparently it is legal in C++17. But this object being a union has nothing to do with that legality.
This answer is based off of discussion with @Lorehead below their answer. I felt I should provide an answer that aims directly at I think the core of the problem.
回答4:
The elephant in the room: unions are not supported at all in complete strict C++, the "language" that you get when you try to apply all the standard clauses of the failed attempt at formalizing the intuition of C++ called the standard.
This is because:
- an lvalue refers to an object,
- a member access (
x.m
) is a normal lvalue for any class or union,
- all members of a live class or union can be designated at any time by a member access,
- according to the strict lifetime rules, only one member object can be alive in a union,
- the notion of an lvalue referring to a soon to be created object is not defined in the standard.
So a simple use of a union like:
union {
char c;
int i;
} u;
u.i = 1;
has no defined behavior because the result of the evaluation of u.i
can't refer to any int
object, as there is no such object at the time of evaluation.
The C++ committee failed at its mission.
In fact nobody uses complete strict C++ for any purpose, people need to dismiss whole parts of the standard or make up whole imaginary clauses inspired by the written text, or go back from the text to the intent they imagine, then re-formalize the intent, to make sense of it.
Different people dismiss different parts and end up with complete different formalismes.
My proposal is to dismiss the lifetime rules and have an object at any address that can possibly hold such object. That solves the whole issue and nobody has ever presented a valid objection to the approach (vague assertions that "this breaks all invariants" isn't a valid objection). Having an object at any valid address just creates an infinite number of potential objects (notably all pointer types, int*
, int**
, int***
...) but these are not usable for reading as no valid value has been written.
Note that without that relaxation of either the lifetime rule or the definition of lvalues, you can't even have a non trivial "strict aliasing rule" as that rule wouldn't apply to a well defined program without that rules. As currently interpreted, the "strict aliasing rule" is useless. (Also it's so badly written nobody knows what it means anyway.)
Or maybe someone will tell me that to make sense of the strict aliasing rule, an lvalue of int
refers to an object, just of a different type. That would be so surprising and silly that even if you make a consistent interpretation of the standard that way, I would still say it's broken.