can memcpy for std::aligned_storage?

915 views Asked by At

std::aligned_storage::type is POD type. POD type can memcpy. However, What happens if placement new non-trivially-copyable type to std::aligned_storage? Can it memcpy that std::aligned_storage?

non-trivially-copyable type(non-POD type) can NOT memcpy, Behavior is undefined. If std::aligned_storage memcpy non-trivially-copyable type, is it also undefined behavior?

#include <new>
#include <type_traits>
#include <cstring>
#include <iostream>

struct y { int a; } ;

// non-trivially-copyable
struct t
{
    y a;
    int* p;
    t(){ p = new int{ 300 }; }
    t( t const& ){ a.a += 100; }
    ~t(){ delete p; }
};

int main()
{   // Block 1
    { 
        t a; a.a.a = 100;
        t b; b.a.a = 200;
        // std::memcpy(&b,&a,sizeof(t));  // abort...non-trivially-copyable
    }
    // Block 2
    {
        std::aligned_storage_t<sizeof(t),alignof(t)> s;
        {
            t a;
            a.a.a = 100;
            std::memcpy(&s,&a,sizeof(t)); // OK...Coincidence? Behavior is undefined?
        }
        std::cout << static_cast<t*>(static_cast<void*>(&s))->a.a << std::endl; // OK...
    }
    // Block 3
    {
        std::aligned_storage_t<sizeof(t),alignof(t)> s1; new( &s1 ) t; 
        std::aligned_storage_t<sizeof(t),alignof(t)> s2; new( &s2 ) t;

        std::memcpy(&s2,&s1,sizeof(t)); // trivially-copyable????
    }
}

I thought that it is undefined as well. However, we have work. Is this for coincidence?

1

There are 1 answers

7
M.M On BEST ANSWER

First of all: as discussed on this thread, the C++ standard only defines the behaviour of memcpy for trivially copyable objects. That thread gives a specific example of how it can break for non-trivially copyable options

So, a narrow interpretation of the Standard would say that the mere act of calling memcpy causes UB.

However, a more common sense interpretation would be that it's OK to copy the bytes, but any attempt to treat the target as actually containing an object of the same type would cause UB. Especially in the case where the program depends on side-effects of the destructor, as yours does. The rest of my answer is based on this latter interpretation.


Starting with Block 2:

std::aligned_storage_t<sizeof(t),alignof(t)> s;
{
    t a;
    a.a.a = 100;
    std::memcpy(&s,&a,sizeof(t)); 
}
std::cout << static_cast<t*>(static_cast<void*>(&s))->a.a << std::endl

Since t is not trivially copyable, we have not created a valid t object in s's storage. So the attempt to use s as if it contained a valid t object certainly causes undefined behaviour. When UB occurs any results can follow, including (but not limited to) it appearing to "work as expected".


In Block 1 (if the memcpy is uncommented):

{
    t a; a.a.a = 100;
    t b; b.a.a = 200;
    std::memcpy(&b,&a,sizeof(t));  // abort...non-trivially-copyable
}

Here we have destructor side-effects. The memcpy ends the lifetime of a (because a's storage is re-used). However the code will go on to try and call the destructor on a.

In this case, even if the copy "appears to work", your abort probably comes from the double-free of a.p.

If we changed t to be a non-trivially-copyable type but with no destructor side-effects then this example would be unclear.

There is no such double-free in Block 2 because no destructor is ever invoked for the t stored in s.


Block 3:

This is similar to Block 2: the memcpy does not create an object. It "appears to work" because you never invoke destructors for the objects in the aligned_storage.

In fact, under our common-sense interpretation of memcpy, there is no UB here because you never attempted to use the result of copying the bytes and the target does not have a destructor called on it. (If you copied your cout line to here, it would cause UB for the same reason as in Block 2).


Related discussion: Even for trivially copyable classes it is still murky

The C++ standard is unclear around the issues of when object lifetime begins for objects in malloc'd space or aligned_storage. There was a submission N3751 recognizing that this needs cleanup but there is still a lot of work to do.

In your Block 2, the lifetime has not begun for s. This is because t has non-trivial initialization. (This is actually not clearly stated by the C++ standard either). However Block 1's a is an object whose lifetime has begun.

N3751 proposes that (if t were trivially copyable) then the memcpy would in fact begin the lifetime of s.