Overloading in C ++. Part III. Overloading new / delete statements


We continue the series "C ++, digging in depth." The purpose of this series is to tell you as much as possible about the various features of the language, possibly quite special. This article is about operator overloading new/delete. This is the third article in the series, the first dedicated to overloading functions and templates, located here , the second dedicated to overloading operators, located here . This article concludes a three-article series on overloading in C ++.


Table of contents


Table of contents
  1. new/delete
    1.1.
    1.2.
    1.3.
  2. new/delete
    2.1.
    2.2.
      2.2.1. new/delete
      2.2.2. new/delete
      2.2.3.
      2.2.4.
      2.2.5. operator delete()
  3. new/delete
  4.
  5. -
  

1. Standard forms of new / delete operators


C ++ supports several operator options new/delete. They can be divided into basic standard, additional standard and custom. This section and section 2 discuss standard forms; custom forms will be discussed in section 3.

1.1. Basic standard forms


The main standard forms of operators new/deleteused when creating and deleting an object and an array of the type are Tas follows:

new T(/*   */)
new T[/*   */]
delete ptr;
delete[] ptr;

Their work can be described as follows. When the operator is called new, memory is first allocated to the object. If the selection is successful, the constructor is called. If the constructor throws an exception, then the allocated memory is freed. When the operator is called, deleteeverything happens in the reverse order: first, the destructor is called, then the memory is freed. The destructor should not throw exceptions.

When the operatornew[]used to create an array of objects, memory is first allocated for the entire array. If the selection is successful, then the default constructor (or another constructor, if there is an initializer) is called for each element of the array starting from zero. If any constructor throws an exception, then for all created array elements the destructor is called in the reverse order of the constructor call, then the allocated memory is freed. To delete an array, you must call the operator delete[], and for all elements of the array, the destructor is called in the reverse order of the constructor, then the allocated memory is freed.

Attention! It is necessary to call the correct form of the operatordeletedepending on whether a single object or an array is deleted. This rule must be observed strictly, otherwise you can get undefined behavior, that is, anything can happen: memory leaks, crash, etc. See [Meyers1] for details.

In the above description, one clarification is necessary. For the so-called trivial types (built-in types, C-style structures), the default constructor may not be called, and the destructor does nothing in any case.

The standard memory allocation functions, when it is not possible to satisfy the request, throw a type exception std::bad_alloc. But this exception can be caught, for this you need to install a global interceptor using a function call set_new_handler(), for more details see [Meyers1].

Any form of operatordeleteapply safely to null pointer.

When creating an array with an operator, the new[]size can be set to zero.

Both forms of the operator newallow the use of initializers in braces.

new int{42}
new int[8]{1,2,3,4}

1.2. Additional standard forms


When connecting the header file <new>, 4 more standard operator forms become available new:

new(ptr) T(/*  */);
new(ptr) T[/*   */];
new(std::nothrow) T(/*   */);
new(std::nothrow) T[/*   */];

The first two of them are called newnon-allocating placement new. An argument ptris a pointer to a region of memory that is large enough to hold an instance or array. Also, the memory area must have an appropriate alignment. This version of the operator newdoes not allocate memory; it provides only a call to the constructor. Thus, this option allows you to separate the phases of memory allocation and initialization of objects. This feature is actively used in standard containers. The operator deletefor objects created in this way cannot, of course, be called. To delete an object, you must directly call the destructor, and then free the memory in a way that depends on the method of allocating memory.

The second two options are called the operator not throwing exceptions new(nothrow new) and differ in that if it is impossible to satisfy the request, they return nullptr, but do not throw a type exception std::bad_alloc. Deleting an object occurs using the main operator delete. These options are considered obsolete and not recommended for use.

1.3. Memory Allocation and Free Functions


Standard forms of operators new/deleteuse the following allocation and deallocation functions:

void* operator new(std::size_t size);
void operator delete(void* ptr);
void* operator new[](std::size_t size);
void operator delete[](void* ptr);
void* operator new(std::size_t size, void* ptr);
void* operator new[](std::size_t size, void* ptr);
void* operator new(std::size_t size, const std::nothrow_t& nth);
void* operator new[](std::size_t size, const std::nothrow_t& nth);

These functions are defined in the global namespace. The memory allocation functions for the host statements newdo nothing and simply return ptr.

C ++ 17 supported additional forms of memory allocation and deallocation functions, indicating alignment. Here are some of them:

