Is it practically OK to delete object not constructed using the new expression?
Asked Answered
M

2

7

Pedantically, this may not be OK. As per cppref:

If expression is anything else, including if it is a pointer obtained by the array form of new-expression, the behavior is undefined.

Putting that aside, is the following code OK in practice (T is non-array, and assuming that new is not replaced)?

auto p = (T*)operator new(sizeof(T));
new(p) T{};
delete p;

It is said that in cppref that

When calling the allocation function, the new-expression passes the number of bytes requested as the first argument, of type std::size_t, which is exactly sizeof(T) for non-array T.

So I guess this is probably OK. However, it is also said that since C++14,

New-expressions are allowed to elide or combine allocations made through replaceable allocation functions. In case of elision, the storage may be provided by the compiler without making the call to an allocation function (this also permits optimizing out unused new-expression). In case of combining, the allocation made by a new-expression E1 may be extended to provide additional storage for another new-expression E2 if all of the following is true: [...]

Note that this optimization is only permitted when new-expressions are used, not any other methods to call a replaceable allocation function: delete[] new int[10]; can be optimized out, but operator delete(operator new(10)); cannot.

I'm not quite sure of the implications. So, is this OK in C++14?

Why am I asking this question? (source)

Sometimes, memory allocation and initialization cannot be done in a single step. You have to manually allocate the memory, do something else, and then initialize the object, e.g., to provide strong exception safety. In this case, if the delete expression cannot be used on the resulting pointer, you have to manually uninitialize and deallocate, which is tedious. To make things worse, in case both new expression and the manual method are employed, you have to track which one is used for each object.

Mikey answered 29/3, 2018 at 1:44 Comment(19)
Before you can even get to delete, you have to make sure that your placement new uses suitably-aligned memory which, in this case, does not happen.Miles
@SamVarshavchik It does happen. operator new() guarantees this. This function is required to return a pointer suitably aligned to hold an object of any fundamental alignment. See here.Mikey
Well, you did construct a T object using a new-expression. Though a placement one. And though p doesn't point to it because you threw away the result of that expression.Occurrence
One case where it would probably fails is if operator new and delete are redefined for a specific class if it manage memory using another method (for ex. calling the OS instead of he run-time)Gillyflower
@Gillyflower Let's ignore the case of replaced new. Updated question.Mikey
"Is it practically OK" is hard to answer; you seem to agree that the code is undefined behaviour, so you are really asking for a survey of how all the compilers that exist manifest this undefined behaviour. Also they might manifest it differently in future versions or modes of the compiler.Lexical
@Lexical Well, the standard doesn't guarantee two's complement representation [ref], but it's safe to assume so in practice. I wonder whether the same applies here too.Mikey
I'm wondering why you're even asking. If you don't do it, which you shouldn't, you never have to find the answer, handle the bug reports, rewrite the whole thing the way it should have been in the first place when it doesn't work on some new platform or even some new compiler version, ...Herald
@EJP I realized this potential issue after I have written the code where some may be constructed using the new expression while some are not. To be on the safe side, I decided to write my own utilities as a replacement for new/delete expression. Still, out of curiosity, I want to know an answer.Mikey
Voted to close as a call for debate and not showing actually a problem with some error or malfunctioning or some diagnostic issue.Perretta
It's never a good idea to invoke UB: The next compiler version may change your program in interesting ways...Trichromatism
@cmaster Agreed. So I decided not to do this. Still, I intend to keep this thread open, in case some other folks are unaware of this potential UB.Mikey
@Occurrence Are you saying this would actually not be UB if the second line were p = new(p) T{}; or if the third line were delete std::launder(p);?Sapodilla
@DanielH That's exactly what I'm saying.Occurrence
@Occurrence Yeah, looking at the standard (latest draft on eel.is, not C++17, but I don’t think this has changed), I see you’re right.Sapodilla
@DanielH So this std::launder() thing is used to... OK, it's way above my current level to understand XDMikey
@Mikey Technically, with the code you wrote, p doesn’t point to the newly-constructed T. The memory address is the same, but the compiler can assume it refers to the (non-existent) T that was there before the placement new. In code this simple I don’t think it would make a difference, but if T had a const member or something, the compiler might load that from memory before the call to new. The std::launder call tells it not to do that kind of optimization, but it’s easier and cleaner to just assign the result of the placement new back to p.Sapodilla
All of that said, based on your source link, I think you might want to look into the standard Allocator concept and the default std::allocator; you seem to have independently discovered the need for them but probably do not need to independently implement them yourself.Sapodilla
@DanielH Thanks for the kind reminder :) The thing is: 1) I intend to avoid the hassles associated with writing allocator-aware structures, at least for now. 2) Then I may just stick to the stateless std::allocator. But I don't want to drag an allocator object along, or construct a new one just at the place where I need it. So I made these free-function versions :)Mikey
O
4

This code has well-defined behavior if you have p = new(p) T{};, assuming that no custom (de)allocation functions are in play. (It can be easily hardened against such things; doing so is left as an exercise for the reader.)

The rule is that you must give non-array delete a pointer to an object created by a non-array new (or a base class (with virtual destructor) subobject of such an object, or a null pointer). And with that change your code does that.

There's no requirement that the non-array new be of a non-placement form. There can't be, since you'd better be able to delete new(std::nothrow) int. And that's a placement new-expression too, even though people don't usually mean that when they talk about "placement new".

delete is defined to result in a call to a deallocation function (ignoring the elision cases, which is irrelevant here because the only allocation function called by the new-expression is not a replaceable global allocation function). If you set things up so that it passes invalid arguments to that deallocation function, then you get undefined behavior from that. But here it passes the right address (and the right size if a sized deallocation function is used), so it's well-defined.

