Matthieu M.
Matthieu M.

Reputation: 299800

May placement `new` rely on underlying storage value?

Let's start with some context.

A custom memory pool was using code similar to the following:

struct FastInitialization {};

template <typename T>
T* create() {
    static FastInitialization const F = {};

    void* ptr = malloc(sizeof(T));
    memset(ptr, 0, sizeof(T));
    new (ptr) T(F);
    return reinterpret_cast<T*>(ptr);
}

The idea is that when called with FastInitialization, a constructor could assume that the storage is already zero-initialized and therefore only initialize those members who need a different value.

GCC (6.2 and 6.3, at least) however has an "interesting" optimization which kicks in.

struct Memset {
    Memset(FastInitialization) { memset(this, 0, sizeof(Memset)); }

    double mDouble;
    unsigned mUnsigned;
};

Memset* make_memset() {
    return create<Memset>();
}

Compiles down to:

make_memset():
        sub     rsp, 8
        mov     edi, 16
        call    malloc
        mov     QWORD PTR [rax], 0
        mov     QWORD PTR [rax+8], 0
        add     rsp, 8
        ret

But:

struct DerivedMemset: Memset {
    DerivedMemset(FastInitialization f): Memset(f) {}

    double mOther;
    double mYam;
};

DerivedMemset* make_derived_memset() {
    return create<DerivedMemset>();
}

Compiles down to:

make_derived_memset():
        sub     rsp, 8
        mov     edi, 32
        call    malloc
        mov     QWORD PTR [rax], 0
        mov     QWORD PTR [rax+8], 0
        add     rsp, 8
        ret

That is, only the first 16 bytes of the struct, the part corresponding to its base, have been initialized. Debugging information confirm that the call to memset(ptr, 0, sizeof(T)); has been completely elided.

On the other hand, both ICC and Clang both call memset on the full size, here is Clang's result:

make_derived_memset():               # @make_derived_memset()
        push    rax
        mov     edi, 32
        call    malloc
        xorps   xmm0, xmm0
        movups  xmmword ptr [rax + 16], xmm0
        movups  xmmword ptr [rax], xmm0
        pop     rcx
        ret

So the behavior of GCC and Clang differ, and the question becomes: is GCC right and producing better assembly, or is Clang right and GCC buggy?


Or, in terms of language lawyering:

Under which circumstances may a constructor rely on the previous value stored in its allocated storage?

Note: I assume this only matters with placement new, but I am happy to be shown otherwise.

Upvotes: 8

Views: 1073

Answers (2)

Barry
Barry

Reputation: 302852

May placement new rely on underlying storage value?

No, it may not. From [dcl.init]:

If no initializer is specified for an object, the object is default-initialized. When storage for an object with automatic or dynamic storage duration is obtained, the object has an indeterminate value, and if no initialization is performed for the object, that object retains an indeterminate value until that value is replaced (5.18).

Indeterminate value means just that, indeterminate. It does not mean that, in the case of a placement new expression, the previous memory is necessarily maintained. The compiler is allowed to do whatever it wants to the memory - which includes, but is not limited to, nothing.

Under which circumstances may a constructor rely on the previous value stored in its allocated storage?

The subsequent paragraph in [dcl.init] lists cases where the behavior is not undefined when producing an indeterminate value, but they only have to do with unsigned narrow character types.

So, under no circumstances.

Upvotes: 11

Leon
Leon

Reputation: 32474

Both GCC and Clang are correct - leaving the (own) data members of DerivedMemset uninitialized will result in undefined behavior as soon as their values are accessed. Therefore the compiler is given the license to leave - right before the constructor is invoked - any bit pattern at the storage range that will be occupied by those fields.

Upvotes: 1

Related Questions