Every C++ programmer knows new and delete and how they work. At least it must be so. I sure as hell didn’t know all the theory and detail behind C++ memory management facilities until 3-4 years ago, and I’m obviously still learning the practical details. So, please bear with me and see if there are things that you can learn about these old pals of ours, new and delete.
First, we all should know that new and delete are C++ operators, with all their facilities (and shortcomings, of course.) But not exactly like other operators, you can override them at a global level for every type that does not provide its own type-specific such operators. These global operators are provided as part of the C++ runtime library and are easily overridden. Their declarations are:
1
2
3
4
5
6
7
| // The single-object versions
void * operator new (size_t mem_size);
void operator delete (void * mem_block);
// The array versions
void * operator new[] (size_t mem_size);
void operator delete[] (void * mem_block); |
What new does is allocate a block of memory, and then call the constructor for the type with the address of the newly allocated block passed in as the this pointer. A delete call does the reverse; calling the destructor and then de-allocating the memory. The difference between the single-object versions and the array versions is only in the number of c’tor/d’tors they call. It amazes me how many C++ programmers don’t know and don’t care about details such as this (if you are programming in C++, these kind of details can and will bite you in the places you don’t want to be bitten!) If you fail to match them correctly, they allocate and de-allocate the correct amount of memory for the array or the single object, they just might not call constructors and destructors for all the objects being allocated or freed. That’s it.
Also, there is that small detail about exception-handling handling (yeah, two “handling”s!) The new operators may only throw an object of a sub-type of std::bad_alloc and only in the event that the requested amount of memory cannot be allocated. delete operators should never throw any exceptions (just like destructors. Remember that!) So, the correcter declaration for these operators would be:
1
2
3
4
5
6
7
| // The single-object versions
void * operator new (size_t mem_size) throw (std::bad_alloc);
void operator delete (void * mem_block) throw ();
// The array versions
void * operator new[] (size_t mem_size) throw (std::bad_alloc);
void operator delete[] (void * mem_block) throw (); |
Again, it is amazing how many programmers either don’t know about function exception specification and exception safety or just don’t use them (that includes me.) Of course, compiler providers are at least a little to blame here too. For example, Microsoft C++ compiler only distinguishes the empty exception list after a function declaration (meaning it doesn’t throw anything.) Anything else put there, just is taken to mean the default behavior is used (i.e. this function does throw something sometimes.)
In the meantime, the relationship between C++ programmers and exception handling remains in the love/hate/ignorance/hate/apathy/hate stage.
Later on, I’m going to talk abut overloading these global operators for fun and profit. Stay tuned!
Obviously, a related problem to memory management is calling the c’tor and d’tor for an object directly. Uses for this may not be immediately apparent, but as a few examples I could name implementing good smart pointers, memory pools, memory managers, garbage collectors, generic object containers (e.g. std::vector) and such.
You probably already know how to call the destructor on an instance directly. If you have a pointer x to an object of type T, you can call its d’tor like this: x->~T() (note that you should not generally call the d’tor in this way, unless you yourself have called the c’tor directly on that instance as well.) Calling the constructor is a bit trickier though (not really; I’m just being foreboding!)
What you should realize is that you can overload new and delete with different signatures that the ones above. You can add arguments and of different types. There are a few other signatures for these two provided by the standard C++ library (yeah, there are others!) The less interesting of them are:
8
9
10
11
12
| void * operator new (size_t mem_size, std::nothrow_t const & please_dont) throw ();
void operator delete (void * mem_block, std::nothrow_t const & please_dont) throw();
void * operator new[] (size_t mem_size, const std::nothrow_t & please_dont) throw ();
void operator delete[] (void * mem_block, std::nothrow_t const & please_dont) throw(); |
Forget about the deletes for a minute. The additional parameters to the new calls above are actually not used inside of the functions. Any object of type std::nothrow_t will suffice as the second parameter; it will be there just to signal the compiler to use this particular overload of the operator, which doesn’t throw any exceptions whatsoever. I just need to emphasize again that the regular new never returns a NULL pointer. It just throws an exception. But this one returns a 0 pointer upon failure and never throws anything, being it rocks, shoes or exceptions. The syntax for calling them, as you might suspect, is peculiar:
13
14
15
16
17
18
19
20
| #include <new> // for std::nothrow
//...
T * p = new (std::nothrow) T (/* usual constructor parameters */);
// The line above calls a particular "new" overload with two
// parameters: a size_t and a std::nothrow_t.
// Oh, and std::nothrow is just an object of type std::nothrow_t.
//...
delete p; |
Notice that I didn’t call the delete with any extra parameters or anything. In fact, there is no syntax in C++ for calling delete with any parameters! Besides, delete is already a non-throwing function. So what gives?! Why is there a paired delete for every frakking new when there is no frakking way of calling them?! You should keep your cool. I may explain them (if you don’t already know,) or we can leave the subject as an exercise. I would just say that the paired delete is called by the code generated by the compiler in a very specific situation.
Note that anything other than the straightforward, unary new and delete is called a “placement new/delete“. However, I’ve heard the term be used for a specific overload, which is more interesting and looks like this:
21
22
23
24
25
| void * operator new (size_t mem_size, void * mem_ptr) throw ();
void operator delete (void * mem_block, void * mem_ptr) throw ();
void * operator new[] (size_t mem_size, void * mem_ptr) throw ();
void operator delete[] (void * mem_block, void * mem_ptr) throw (); |
The implementations for the above operators are really simple. Here’s a complete listing:
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| void * operator new (size_t mem_size, void * mem_ptr) throw ()
{
return mem_ptr;
}
void operator delete (void * mem_block, void * mem_ptr) throw ()
{
}
void * operator new[] (size_t mem_size, void * mem_ptr) throw ()
{
return mem_ptr;
}
void operator delete[] (void * mem_block, void * mem_ptr) throw ()
{
} |
Note that although I haven’t written it, the constructor and destructor calls happen outside of my control. These versions of new and delete are used when we don’t want to allocate or free any memory, and just want the constructors and destructors to be called. For new, we just pass in a pointer to another sufficiently-sized memory location and ask the compiler to generate the code to call the c’tor upon that area of memory. That’s how we call a c’tor directly. We procure some memory area from somewhere and use that, like this:
43
44
45
| T * x = (T *)malloc (10 * sizeof(T));
for (unsigned i = 0; i < 10; ++i)
x[i] = new (x + i) T (/*usual c'tor params. */); |
These standard placement operators cannot be hidden or overridden in user code, but there is still a ton of fun to be had.
You can very easily replace the old, simple and default operators with an implementation of yours, a la:
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| void * operator new (size_t mem_size)
{
void * ret = malloc (mem_size);
if (0 == ret)
throw std::bad_alloc ();
return ret;
}
void delete (void * mem_block)
{
free (mem_block);
}
// the array versions are exactly the same |
As I have stated earlier, the c’tor/d’tor calls are generated automatically for you by the compiler. So now you are free to write your own memory manager!
The way that memory manager/debugger/helper/whatevers usually work under the hood is that they allocate more memory than you have requested, and put their own junk right before and/or right after the area that gets passed back to the user (that’s a bad way to do memory management, but that one is a long story.) Some of the stuff that are usually kept there include a pointer to the next and/or previous allocated block of memory (so all the blocks form a linked list,) sentinel values right before and right after the user area to detect buffer overruns (e.g. 0xdeadbabe,) the size of the memory block, the code file/line/function/module that allocated this particular block and so on and so forth. Actually, your default memory manager in the CRT is doing this right now. Just new two large-enough blocks of memory and compare their address differences with the size of the first block. The runtime accompanying some compilers (like VC++) even exposes their internal data structures and means to work with the memory manager (although rather passively.)
You need to keep in mind though, that what I have discussed so far barely scratches the surface of writing memory managers. These are just practicalities and implementation details; the state of the art on the theory of the matter and memory allocation algorithms, policies and mechanisms can fill several books. Even on the implementation side, there are really serious issues with performance, cache-friendliness, thread-safety, multiple thread support, etc. need taking care of.
Besides, much (if not most) of the memory (de)allocation going on in a C++ program these days go through C or operation system API, shared object files (DLLs,) through third part code or through STL, all of which bypass the basic technique discussed above. So, if you really are serious, you should investigate the existing memory debuggers or memory leak detectors or memory managers. There are several open source ones out there, with various degrees of sophistication and complexity. Have fun! (But for what it’s worth, I should mention that we have used a memory leak detector using nothing but this technique in Garshasp and a similar project before, and in both projects it has been a great help.)
VN:F [1.8.4_1055]
casting vote; please wait...
Rating: 8.0/10 (9 votes cast)
VN:F [1.8.4_1055]
Rating: +1 (from 5 votes)