Occurrence answered 29/3, 2018 at 21:41 Comment(10)
I can see potential problems if T is over-aligned (so the deallocation function will be the one taking a std::align_val_t or if T is an array type (in which case, even though it doesn’t look like it, the placement new expression actually is an array new, which introduces several complications). If this is generic code both of those can be handled, and if it isn’t generic except for this question you should know if you’re dealing with those cases.Sapodilla
Wow~ O_O One minor question. I think the change here is purely pedantic, right? It's said here that such allocation functions are known as "placement new", after the standard allocation function void* operator new(std::size_t, void*), which simply returns its second argument unchanged.Mikey
@Mikey As I said in response to your comment on the question, there is a real difference. Suppose you had struct T { const int i }; and T* p = new T{5};. If you did int sum = p->i; p->~T; new(p) T{20}; sum += p->i;, you would invoke UB and there’s a good chance the compiler would look at p->i only once (after all, it’s const; it’s not like it could have changed), leaving sum == 10. You need to either re-assign p with the result of the new expression, or use std::launder to not do that optimization.Sapodilla
const is perhaps the easier to understand example, but not the only one. Another one can be found in N4303, the paper that originally proposed launder.Occurrence
@DanielH I think many folks are simply discarding the return value of placement new when used to initialize objects. And when the issue does happen, chance is slim for the writer to find the root cause. I'm considering posting an QA for this. Too important to be left only in the comment section.Mikey
@DanielH Done. Feel free to edit it the way you see fit. Really happy that we have genuine C++ experts to help folks out at SO :)Mikey
@DanielH I'm confused. Even the standard library itself may discard the return value, see this. Does the compiler grant special privileges to these standard library constructs, or the standard library itself is actually subject to change?Mikey
@Mikey That is a good question that I haven’t found the answer to. It doesn’t look like there are any plans to change this in the standard library, but other than that I’m not sure why it works. If I were writing the Allocator API, I’d have allocate return a void* and construct return a T*. @T.C., do you know how this works?Sapodilla
The full implications of the pointer model isn't really fully understood until the recent years. There is a paper in flight to change construct. @DanielHOccurrence
@Occurrence Since in practice no compiler writers want to break all the standard containers, I guess for now the optimizer will probably act as though the pointer is reassigned? Just in case, would it be a good idea to launder all pointers after a call to construct unless you know the type is one that allows storage reuse?Sapodilla
D
-3

I didn't quite grasp OP's last paragraph. What is tedious about explicitly calling the destructor and then deallocating the pointer? That is what STL and any reputable lib that I'v seen do all the time. And yes, you have to keep track of how you allocate memory, in order to deallocate it back. Reading the api docs of std::vector and/or std::shared_ptr can familiarize one with the correct way of doing things. If you are unfamiliar with the syntax of explicit dtor call here is a simple snippet:

void * raw_ptr= my_alloc(sizeof(my_type),alignof(my_type));
if (!raw_ptr)
     throw my_bad_alloc();
my_type* ptr {new(raw_ptr) my_type{init_params}};
raw_ptr=nullptr;
    //...
ptr->~my_type();//hello! Is it me you're lookin' for?
if (!my_dealloc(ptr))
     throw my_runtime_error();
ptr=nullptr;
Dougald answered 29/3, 2018 at 13:52 Comment(10)
If you are zeroing pointers after invalidating them, you should move the ptr = nullptr; to directly after the destructor call, replace ptr with raw_ptr in the my_dealloc() call, and finish with a raw_ptr = nullptr;...Trichromatism
Good point; I missed that one. But I was more focused on explicit destructor call.Dougald
Then, why are you not editing it into your answer? My comments are going to be deleted sometime, your answer is not.Trichromatism
Reading at the question it seems obvious that the OP is aware of what you say, you don't answer the question. You are exposing your opinion: you do not think that decomposing the delete expression in destruction and allocation steps is tedious. The question is: is it OK in practice to use the delete expression in the OP case?Spies
I did some editing, but not everything; some verbosity is intentional; raw pointer usage is discouraged and [de]allocations are supposed to be placed in ctor/dtor. For the sake of illustration and simplicity, zeroing is put in the snippet.Dougald
To Oliv: Disagreed. It is by no means ok to call delete on objects not allocated by new. Composing destruction with subsequent deallocation is not big challenge; just one template away; that is what a template library programmer actually does. I strongly think that OP was looking for a syntax to explicitly destruct the object.Dougald
@Dougald Whether using delete p here is UB is a somewhat different issue than whether it works in practice (something can be UB but work as expected on all compilers, or something could be well-defined by the standard but not implemented by any compiler). Both are completely orthogonal to whether the alternatives are tedious. You only answer the last issue, which is not what was asked.Sapodilla
I didn't talk about UB. Using delete on any pointer not allocated by new is seeking troubles. Can you guarantee that the default implementation of operator new call or behave like malloc everywhere? And I am not considering class specific overloads of new/delete yet. Sometimes we ask the wrong question, simply because of not knowing what to look for. IMHO the OP was trying to learn C++ through reading standardization articles. It is just like learning english through memorizing every word in an oxford dictionary.Dougald
This doesn't answer the questionLexical
The last paragraph -Entitled as "Why am I asking this question?" - is the heart of the question; and believing or not, I seriously doubt any different reply would come in handy. But I would be glad to see what the question is assumed to be. For now I don't intend to distract attention from what I believe is the source of question.Dougald

© 2022 - 2024 — McMap. All rights reserved.