c++new-operatornoexcept

Unexpected call to constructor when non-throwing operator new returns a nullptr


In the following code, there is a no-throw operator new for the struct TestNew. It is required to further declare it as noexcept to ensure that the constructor is not called, when operator new returns a nullptr. It is surprising to see the need for explicit noexcept declaration, as this seems redundant given the std::nothrow_t tag.

#include <cstdlib>
#include <iostream>
#include <new>

struct TestNew {
  void* operator new(std::size_t, std::nothrow_t) noexcept { // Doesn't call the constructor
  // void* operator new(std::size_t, std::nothrow_t) { // Unexpected call to constructor
    std::cout << "operator new of TestNew called!" << std::endl;
    return nullptr;
  }
  TestNew() {
    std::cout << "A TestNew created!" << std::endl;
  }
};

int main() {
  auto p = new (std::nothrow) TestNew;
  if (!p) {
    std::cout << "nullptr!" << std::endl;
    std::abort();
  }

  return 0;
}

Both GCC 14.1 and Clang 18.1.0 give the following output when operator new is not declared as noexcept:

operator new of TestNew called!  
A TestNew created!  
nullptr!

With the noexcept declaration, the output is the following:

operator new of TestNew called!  
nullptr!

Both give warnings, though:

GCC: warning: 'operator new' must not return NULL unless it is declared 'throw()' (or '-fcheck-new' is in effect)

Clang: warning: 'operator new' should not return a null pointer unless it is declared 'throw()' or 'noexcept' [-Wnew-returns-null]

Update: The accepted answer has made it clear that there are two necessary aspects for a non-throwing operator new (whether user implemented or not). One, the std::nothrow_t tag which is essential to pick the correct function when new (std::nothrow) is called. Two, the function to be declared as noexcept, which is expected by the compiler. std::nothrow_t only does what is expected of a tag type, which is to help with overload resolution. It cannot tell the compiler that a function will not throw exceptions.


Solution

  • When you override operator new, you have to follow the rules for allocation functions.

    An allocation function that has a non-throwing exception specification ([except.spec]) indicates failure by returning a null pointer value.

    Any other allocation function never returns a null pointer value and indicates failure only by throwing an exception ([except.throw]) of a type that would match a handler ([except.handle]) of type std​::​bad_­alloc ([bad.alloc]).

    [basic.stc.dynamic.allocation]

    This isn't a statement about the built-in new, it is a requirement on all news. When you fail to follow the rules, your program has undefined behaviour. That can include constructing an instance of your class at the null address.