void* operator new (std::size_t size, std::align_val_t al);
void* operator new[](std::size_t size, std::align_val_t al);

These forms are not directly accessible to the user, they are used by the compiler for objects whose alignment requirements are superior __STDCPP_DEFAULT_NEW_ALIGNMENT__, so the main problem is that the user does not accidentally hide them (see section 2.2.1). Recall that in C ++ 11 it became possible to explicitly set the alignment of user types.

struct alignas(32) X { /* ... */ };

2. Overloading the standard forms of new / delete operators


Overloading the standard forms of operators new/deleteconsists in defining user-defined functions for allocating and freeing memory whose signatures coincide with the standard ones. These functions can be defined in the global namespace or in a class, but not in a namespace other than global. The memory allocation function for a standard host statement newcannot be defined in the global namespace. After such a definition, the corresponding operators new/deletewill use them, not the standard ones.

2.1. Overload in the global namespace


Suppose, for example, in a module in a global namespace that user-defined functions are defined:

void* operator new(std::size_t size)
{
// ...
}

void operator delete(void* ptr)
{
// ...
}

In this case, there will actually be a replacement (replacement) of the standard functions for allocating and freeing memory for all operator calls new/deletefor any classes (including standard ones) in the entire module. This can lead to complete chaos. Note that the described substitution mechanism is a special mechanism implemented only for this case, and not some general C ++ mechanism. In this case, when implementing the user functions for allocating and freeing memory, it becomes impossible to call the corresponding standard functions, they are completely hidden (the operator ::does not help), and when you try to call them, a recursive call to the user function occurs.

Global Namespace Defined Function

void* operator new(std::size_t size, const std::nothrow_t& nth)
{
// ...
}

It will also replace the standard one, but there will be fewer potential problems, because the operator that does not throw exceptions newis rarely used. But the standard form is also not available.

The same situation with functions for arrays.

Overloading statements new/deletein the global namespace is strongly discouraged.

2.2. Class overload


Overloading operators new/deletein a class is devoid of the disadvantages described above. Overloading is effective only when creating and deleting instances of the corresponding class, regardless of the context of invoking operators new/delete. When implementing user-defined functions for allocating and freeing memory using the operator, ::you can access the corresponding standard functions. Consider an example.

class X
{
// ...
public:
    void* operator new(std::size_t size)
    {
        std::cout << "X new\n";
        return ::operator new(size);
    }

    void operator delete(void* ptr)
    {
        std::cout << "X delete\n";
        ::operator delete(ptr);
    }

    void* operator new[](std::size_t size)
    {
        std::cout << "X new[]\n";
        return ::operator new[](size);
    }

    void operator delete[](void* ptr)
    {
        std::cout << "X delete[]\n";
        ::operator delete[](ptr);
    }
};

In this example, tracing is simply added to standard operations. Now, in terms new X()and new X[N]will use these functions to allocate and free memory.

These functions are formally static and can be declared as static. But essentially they are instance, with the function call operator new(), the creation of the instance begins, and the call of the function operator delete()completes its deletion. These functions are never called for other tasks. Moreover, as will be shown below, the function operator delete()is essentially virtual. So itโ€™s more correct to declare them without static.

2.2.1. Access to standard forms of new / delete operators


Operators new/deletecan be used with an additional scope resolution operator, for example ::new(p) X(). In this case, the function operator new()defined in the class will be ignored, and the corresponding standard will be used. In the same way, you can use the operator delete.

2.2.2. Hiding other forms of new / delete operators


If now for the class Xwe try to use throwing or not throwing exceptions new, we get an error. The fact is that the function operator new(std::size_t size)will hide other forms operator new(). The problem can be solved in two ways. In the first, you need to add the appropriate options to the class (these options should simply delegate the operation of the standard function). In the second, you need to use an operator newwith a scope resolution operator, for example ::new(p) X().

2.2.3. Standard containers


If we try to place the instances Xin some standard container, for example std::vector<X>, we will see that our functions are not used to allocate and free memory. The fact is that all standard containers have their own mechanism for allocating and freeing memory (a special allocator class, which is a template parameter of the container), and they use a placement operator to initialize the elements new.

2.2.4. Inheritance


Functions for allocating and freeing memory are inherited. If these functions are defined in the base class, but not in the derived one, then the operators will also be overloaded for the derived class new/deleteand the functions defined and allocated in the base class will be used to allocate and free memory.

