3

The C++20 standard says (see [expr.delete])

If the value of the operand of the delete-expression is a null pointer value, it is unspecified whether a deallocation function will be called as described above.

And cppreference.com says (see delete expression)

If expression evaluates to a null pointer value, no destructors are called, and the deallocation function may or may not be called (it's unspecified), but the default deallocation functions are guaranteed to do nothing when passed a null pointer.

Why would a compiler call a deallocation function, if the operand of the delete-expression is null?

The rule would be simpler if it would be guaranteed, that a deallocation function would not be called, but I suppose the standard allows this for a good reason.


PROPOSAL: In my opinion delete ptr; should be specified as equivalent to this:

if (ptr) {
   // call dtor.
   // call deallocation function
}

If ptr is known to be null or not null, we optimize accordingly. The deallocation functions would assume that their argument is not null, therefore no double != nullptr check would happen.

I think this is simpler.

Dr. Gut
  • 2,053
  • 7
  • 26
  • 5
    It just gives the compiler the ability to optimize out the call if it is known to be a nullptr. Requiring a test for nullptr would introduce additional code for no purpose since the typical case would not be a nullptr. – doug Apr 09 '23 at 01:21
  • 1
    It's not necessarily a given that the standard does (or doesn't do) anything for a "good reason". In any event, tagging something as "unspecified" is often a hint from the standards committee to the effect "We can't yet imagine any reason to do it one way or the other, but it makes no practical difference for now, so we're leaving the door open for future change. It is easier to leave something unspecified for now than to remove it entirely and have to reintroduce it later." – Peter Apr 09 '23 at 01:35
  • 1
    FWIW: gcc used to call the destructor even if it knew that the pointer value is null, but no longer does. [This question](https://stackoverflow.com/questions/45689690/why-doesnt-gcc-optimize-out-deletion-of-null-pointers-in-c?rq=1) talks about some more details. – ChrisB Apr 09 '23 at 01:40
  • 1
    Regarding your proposal to allow deallocation functions to omit the check: the dealloc function is most likely gonna be [`free`](https://github.com/llvm-mirror/libcxx/blob/78d6a7767ed57b50122a161b91f59f19c9bd0d19/src/new.cpp#L131), which has numerous tooling benefits. And that API cannot be changed for compatability reasons. Despite that, once you are calling a dynamically linked library function anyways, doing one additional check (easily branch predicted if done twice anyways) is not that big of a deal. – ChrisB Apr 09 '23 at 03:40

2 Answers2

6

From a historical perspective, the standard used to say in c++03 §5.3.5/2

[...] if the value of the operand of delete is the null pointer the operation has no effect.

This wording was found to be problematic and triggered a defect report:

Standard doesn't specify term "has no effect". It is not clear from this context, whether the called deallocation function is required to have no effect, or delete-expression shall not call the deallocation function.

Since compilers interpreted the old, ambiguous wording in different ways (see this example where GCC 7.5 and clang 7.0 have conflicting opinions on whether or not to insert this check), leaving it unspecified was the most compatible decision.

From the perspective of the optimizer, this is also preferrable, since it may allow for optimization opportunities, depending on circumstance, e.g.:

  • insert a null check to avoid the function call if that is the common case,
  • omit a function call if the pointer value is known to be null,
  • remove the check if the pointer value is known to be not null.
Dr. Gut
  • 2,053
  • 7
  • 26
ChrisB
  • 1,540
  • 6
  • 20
3

In the expression delete pT;, two things need to happen. The destructor of the object pointed to by pT needs to be called. And the deallocation function needs to be called.

However, if pT is nullptr, then you cannot do the first one, and you know the second one is a no-op. Therefore, it would make sense for a compiler to conceptually convert this expression to something to the effect of:

if(pT)
{
  pT->~T();
  deallocation_function(pT);
}

Note that if pT passes, then it gets checked twice: once here, and once in the deallocation function. But since we need to check it here before calling the destructor, that double unavoidable because the compiler doesn't own the deallocation function.

However, if T is trivially destructible (and therefore no actual code will execute), the compiler doesn't need to call the destructor. Which means the conditional check is now completely redundant. So, a smart compiler could optimize it out. And in those cases, the deallocation function will be called with a nullptr value.

This is all predicated on the assumption that deallocation functions will always do the nullptr check. This is what all of the standard library deallocation functions do, and it makes sense for most such functions.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982