Is this undefined because I memcpy'ed a non_trivially_copyable type?

344 views Asked by At

I'm having a lot of trouble understanding why memcpy'ing non_copyable types is not allowed, or even if my code in the following is not allowed:

struct trivially_copyable_type
{
    int member;
};

struct non_trivially_copyable_type
{
    int member;
    non_trivially_copyable_type() { }
    non_trivially_copyable_type(const non_trivially_copyable_type& other) { }
};
int main()
{
    bool result = std::is_trivially_copyable_v<trivially_copyable_type>; // True
    result = std::is_trivially_copyable_v<non_trivially_copyable_type>; // False

    trivially_copyable_type copyable;

    void* memory = malloc(128);

    memcpy(memory, &copyable, sizeof(copyable));
    trivially_copyable_type* p1 = (trivially_copyable_type*)memory;
    p1->member = 7; // This is allowed


    non_trivially_copyable_type noncopyable;

    memcpy(memory, &noncopyable, sizeof(noncopyable));

    non_trivially_copyable_type* p2 = (non_trivially_copyable_type*) memory;

    p2->member = 7; // This is undefined or illegal?
} 

If we malloc memory and access that memory through a pointer to int, as in:

int * ptr = (int*) malloc(128);
*ptr = 7; // We didn't create an int, but can treat that memory as int

In the same way:

trivially_copyable_type* ptr = (trivially_copyable_t*) malloc(128);
*ptr = 7; // Same, we didn't create a 'trivially_copyable_type object at memory, but can read and write to it

But:

non_trivially_copyable_type* ptr = (non_trivially_copyable_t*) malloc(128);
*ptr = 7; // Is whether this is legal dependent on whether the type is trivially_constructible or not?

I don't understand why or if in the earlier example where I memcpyed into the buffer would be illegal.

3

There are 3 answers

3
David Schwartz On

Consider:

struct non_trivially_copyable_type
{
    int member;
    non_trivially_copyable_type *self;
    non_trivially_copyable_type() { self = this; }
    non_trivially_copyable_type(const non_trivially_copyable_type& other) { }
    int get_member() { return self->member; }
};

Now do you see the problem? If you memcpy this type and then destroy the object you copied, the next call to get_member will do the wrong thing.

The class has a constructor. That means that an instance of it does not exist until it's constructed. Thou shalt not access a member of a non-existent object.

4
Jeff Garrett On

The question is confused. Trivially copyable guarantees that when you copy the bits of an object back and forth, you get the same object. But you have to have an object already. In all of the cases, you are simply populating bits into a place where you want an object to be, and that is a question of object lifetime. When can you assume such an object exists?

If we malloc memory and access that memory through a pointer to int, as in:

int * ptr = (int*) malloc(128);
*ptr = 7; // We didn't create an int, but can treat that memory as int

This was false prior to P0593, which was adopted as a defect resolution for C++17. The lifetime of the int must be started. Prior to P0593, this had to be done with placement new. Now, it starts implicitly because int is an implicit lifetime type and malloc starts an implicit lifetime.

In the same way:

trivially_copyable_type* ptr = (trivially_copyable_t*) malloc(128);
*ptr = 7; // Same, we didn't create a 'trivially_copyable_type object at memory, but can read and write to it

This actually has nothing to do with being trivially copyable, because there is no copying. This is OK because trivially_copyable_type is an aggregate which is an implicit lifetime type, containing an int which also is.

But:

non_trivially_copyable_type* ptr = (non_trivially_copyable_t*) malloc(128);
*ptr = 7; // Is whether this is legal dependent on whether the type is trivially_constructible or not?

This does not have to do with trivial copyability. There is no object, because non_trivially_copyable_type is not an implicit lifetime type.

0
zkoza On

To my understanding, trivially copyable types are types that can be safely treated, e.g. by the standard library, as "C-style" types, with no side effects during their copying. Hence no (user-defined) constructors, destructors, assignment operators. They're what we used to know as POD types, but updated to the requirements and needs of modern C++.

Suppose you want to count the number of operator= is called by some standard algorithm. Say, std::copy. You overload operator= for a class, and it does some counting. Can the library optimize the implementation of std::copy by resorting to memcpy? Eh, unconditionally? Then all your counting will be lost! This, in general, would be a disaster, this is not what we expect in C++! Classes in C++ usually have some invariants and constructors are there to force them. Copying and memmoving is not the same thing.

When such an optimization is C++-safe? Only if it does not matter if the objects were moved (copied) element by element in a loop, or memcpy-ed.

Here's part of description of std::copy in cppreference:

Copies the elements in the range, defined by [first, last), to another range beginning at d_first.
...
In practice, implementations of std::copy avoid multiple assignments and use bulk copy functions such as std::memmove if the value type is TriviallyCopyable and the iterator types satisfy LegacyContiguousIterator.

So let me repeat: for trivially copyable types there's no difference if you use element-wise copy in a while loop or memcpy.

Does it guarantee that copying trivially copyable objects is always safe? Of course - not! Take as an example a simple linked list struct:

struct Node
{
   int value;
   Node* next;
};

This is a trivially copyable type. You can place such structs in a vector so they form a perfect linked list. But if you copy this vector to a different location or have the data automatically realocated due to some push_back, you're doomed to fail. However, this is something you could and should expect if you copy such objects in a simple while loop!