Author : Yordan Gyurchev
Page : 1 Next >>
or several ways to write the SafeDelete function
by Yordan Gyurchev
Abstract
I’ll be discussing here deletion of dynamically allocated pointers to objects. This article is not to say something revolutionary new but instead summarizes several forum postings, book chapters and personal thoughts.
I’m using the term “undefined behavior” several times. Many programmers wrongly assume that “indefinite behavior” always results in obvious failure like exception or segmentation fault. Though this is sometimes true it often results in more subtle and hard to find mistakes. I like Scott Meyers definition of this: “works for me, works for you, works during development and QA, but blows up in your most important customer’s face” [2].
Deleting dynamically allocated pointers
Every object has its lifetime. It is “born” at some point (its life starts – right after the constructor is successfully finished) and deceases in another (start of the destructor) [3]. Take heed that if an exception is thrown out of the constructor the object is not considered created. (Refer to [3] for a deep discussion of object lifetime and exceptions during construction and destruction of an object)
Dynamically allocated objects start their lives with a “new” and end with a “delete”. New and delete are responsible not only for memory allocation/deallocation process but also for calling constructors/destructors respectively. This sequence is quite straightforward though this is the place where the things can go really bad.
Programmers are constantly in war with the consequences of this very straightforward combination. Lots of “programming” weapons were developed during this never-ending battle. To name some of them: smart pointers, resource and memory managers, custom allocation schemes, garbage collectors etc.
One of the simplest weapons is SafeDelete function.
It has to accomplish the following tasks:
1. Check if the object is already NULL
2. Delete “properly” the object
3. Assign a NULL to the pointer to indicate a clear one
Check if the object is NULL
The C++ Standard ensures that NULL pointers will be safely deleted. This means that “delete NULL” is perfectly legal and has a defined behavior. Some people will argue that some compilers are not quite “standard” though. Despite this issue it is useful to check for NULL just for debugging purposes in order to track double deletions of the same pointer (dangling pointer behavior)
Delete “properly” the object
Proper deletion of an object means that its destructor is properly invoked. Consider the case:
void SafeDelete(void* pointer)
{
if (pointer)
{
delete pointer;
}
else
{
DeleteNull();
}
}
The void pointer will be deleted but the destructor of the objects will not be called because the voidness pointer provides no knowledge of the type of the object. What if we have inherited objects and virtual destructor – again failure. All we get with this function is undefined behavior. Moreover, if we have some custom allocation schemes and overload “new” and “delete”, we will probably end with the wrong delete operator called using this function. Hence it cannot be used. Remember: the object has to be properly destroyed.
Assign a NULL to the pointer to indicate a clear one
Assigning NULL to the pointer in question will ensure it has the proper value after the deletion. Otherwise we can delete the same pointer twice. The NULL check will fail, the pointer will be deleted again and we will get undefined behavior.
Macros - The good old times
The good old times were dominated by C language. The macro was common (if not the only possible) and very useful technique.
Here is how our function (macro) would have looked in these times.
#define SAFEDELETE(pointer) if((pointer)) \
{ delete (pointer); pointer = NULL; }
The pointer is checked for NULL and is deleted. As the macro is simply substitution the “delete” operation will be properly compiled and the object will get properly deleted.
Though widely used in the past macros are a bit out of date with the C++ language. They are not type safe, they are not scoped by namespaces, and they are source of many bugs and should be used with a lot of care.
Our (C++) days – better SafeDelete functions
C++ as a type safe language strongly discourages use of macros. Instead a programmer can use inline functions and templates. Inline functions are fine but wont do much in this case as we would have to write an inline function for each object type that has to be deleted. On other hand the templates are what we want. (Refer to your introductory C++ book for more detailed explanation of templates, like Bruce Eckel’s
Thinking in C++). They can be implicitly instantiated and are type safe. Here is our first template version of SafeDelete.
template<typename PointerType>
SafeDelete(PointerType* pointer)
{
if (!pointer)
{
DeleteNull();
}
else
{
delete pointer;
}
}
This template solution misses one of our requirements and mainly to set the pointer to NULL. To meet this case we would have to pass a reference to the pointer.
template<typename PointerType>
SafeDelete(PointerType*& pointer)
{
if (!pointer)
{
DeleteNull();
}
else
{
delete pointer;
pointer = NULL;
}
}
STL compatibility – SafeDelete that works with the standard library
The code above is one good solution to the SafeDelete problem, but won’t work for STL algorithms. If you used a STL container to store dynamically allocated pointers then you probably know that it won’t delete your pointers upon “clear()” operation. To enable such behavior we have to write a functional object – functor that can be used with STL algorithms.
The C++ Standard:
1. The for_each STL algorithm is a non-modifying algorithm [4]
2. The transform is mutable algorithm. [4]
As many other programmers first I was deceived to interpret this “non-modifying” term in a wrong direction and I was tempted to use the “transform” algorithm. Here is what Angelica Langer (a known C++ expert) responded to my interpretation:
“I am struggling with the use of non-modifying and mutable here. I know that you use the qualifications that the standard uses, but transform is not really a mutating algorithm in the sense that it modifies element in the input sequence. sort() and remove_if() are examples of mutating algorithms, whereas remove_copy_if() and transform() only modify the output sequence and leave the input sequence unaltered.
for_each() on the other hand is a potentially mutating algorithm because it permits that the functor modifies elements in the input sequence, which is prohibited for the function objects used with transform() and
all the other algorithms that I mentioned above.”
Concerning this if we want to use a SafeDelete functor with an STL algorithm the for_each will be the most suitable one.
struct SafeDeletor
{
// The template member function will be resolved by the compiler
// in compile time
template<typename PointerType>
void operator()(PointerType*& pointer) const
{
if (!pointer)
{
//DeleteNull();
}
else
{
delete pointer;
pointer = NULL;
}
}
};
And here is the code example for usage of this functional object:
vector<int*> vp;
vp.push_back(some_int_pointer);
for_each(vp.begin(), vp.end(), vp.begin(), SafeDeletor());
So, we have our STL version. If we want to use the same code for deleting single (not in container) objects we have to create an object of type SafeDeletor and use it like this:
SafeDeletor SafeDelete;
void main()
{
int *p = new int(1);
SafeDelete(p);
}
SmartPointers – or how to avoid writing SafeDelete at all
Whenever there is a dynamically allocated piece of memory there is a question of ownership. Some times this ownership is well defined sometimes it is not so obvious. A lot of pointer mistakes come from wrong ownership assumption. Pointers escaping from functions, pointers passed to functions, pointers stored in more than one container…
Smart pointers are classes that “own” these dynamically allocated pointers. They usually implement some reference counting scheme and “delete” the pointer when the last reference goes out of scope. This automatic behavior eliminates the half part
Page : 1 Next >>