Is placement new of Derived type within a vector / array of Base type legal
Asked Answered
R

1

7

Is placement new of derived object within a vector / array defined to be base object legal

#include <iostream>
#include <memory>
#include <vector>

struct A
{
    virtual ~A() 
    {
        std::cout << "~A()" << std::endl;
    }
};

struct B : public  A
{
    virtual ~B()
    {
        std::cout << "~B()" << std::endl;
    }
};

int main()
{
    std::vector<A> a(1);
    a[0].~A();
    :: new(std::addressof(a[0])) B();
}

Is this legal C++?

The question is related to the one here. The gist of the discussion there is that we are able to determine the type of the objects in a vector / array at compile time and so we do not have to do any dynamic dispatch. We can use this information to optimize whatever gcc is doing right now.

I filed an issue with gcc here and the argument against this optimization is that we could potentially have placement new changing the type stored in the vector / array, but we aren't sure if it is legal, hence this stackoverflow question.

Interestingly, gcc behaves differently from clang for arrays, and gcc has a different behavior with itself depending on the length of the array. It does the virtual dtor when len >= 3 but qualified dtor call when len < 3.

https://godbolt.org/z/f33Gh5EGM

Edit:

The question is purely "Is this legal C++". Any other information in the question is for context on why I'm asking such a question.

Ribwort answered 1/6, 2023 at 9:24 Comment(23)
"and the argument against this optimization" .. .which optimization do you refer to?Cinelli
There will be issues when the vector is reallocated, so I wouldn't try it.Trincomalee
the effect of >= 3 might be just that, reallocation when the vector grows. Pretty sure this is ubCinelli
your godbolt link is not using a vectorCinelli
Also read this : placement new in a home made vector. Basically you have to preallocate enough memory up front and make sure that memory stays there (for a longer time then the lifetime of the objects)Trincomalee
Is this really about placement new? One could equally-well invoke UB by copying a B object to std::addressof(a[0]). Putting an object of type cat into a container of type dog is UB, even though there are a number of ways you can do it.Furthermore
@463035818_is_not_a_number The optimization is the linked stackoverflow issue. Suppose we have vector<Base> v; we know the objects in the vector has to be of type Base and never Derived. If we pushed Derived in, it will get sliced. Similarly, we know Base v[100]; only has object of type Base (and never Derived) and so we never have to rely on a virtual call (but gcc still does)Ribwort
@NgYongXiang a) the question should be selfcontained. As written, "the optimization" is missing the context. b) Its not really an optimization, but more often than not B is more than A and then you cannot possibly fit the Bs into the same memory as the As. There are solutions for what you want to do, but placement new seems to be not the right routeCinelli
@Adrian Mole Yes, the bigger question is whether it is true that vector<T> always contain T without invoking UB. If we do, then we are able to remove virtual function calls. I am not sure of any other way to construct B at std::addressof(a[0]). are you able to provide an example? Doing a copy will cause slicing and will not compile for irrelevant types (unrelated by inheritance)Ribwort
@463035818_is_not_a_number It is an optimization. If we can be sure that an array of A always contains A, then we will never have to do a virtual function call to determine the type at run time.Ribwort
@NgYongXiang You can ensure that an array of A always contains A by not putting anything else than As in it. The compiler will help you to do this by not allowing you to put non-A types into it in most cases.Cid
If, by 'slicing', you mean that a memcpy(std::addressof(a[0]), &b, sizeof(B)); will overwrite the vtable of an A at the destination, then: yes, it will. But so will using a placement new constructor - it will put a B vtable in the space.Furthermore
Basically this is what std::vector does underneath. That is why yo have capacity and size for the vecotr.Intermarry
But, to answer your "title" question: Yes, using placement new is legal - that's effectively what the .emplace() member of a std::vector will do.Furthermore
@Adrian Mole you are right the title is quite misleading. Let me change it to placement new of derived type in array of base type for more clarityRibwort
@NgYongXiang an A is always an A but not a B, the array is a red herring. Only a A* can point to an A or a B. You need pointers or references for virtual dispatch. Btw I am a little confused by the motivation, on the one hand you want to avoid virtual dispatch, but then your issue is no virtual dispatch for the destructor.Cinelli
anyhow, my point was merely that imho in the question it is unclear what you mean with "this optimization".Cinelli
@463035818_is_not_a_number there is virtual dispatch in this example of the array destructing which is surprising and it seems like a miss optimization godbolt.org/z/f33Gh5EGMRibwort
There might not be enough space for a B , and it'll be UB if you let the vector clean itself upIndo
Just to clarify things, I do not think it is legal as well but I am trying to confirm this by asking if there is an quote in the standard that says that this is not legal.Ribwort
Why would you ask that? Suppose this particular program is legal C++ (I don't believe it is, but let's suppose that for the sake of argument); what next? How would you use this information to build real practical software?Congratulant
@n.m. In fact I would like it to be illegal. If we can be certain that this behavior is illegal and the standard says so, we can make the assumption that all items in a vector / array are of the type that they defined to contain e.g. Base arr[n] contains objects of Base and vector<Base> contains objects of Base. If we know this, we can devirtualize (and optimize) the virtual methods (the premise of using the vptr is because we do not know the underlying type until run time, but in this case if we know). This information can be used to build better compilersRibwort
If your users put objects with virtual methods in a vector, they get exactly what they deserve. But if it makes you sleep better, this thing is illegal. After you destroy an object and placement-new a different object in its memory, you cannot use pointers or references to the original object unless the new object is of the same type and unless they are both most-derived objects ref.Congratulant
C
-1

Note: This conclusion is based on the semantics of ::delete[] and may or may not be transferable to destruction of automatic storage arrays.

If we look at the documentation of ::delete[] then we can find the statement about deleting array types

For the second (array) form, expression must be a null pointer value or a pointer value previously obtained by an array form of new-expression whose allocation function was not a non-allocating form (i.e. overload (10)). The pointed-to type of expression must be similar to the element type of the array object. If expression is anything else, including if it's a pointer obtained by the non-array form of new-expression, the behavior is undefined.

So what does it mean that the pointed-to type of expression must be similar to the element type of the array?

If we look at how similar is defined then we find this statement

Informally, two types are similar if, ignoring top-level cv-qualification:
they are the same type; or
they are both pointers, and the pointed-to types are similar; or
they are both pointers to member of the same class, and the types of the pointed-to members are similar; or
they are both arrays of the same size or both arrays of unknown bound, and the array element types are similar. (until C++20)
they are both arrays of the same size or at least one of them is array of unknown bound, and the array element types are similar. (since C++20)

Given that the element type of the array is A and the pointed-to type is B we can see by the informal definition of similar that A and B are not similar. Therefore, according to ::delete[] this behavior is undefined.

Cid answered 1/6, 2023 at 10:36 Comment(1)
The foundation of this answer is wrong; vector uses allocator_traits to allocate/deallocate storage, but not explicit object pointers; delete[] is never used, making none of this relevant. The definition of allocator::deallocate, which is used in vector, never calls delete[] -- so none of this applies. Destruction is also done element-wise using allocator_traits::destroyLornalorne

© 2022 - 2025 — McMap. All rights reserved.