C++ / a working model

42 / 163   ·   C++11   ·   11 min

new/delete Expressions and Allocation Functions

Keep this sentence

A new expression usually obtains storage and initializes an object; a delete expression usually destroys the object and releases storage. operator new / operator delete are the underlying allocation functions inside those expressions, not equivalents of the full expressions. Understanding that layering is what lets you handle exceptions, arrays, and in-place construction correctly.

In this lesson
  1. The Expression Performs the Complete Object Operation
  2. Arrays, Initialization, and the Failure Protocol
  3. Expand the Steps Only for Low-Level Wrapping
  4. Example
  5. Exercise

The Expression Performs the Complete Object Operation

new T(args) is a language expression. It selects a suitable allocation function, obtains storage, and performs initialization. Calling ::operator new(sizeof(T)) directly yields raw storage, which is not a fully constructed T of arbitrary type. Correspondingly, a delete expression is responsible for the object destruction sequence; calling operator delete directly will not invoke the class destructor for you. Mixing those layers is how destructors get skipped.

new does not always request a fresh heap block. Standard non-allocating placement new takes an address supplied by the caller and constructs an object in existing storage. The caller must first guarantee size, alignment, and lifetime conditions, and must arrange the eventual return of that storage. The address must not be handed casually to an ordinary delete.

Arrays, Initialization, and the Failure Protocol

new T and new T[n] must be matched with delete and delete[] respectively; array release has to handle every element. new int does not initialize the integer value, while new int{} value-initializes it to zero. Do not demonstrate the difference by reading an uninitialized integer; stating the rule is enough.

Ordinary throwing allocation reports failure with bad_alloc. The nothrow allocation form returns a null pointer on failure, but an exception from the constructor itself can still propagate out of a nothrow new expression. If an ordinary new expression has already allocated successfully and construction then throws, the matching deallocation function is called to return that storage.

Expand the Steps Only for Low-Level Wrapping

The example obtains storage directly and immediately places it in an owner that uses operator delete, then constructs a Token with placement new, checks its state, and destroys it explicitly. Neither Token construction nor destruction throws, and the sample's access region has no throwing business operation, so the cleanup order is complete and traceable.

If real business code performs a throwing operation after construction, you must still separately guarantee that the object is destroyed; a release guard on raw storage is not enough. Usually make_unique already combines these steps correctly. malloc likewise does not construct, but it has a different failure and release protocol. Do not mix the interfaces merely because some implementation reuses malloc internally.

Pitfalls

  • Running ordinary delete on automatic arrays or allocator storage used with placement new violates that storage's release protocol.
  • nothrow only changes how the related allocation failure is reported; it is not a switch that swallows exceptions from the whole construction process.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <cassert>
#include <memory>
#include <new>

struct Deallocate {
    void operator()(void* p) const noexcept { ::operator delete(p); }
};
struct Token {
    static int alive;
    int value;
    explicit Token(int n) noexcept : value(n) { ++alive; }
    ~Token() noexcept { --alive; }
};
int Token::alive = 0;

int main() {
    std::unique_ptr<void, Deallocate> storage(::operator new(sizeof(Token)));
    assert(Token::alive == 0);
    Token* token = ::new (storage.get()) Token(7);
    assert(Token::alive == 1 && token->value == 7);
    token->~Token();
    assert(Token::alive == 0);
}

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread memory-new-delete.cpp -o example && ./example

Expected result

Expected: exit 0, no output; every assert holds.

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

You call operator new directly and then construct T with placement new. If T's constructor throws, who returns the storage obtained earlier?

Show a reference answer

The placement delete that corresponds to standard non-allocating placement new does not return this external storage; the caller still owns that duty. You can, as in the example, install an owner for the raw storage first so that a failed constructor automatically calls the matching operator delete. If you write an ordinary new T(args) instead, the expression handles the matching deallocation when construction fails. At a higher level you can use make_unique directly.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog