19

And if so, why does the following code give me the warning

note: neither the destructor nor the class-specific operator delete will be called, even if they are declared when the class is defined

?

struct C;

int main()
{
    C *c = nullptr;

    delete c;

    return 0;
}

I understand why it might be undefined behavior in the general case if C has non-trivial/virtual destructors, but doesn't the standard guarantee/define that delete on nullptr is always a noop no matter the situation?

To reiterate: I'm asking specifically about the case where the pointer to incomplete type is nullptr!

Peter Mortensen
  • 28,342
  • 21
  • 95
  • 123
Dan M.
  • 3,410
  • 1
  • 20
  • 34
  • 1
    The fact, that compiler gives you a warning does not mandatory mean code is wrong. For example unused variable. It usually means that code is unsafe. – Slava Nov 17 '17 at 15:12
  • 3
    To me the question does not make much sense. Either, you plan the `c` to be always NULL, then you don't need the line with delete at all, or you eventually allow to assign it some value later, in which case you need the complete type definition... – lukas Nov 17 '17 at 15:13
  • @Fang my question is specific to incomplete types. – Dan M. Nov 17 '17 at 15:15
  • @Slava well, true. That's why I decided to ask to be sure. At least this case seems easy enough for compilers to figure out and not give a warning (but then again, they could've just not bother to implement such corner case). – Dan M. Nov 17 '17 at 15:15
  • 2
    Well either `C *c = nullptr; delete c;` should not be there anyway, because it does nothing and the compiler indeed would be able to detect this and optimize that out. But such a code indicates that something is wrong with your code, it is not really worth to implement this special case into the compiler. – t.niese Nov 17 '17 at 15:15
  • Isn't a runtime check required to determine if the pointer is null? So it is a check followed by a noop? – wally Nov 17 '17 at 15:15
  • @lukas probably `language-lower` tag should be added, but it is legit question anyway as standard could be ambiguous in this case. noop on one side, UB on another. – Slava Nov 17 '17 at 15:16
  • @Bo Persson, I clearly stated in my question why it's not a duplicate, please see the last paragraph and remove duplicate mark. I've read many similar SO questions (even with almost exactly the same code snippet) bu they all were about different thing and didn't answer my question. – Dan M. Nov 17 '17 at 15:19
  • Why not `delete nullptr;` directly? – iBug Nov 17 '17 at 15:21
  • @t.niese it's the most simple example. Same case might (and probably will) occur in, for example, some kind of smart pointers, where you know that the destructor can be called only if the stored ptr is null or the type is complete. – Dan M. Nov 17 '17 at 15:22
  • @iBug because `nullptr` has different type – Slava Nov 17 '17 at 15:22
  • @iBug there are differences between types and values. In `delete c` type is `C` with value `nullptr`. In `delete nullptr` value is `nullptr` with type `nullptr_t`. – Severin Pappadeux Nov 17 '17 at 15:26
  • I've swapped out one of your tags: I feel this is more of a lawyer question than a UB one. – Bathsheba Nov 17 '17 at 15:36

1 Answers1

17

The standard says ([expr.delete]/5):

If the object being deleted has incomplete class type at the point of deletion and the complete class has a non-trivial destructor or a deallocation function, the behavior is undefined.