Now consider a polymorphic class hierarchy, where each class overloads operators new/delete. Now, let the instance of the derived class be deleted using the operator deletethrough a pointer to the base class. If the destructor of the base class is virtual, then the standard guarantees that the destructor of this derived class is called. In this case operator delete(), the function call defined for this derived class is also guaranteed . Thus, the function is operator delete()actually virtual.

2.2.5. Alternative form of operator delete () function


In a class (especially when inheritance is used), it is sometimes convenient to use an alternative form of the function to free memory:

void operator delete(void* p, std::size_t size);
void operator delete[](void* p, std::size_t size);

The parameter sizesets the size of the element (even in the version for the array). This form allows you to use different functions to allocate and free memory, depending on the specific derived class.

3. User operators new / delete


C ++ can support custom operator forms of the newfollowing form:

new(/*  */) T(/*   */)
new(/*  */) T[/*   */]

In order for these forms to be supported, it is necessary to determine the appropriate functions for allocating and freeing memory:

void* operator new(std::size_t size, /* .  */);
void* operator new[](std::size_t size, /* .  */);
void operator delete(void* p, /* .  */);
void operator delete[](void* p, /* .  */);

The list of additional parameters of the memory allocation functions should not be empty and should not consist of one void*or const std::nothrow_t&, that is, their signature should not coincide with one of the standard ones. Lists of additional parameters in operator new()and operator delete()must match. Arguments passed to the operator newmust correspond to additional parameters of the memory allocation functions. A custom function operator delete()can also be in the form with an optional size parameter.

These functions can be defined in the global namespace or in a class, but not in a namespace other than global. If they are defined in the global namespace, they do not replace, but overload the standard functions of allocating and freeing memory, so their use is predictable and safe, and standard functions are always available. If they are defined in the class, they hide the standard forms, but access to standard forms can be obtained using the operator ::, this is described in detail in section 2.2.

User -defined operator forms neware called newuser-defined placement new. They should not be confused with the standard (non-allocating) placement operator newdescribed in section 1.2.

The corresponding operator form deletedoes not exist. There neware two ways to delete an object created using a user-defined operator . If a user-defined function operator new()delegates a memory allocation operation to standard memory allocation functions, then a standard operator can be used delete. If not, you will have to explicitly call the destructor, and then the user-defined function operator delete(). The compiler calls the user-defined function in operator delete()only one case: when the newconstructor throws an exception during the operation of the user-defined operator .

Here is an example (in global scope).

void* operator new(std::size_t size, int a, const char* b)
{
    std::cout << "new " << a << " + " << b << "\n";
    return ::operator new(size);
}
void operator delete(void* p, int a, const char* b)
{
    std::cout << "delete " << a << " + " << b << "\n";
    ::operator delete(p);
}

class X {/* ... */};
X* p = new(42, "meow") X(); // : new 42 + meow
delete p; //   ::operator delete()

4. Definition of memory allocation functions


In these examples, user functions operator new()and operator delete()delegated operation corresponding to the standard functions. Sometimes this option is useful, but the main goal of overloading new/deleteis to create a new mechanism for allocating / freeing memory. The task is not simple, and before undertaking it, one must carefully think through everything. Scott Meyers [Meyers1] discusses possible motives for making such a decision (of course, the main one is efficiency). He also discusses the main technical problems associated with the correct implementation of user-defined functions for allocating and freeing memory (using the functionset_new_handler(), multi-threaded synchronization, alignment). Guntheroth provides an example of the implementation of relatively simple user-defined memory allocation and deallocation functions. Before creating your own version, you should look for ready-made solutions, as an example, you can bring the Pool library from the Boost project.

5. Allocator classes of standard containers


As mentioned above, standard containers use special allocator classes for allocating and freeing memory. These classes are template parameters of containers and the user can define his version of such a class. The motives for such a solution are approximately the same as for overloading operators new/delete. [Guntheroth] describes how to create such classes.

Bibliography


[Guntheroth]
Gunteroth, Kurt. Optimization of programs in C ++. Proven methods for increasing productivity .: Per. from English - SPb .: Alpha-book LLC, 2017.
[Meyers1]
Meyers, Scott. Effective use of C ++. 55 sure ways to improve the structure and code of your programs .: Per. from English - M.: DMK Press, 2014.

All Articles