1

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.

Zebrafish
  • 11,682
  • 3
  • 43
  • 119
  • Imagine `memcpy()`'ing a struct that has a `std::string` member in it. The `std::string` will not be copied correctly. That is why non-trivial types should not be copied with `memcpy()` – Remy Lebeau Apr 06 '21 at 00:31
  • I'm not explaining myself well. I'm coming at this from the point of reallocating a vector to another part of memory and abandoning the old memory. Essentially I'm 'moving' the object from one area to another, using memcpy instead of looping through each object and moving it. This would seem to be sensible, but memcpy isn't supposed to be used on non_trivially_copyable types. Even though I don't want a copy of it, I just want to move it to another area of memory. I'm wondering whether accessing the pointers is illegal from just having memcpyed the object. – Zebrafish Apr 06 '21 at 00:38
  • You can't move memory, you have to copy the contents from one location to another. The compiler doesn't know what the constructor/destructor of a non-trivial type will actually do, so without knowing what the old memory looks like and how it will be destroyed, it is just not safe to memcpy() the contents of the old memory to the new memory. And yes, internal pointers is usually one common reason for that. With trivial types, you don't have to worry about that. – Remy Lebeau Apr 06 '21 at 00:56

3 Answers3

2

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!

zkoza
  • 2,644
  • 3
  • 16
  • 24
2

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.

Jeff Garrett
  • 5,863
  • 1
  • 13
  • 12
  • So if I have struct SimpleStruct { int a; }, I malloc a buffer and point SimpleStruct* to it, a SimpleStruct object exists there. But instead if I add a user-defined constructor that does nothing to SimpleStruct, there is no object there in the buffer and I can't access it? – Zebrafish Apr 06 '21 at 15:58
  • Does doing std::launder on that pointer allow me to act as if an object exists there? – Zebrafish Apr 06 '21 at 16:14
  • std::launder pre-supposes an object at the address. It does not implicitly create one. – Jeff Garrett Apr 06 '21 at 16:17
  • So again, SimpleStruct {int a;} , I create one on the stack as a local variable, I memcpy it to the malloced buffer, I can point a SimpleStruct* at that buffer and a SimpleStruct object exists there, and can use it. Instead if I add an empty move assignment operator that doesn't do anything to SimpleStruct and I point to the buffer, an object doesn't exist there and I can't use it? – Zebrafish Apr 06 '21 at 16:26
1

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.

David Schwartz
  • 179,497
  • 17
  • 214
  • 278
  • There's something I don't understand. Your example breaks because if you memcpy you fail to copy an object properly, ie., members don't point to the right thing. But I'm wondering whether there's anything illegal about memcpying a non_trivially_copyable type in itself. For example struct Foo { int a; } works fine with memcpy, yet just adding an empty copy constructor makes it non_trivially_copyable even though a memcpy would seem to just copy it fine. – Zebrafish Apr 06 '21 at 00:23
  • @DavidShwartz Another thing about "Though shalt not access a member of a non-existent object", isn't that what we do with struct Foo { int member;}; Foo* ptr = malloc(128); ptr->member = 7; ??? – Zebrafish Apr 06 '21 at 00:26
  • @Zebrafish The point is that there is no guarantee that `memcpy` will know how to properly copy it so that it has the same meaning. The same bytes might not have the same meaning, so the results are undefined. – David Schwartz Apr 06 '21 at 01:32