So if T has a non-trivial destructor or has an operator delete overload, you get UB. Nothing is said about the UB being based on the value of the pointer (ie: whether it's a null pointer or not).


On what "object being deleted" mean?

One could consider that "object being deleted" means that this clause only applies to delete calls on actual objects. And therefore, if you pass a null pointer, it does not apply.

First, the rest of the standard discussion about the behavior of delete explicitly calls out that its behavior does not apply to null pointers. [expr.delete]/6&7 both start with "If the value of the operand of the delete-expression is not a null pointer value". Paragraph 5 explicitly does not contain these words. Therefore, we must assume it does apply to null pointers.

Second, what would the meaning of "object being deleted" be if it were passed a null pointer? After all, there is no "object" there.

Well, consider what it means to interpret this text if "object being deleted" talks specifically about the object at the end of that pointer. Well, what happens if you're deleting an array of incomplete classes with non-trivial destructors?

By that logic, this clause does not apply, whether the pointer is null or not. Why? Because the "object being deleted" is of an array type, not a class type. And therefore, this clause cannot apply. Which means that a compiler must be able to invoke delete[] on an array of incomplete classes.

But that's impossible to implement; it would require the compiler to be able to track down code that doesn't exist yet.

So either the "object being deleted" is intended to refer to std::remove_pointer_t<std::decay_t<decltype(expr)>>, or the standard requires behavior that is impossible to implement. The standard wording could probably be cleaned up a bit, replacing "If the object being deleted has incomplete class type at the point of deletion" with "If T is a pointer to U or an array of U, and U has incomplete class type at the point of deletion, ..."

Nicol Bolas
  • 378,677
  • 53
  • 635
  • 829
  • 5
    But if the value of the pointer is null then there is no object. So this doesn't apply. – wally Nov 17 '17 at 15:33
  • You should have posted link to the source too – Asesh Nov 17 '17 at 15:40
  • 1
    @rex: Whether it has an incomplete type is a compile-time matter. If the standard wanted to take into account whether the pointer is null, it would have said so, as it consistently does for all of the behavior *after* this statement. – Nicol Bolas Nov 17 '17 at 15:43
  • @rex Is it really clear? I mean if there is an overloaded operator delete, it is unspecified whether it gets called with a nullptr. That sounds pretty hazy, but admittedly only in a language lawyery kind of way. – Tim Seguine Nov 17 '17 at 15:43
  • 3
    @rex Many things that the standard declares to be UB would seem straight-forward defined behavior from a technical point of view. `((Foo*)nullptr)->someStaticVariable` for instance. No compiler in its right mind would actually emit code to dereference the null pointer here. Nevertheless, the statement is UB. And the mere fact that it is declared UB means that you must expect some (future) compiler to just throw away your UB invoking code as an optimization. – cmaster - reinstate monica Nov 17 '17 at 15:46
  • 1
    @rex: "*My contention is that it is not undefined behavior if delete is called on a null pointer and the delete operator is not overloaded.*" But this isn't supported by the standard. – Nicol Bolas Nov 17 '17 at 16:02
  • 3
    If the lifetime of the object didn't start, is it an object? I might be wrong here, and I see that the rest of the standard does call out "null pointer value" as a specific qualifier in the rest of the statements. In the end it seems the determining factor for UB is not the value of the pointer, but whether or not the delete operator has anything to do. – wally Nov 17 '17 at 16:08
  • 3
    What does "the object being deleted" refer to? The operand of the `delete` statement itself, or the object pointed to by the operand of the `delete` statement? (Genuine question, I don't have the standard.) If the former, this answer makes sense. If the latter, with the operand a null pointer, "the object being deleted" lacks a referent, much like "the current king of France", and the statement is simply inapplicable. "It would have made provisions for a null pointer" is irrelevant in that case; implicitly, it *does*, because a null pointer never points to an object. – Jeroen Mostert Nov 17 '17 at 17:05
  • @rex: And what if the pointer is a pointer to an *array* of classes with non-trivial destructors? By your reasoning, this clause would not apply since the "object being deleted" has an array type, not a class type. And therefore, the compiler would *have to* still make it work. Yet if the class is incomplete, it is not possible for the compiler to make it work. So either the standard contradicts itself or your interpretation is wrong. – Nicol Bolas Nov 17 '17 at 17:27
  • @Nicol but [expr.delete.3] reads "*In the second alternative (delete array) if the dynamic type of the object to be deleted differs from its static type, the behavior is undefined*" this implies that the "object to be deleted" refers to the first(or maybe any) element of the array, not the array itself (otherwise the statement above would make no sense). – Massimiliano Janes Nov 17 '17 at 17:50
  • @MassimilianoJanes: That makes even less sense. The "object to be deleted" is an array; its dynamic type and static type are the same. The array type deletes *multiple* subobjects of the array, so it cannot refer to a specific subobject of the array. – Nicol Bolas Nov 17 '17 at 18:06
  • @Nicol if the "object to be deleted" were an array, and given that its dynamic and static types cannot differ, [expr.delete.3] would state the obvious ... would this make sense ? I think not – Massimiliano Janes Nov 17 '17 at 18:10
  • So, on some level... you have to at least try to understand what the intent of the clause is. And the intent of the clause is **surely** to deal with the question of **incomplete types**. This thread is ... something else. – Barry Nov 17 '17 at 20:49
  • 2
    Remember, there's always option number 3: the standard has a defect here and its literal words are not accurately conveying its intention. Wouldn't exactly be the first time. I claim this is the case as soon as reasonable people can start arguing cogently over which interpretation is best. :-P – Jeroen Mostert Nov 17 '17 at 20:50