19.1. Controlling Memory Allocation
Some applications have specialized memory allocation needs that cannot be met by the standard memory management facilities. Such applications need to take over the details of how memory is allocated, for example, by arranging for new
to put objects into particular kinds of memory. To do so, they can overload the new
and delete
operators to control memory allocation.
19.1.1. Overloading new
and delete
Although we say that we can “overload new
and delete
,” overloading these operators is quite different from the way we overload other operators. In order to understand how we overload these operators, we first need to know a bit more about how new
and delete
expressions work.
When we use a new
expression:
// new expressions
string *sp = new string("a value"); // allocate and initialize a string
string *arr = new string[10]; // allocate ten default initialized strings
three steps actually happen. First, the expression calls a library function named operator new
(or operator new[]
). This function allocates raw, untyped memory large enough to hold an object (or an array of objects) of the specified type. Next, the compiler runs the appropriate constructor to construct the object(s) from the specified initializers. Finally, a pointer to the newly allocated and constructed object is returned.
When we use a delete
expression to delete a dynamically allocated object:
delete sp; // destroy *sp and free the memory to which sp points
delete [] arr; // destroy the elements in the array and free the memory
two steps happen. First, the appropriate destructor is run on the object to which sp
points or on the elements in the array to which arr
points. Next, the compiler frees the memory by calling a library function named operator delete
or operator delete[]
, respectively.
Applications that want to take control of memory allocation define their own versions of the operator new
and operator delete
functions. Even though the library contains definitions for these functions, we can define our own versions of them and the compiler won’t complain about duplicate definitions. Instead, the compiler will use our version in place of the one defined by the library.
WARNING
When we define the global operator new
and operator delete
functions, we take over responsibility for all dynamic memory allocation. These functions must be correct: They form a vital part of all processing in the program.
Applications can define operator new
and operator delete
functions in the global scope and/or as member functions. When the compiler sees a new
or delete
expression, it looks for the corresponding operator
function to call. If the object being allocated (deallocated) has class type, the compiler first looks in the scope of the class, including any base classes. If the class has a member operator new
or operator delete
, that function is used by the new
or delete
expression. Otherwise, the compiler looks for a matching function in the global scope. If the compiler finds a user-defined version, it uses that function to execute the new
or delete
expression. Otherwise, the standard library version is used.
We can use the scope operator to force a new
or delete
expression to bypass a class-specific function and use the one from the global scope. For example, ::new
will look only in the global scope for a matching operator new
function. Similarly for ::delete
.
The operator new
and operator delete
Interface
The library defines eight overloaded versions of operator new
and delete
functions. The first four support the versions of new
that can throw a bad_alloc
exception. The next four support nonthrowing versions of new
:
// these versions might throw an exception
void *operator new(size_t); // allocate an object
void *operator new[](size_t); // allocate an array
void *operator delete(void*) noexcept; // free an object
void *operator delete[](void*) noexcept; // free an array
// versions that promise not to throw; see § 12.1.2 (p. 460)
void *operator new(size_t, nothrow_t&) noexcept;
void *operator new[](size_t, nothrow_t&) noexcept;
void *operator delete(void*, nothrow_t&) noexcept;
void *operator delete[](void*, nothrow_t&) noexcept;
The type nothrow_t
is a struct
defined in the new
header. This type has no members. The new
header also defines a const
object named nothrow
, which users can pass to signal they want the nonthrowing version of new
(§ 12.1.2, p. 460). Like destructors, an operator delete
must not throw an exception (§ 18.1.1, p. 774). When we overload these operators, we must specify that they will not throw, which we do through the noexcept
exception specifier (§ 18.1.4, p. 779).
An application can define its own version of any of these functions. If it does so, it must define these functions in the global scope or as members of a class. When defined as members of a class, these operator functions are implicitly static (§ 7.6, p. 302). There is no need to declare them static
explicitly, although it is legal to do so. The member new
and delete
functions must be static because they are used either before the object is constructed (operator new)
or after it has been destroyed (operator delete)
. There are, therefore, no member data for these functions to manipulate.
An operator new
or operator new[]
function must have a return type of void*
and its first parameter must have type size_t
. That parameter may not have a default argument. The operator new
function is used when we allocate an object; operator new[]
is called when we allocate an array. When the compiler calls operator new
, it initializes the size_t
parameter with the number of bytes required to hold an object of the specified type; when it calls operator new[]
, it passes the number of bytes required to store an array of the given number of elements.
When we define our own operator new
function, we can define additional parameters. A new
expression that uses such functions must use the placement form of new
(§ 12.1.2, p. 460) to pass arguments to these additional parameters. Although generally we may define our version of operator new
to have whatever parameters are needed, we may not define a function with the following form:
void *operator new(size_t, void*); // this version may not be redefined
This specific form is reserved for use by the library and may not be redefined.
An operator delete
or operator delete[]
function must have a void
return type and a first parameter of type void*
. Executing a delete
expression calls the appropriate operator
function and initializes its void*
parameter with a pointer to the memory to free.
When operator delete
or operator delete[]
is defined as a class member, the function may have a second parameter of type size_t
. If present, the additional parameter is initialized with the size in bytes of the object addressed by the first parameter. The size_t
parameter is used when we delete objects that are part of an inheritance hierarchy. If the base class has a virtual destructor (§ 15.7.1, p. 622), then the size passed to operator delete
will vary depending on the dynamic type of the object to which the deleted pointer points. Moreover, the version of the operator delete
function that is run will be the one from the dynamic type of the object.
INFO
Terminology: new
Expression versus operator new
Function
The library functions operator new
and operator delete
are misleadingly named. Unlike other operator
functions, such as operator=
, these functions do not overload the new
or delete
expressions. In fact, we cannot redefine the behavior of the new
and delete
expressions.
A new
expression always executes by calling an operator new
function to obtain memory and then constructing an object in that memory. A delete
expression always executes by destroying an object and then calling an operator delete
function to free the memory used by the object.
By providing our own definitions of the operator new
and operator delete
functions, we can change how memory is allocated. However, we cannot change this basic meaning of the new
and delete
operators.
The malloc
and free
Functions
If you define your own global operator new
and operator delete
, those functions must allocate and deallocate memory somehow. Even if you define these functions in order to use a specialized memory allocator, it can still be useful for testing purposes to be able to allocate memory similarly to how the implementation normally does so.
To this end, we can use functions named malloc
and free
that C++ inherits from C. These functions, are defined in cstdlib
.
The malloc
function takes a size_t
that says how many bytes to allocate. It returns a pointer to the memory that it allocated, or 0 if it was unable to allocate the memory. The free
function takes a void*
that is a copy of a pointer that was returned from malloc
and returns the associated memory to the system. Calling free(0)
has no effect.
A simple way to write operator new
and operator delete
is as follows:
void *operator new(size_t size) {
if (void *mem = malloc(size))
return mem;
else
throw bad_alloc();
}
void operator delete(void *mem) noexcept { free(mem); }
and similarly for the other versions of operator new
and operator delete
.
INFO
Exercises Section 19.1.1
Exercise 19.1: Write your own operator new(size_t)
function using malloc
and use free
to write the operator delete(void*)
function.
Exercise 19.2: By default, the allocator
class uses operator new
to obtain storage and operator delete
to free it. Recompile and rerun your StrVec
programs (§ 13.5, p. 526) using your versions of the functions from the previous exercise.
19.1.2. Placement new
Expressions
Although the operator new
and operator delete
functions are intended to be used by new
expressions, they are ordinary functions in the library. As a result, ordinary code can call these functions directly.
In earlier versions of the language—before the allocator
(§ 12.2.2, p. 481) class was part of the library—applications that wanted to separate allocation from initialization did so by calling operator new
and operator delete
. These functions behave analogously to the allocate
and deallocate
members of allocator
. Like those members, operator new
and operator delete
functions allocate and deallocate memory but do not construct or destroy objects.
Differently from an allocator
, there is no construct
function we can call to construct objects in memory allocated by operator new
. Instead, we use the placementnew
form of new
(§ 12.1.2, p. 460) to construct an object. As we’ve seen, this form of new
provides extra information to the allocation function. We can use placement new
to pass an address, in which case the placement new
expression has the form
new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] { braced initializer list }
where place_address must be a pointer and the initializers provide (a possibly empty) comma-separated list of initializers to use to construct the newly allocated object.
When called with an address and no other arguments, placement new
uses operator new(size_t, void*)
to “allocate” its memory. This is the version of operator new
that we are not allowed to redefine (§ 19.1.1, p. 822). This function does not allocate any memory; it simply returns its pointer argument. The overall new
expression then finishes its work by initializing an object at the given address. In effect, placement new
allows us to construct an object at a specific, preallocated memory address.
INFO
When passed a single argument that is a pointer, a placement new
expression constructs an object but does not allocate memory.
Although in many ways using placement new
is analogous to the construct
member of an allocator
, there is one important difference. The pointer that we pass to construct
must point to space allocated by the same allocator
object. The pointer that we pass to placement new
need not point to memory allocated by operator new
. Indeed, as we’ll see in § 19.6 (p. 851), the pointer passed to a placement new
expression need not even refer to dynamic memory.
Explicit Destructor Invocation
Just as placement new
is analogous to using allocate
, an explicit call to a destructor is analogous to calling destroy
. We call a destructor the same way we call any other member function on an object or through a pointer or reference to an object:
string *sp = new string("a value"); // allocate and initialize a string
sp->~string();
Here we invoke a destructor directly. The arrow operator dereferences the pointer sp
to obtain the object to which sp
points. We then call the destructor, which is the name of the type preceded by a tilde (~
).
Like calling destroy
, calling a destructor cleans up the given object but does not free the space in which that object resides. We can reuse the space if desired.
INFO
Calling a destructor destroys an object but does not free the memory.