Let's say you have an object of type T
and a suitably-aligned memory buffer alignas(T) unsigned char[sizeof(T)]
. If you use std::memcpy
to copy from the object of type T
to the unsigned char
array, is that considered copy construction or copy-assignment?
If a type is trivially-copyable but not standard-layout, it is conceivable that a class such as this:
struct Meow
{
int x;
protected: // different access-specifier means not standard-layout
int y;
};
could be implemented like this, because the compiler isn't forced into using standard-layout:
struct Meow_internal
{
private:
ptrdiff_t x_offset;
ptrdiff_t y_offset;
unsigned char buffer[sizeof(int) * 2 + ANY_CONSTANT];
};
The compiler could store x
and y
of Meow within buffer at any portion of buffer
, possibly even at a random offset within buffer
, so long as they are aligned properly and do not overlap. The offset of x
and y
could even vary randomly with each construction if the compiler wishes. (x
could go after y
if the compiler wishes because the Standard only requires members of the same access-specifier to go in order, and x
and y
have different access-specifiers.)
This would meet the requirements of being trivially-copyable; a memcpy
would copy the hidden offset fields, so the new copy would work. But some things would not work. For example, holding a pointer to x
across a memcpy
would break:
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
However, is the compiler really allowed to implement a trivially-copyable class in this manner? Dereferencing px
should only be undefined behavior if a.x
's lifetime has ended. Has it? The relevant portions of the N3797 draft Standard aren't very clear on the subject. This is section [basic.life]/1:
The lifetime of an object is a runtime property of the object. An object is said to have non-trivial initialization if it is of a class or aggregate type and it or one of its members is initialized by a constructor other than a trivial default constructor. [ Note: initialization by a trivial copy/move constructor is non-trivial initialization. — end note ] The lifetime of an object of type
T
begins when:
- storage with the proper alignment and size for type
T
is obtained, and- if the object has non-trivial initialization, its initialization is complete.
The lifetime of an object of type
T
ends when:
- if
T
is a class type with a non-trivial destructor ([class.dtor]), the destructor call starts, or- the storage which the object occupies is reused or released.
And this is [basic.types]/3:
For any object (other than a base-class subobject) of trivially copyable type
T
, whether or not the object holds a valid value of typeT
, the underlying bytes ([intro.memory]) making up the object can be copied into an array ofchar
orunsigned char
. If the content of the array ofchar
orunsigned char
is copied back into the object, the object shall subsequently hold its original value. example omitted
The question then becomes, is a memcpy
overwrite of a trivially-copyable class instance "copy construction" or "copy-assignment"? The answer to the question seems to decide whether Meow_internal
is a valid way for a compiler to implement trivially-copyable class Meow
.
If memcpy
is "copy construction", then the answer is that Meow_internal
is valid, because copy construction is reusing the memory. If memcpy
is "copy-assignment", then the answer is that Meow_internal
is not a valid implementation, because assignment does not invalidate pointers to the instantiated members of a class. If memcpy
is both, I have no idea what the answer is.
memcpy
then it is not any sort of construction or assignment. – Beetmemcpy
instead of a constructor :) – BeetT
. This seems to fit better to the code example than the quote about character arrays IMHO. – Membranememcpy
something that's not aT
into aT
- which definitely counts as "reuse" of the storage and ends the lifetime of theT
object - I see no reason whymemcpy
ing aT
into aT
doesn't count as "reuse" as well. And I agree with @brianbeuning that debating the standard compliance of a hypothetical compiler that no sane person would ever write or use is rather pointless. – FletaMeow_internal
violates [basic.life]/7 if your compiler does not change the pointerpx
if we replace thememcpy
with anew((void*)&a) Meow(b);
. (Though it might be subtle:px
is pointing to a non-complete object; one had to conclude from other sources that it must point to an object of the same type afterwards etc. But I think that is the intention of the Standard.) – MembraneMeow_internal
is an illegal implementation, it means that there is no technical basis for the Standard's restriction thatoffsetof
require a standard-layout structure. It would be possible to formally prove that being trivially-copyable would be sufficient to supportoffsetof
, and justify the Standard changing its definitions as a result. – Heterothallicpx
isn't pointing to an object of typeT
; it's pointing to a subobject, and as far as I can see there's no guarantee that when you reuse the storage of an object pointers to its subobjects remain valid (it does reuse the storage of*px
as well, of course, but there's no guarantee that this reuse also satisfies the other requirements in [basic.life]/7). – Fletareinterpret_cast<std::uintptr_t>(px)
, XOR the resulting unsigned integer value with a random number you got from/dev/urandom
, setpx
tonullptr
, then do thememcpy
. After thememcpy
finishes, usereinterpret_cast<int *>(encrypted_uintptr)
to restore the original pointer value (legal by [expr.reinterpret.cast]/5). The compiler has no way to know that you've hidden the pointer. (This would not be a safely-derived pointer, though, by [basic.stc.dynamic.safety]/3). – Heterothallicchar
orunsigned char
, and you retain a pointer to it, it's clear that some element of the backing storage array will compare equal to that pointer. So to say that pointers are invalidated is incorrect. Perhaps to say that they may be used "in limited ways" as in [basic.life]/5 is more correct, then? – Heterothallicoffsetof`` would interact with this. It is supposed to work with standard-layout classes, so either
offsetof` is unimplementable or your hypothetical compiler violates something else. Note thatoffsetof
is a macro that evaluates to the offset of a member in bytes given a class name (not an instance), implying that OP's hypothetical complier can't fully implement the standard becauseoffsetof
would be impossible. cplusplus.com/reference/cstddef/offsetof – NavarreteMeow
isn't standard-layout, sooffsetof
would not be required to work with it. However, the point of the exercise above is to point out what seems to me to be something silly in the Standard. The idea is to show that a compliant compiler implementation in which a trivially-copyable (i.e.,memcpy
-compatible) class is not necessarilyoffsetof
-compatible is either a contradiction or is so absurd as to never be implemented. Thus, it would be justified to modify the Standard to state thatoffsetof
is allowed on trivially-copyable types, not just standard-layout types. – Heterothallicoffsetof
seems to imply that the offset of a member has to be the same from instance to instance, which breaks your example and makes your hypothetical compiler implicitly non-compliant, I think. Would you agree? – Navarretememcpy
meaningfully is a consequence of a broken abstraction where trivial objects' lifetimes begin and end separately from their storage. – Schistosomiasisfoo
contains struct memberss1
ands2
with a common initial sequence, such a model would make clear what would be accessed by if code reads the lvaluefoo.s2.commonMember
after having writtenfoo.s1.commonMember
. The act of writingfoo.s1.commonMember
may renderfoo.s2
inaccessible, but resolving lvaluefoo.s2
would make that already-existing member accessible without ending the lifetime offoo.s1
nor making it inaccessible. – Schistosomiasis