When should I use __setNoDelete with a smart pointer?
The Ice C++98 mapping provides reference-counting smart pointers that help to ensure your code does not leak memory. Once you assign the return value from operator new
to a smart pointer, you no longer have to worry about memory leaks. For example:
class Node { Node next; }
This defines a simple self-referential class that can be used to create a linked list:
{ // Open scope NodePtr p = new Node; p->next = new Node; // ... } // Close scope, no leak here.
The reference counting that is performed by smart pointers takes care of deleting both Node
instances once the variable p
goes out of scope, even if the code leaves the scope in which p
is defined due to an exception.
Smart pointers are instances of the IceUtil::Handle
template. For example, the Slice compiler generates the following type definition for NodePtr
:
typedef IceUtil::Handle<Node> NodePtr;
The Handle
template contains member functions that take care of maintaining the reference count. The template provides overloaded constructors, a copy constructor, a destructor, and overloaded assignment operators to ensure that the pointed-at object's reference count is maintained appropriately. To maintain the reference count, these member functions of the Handle
template expect the pointed-at object to provide two member functions, __incRef
and __decRef
, that increment and decrement the reference count, respectively. The pointed-at class provides these member functions by deriving from the IceUtil::Shared
base class, which also contains __getRef
and __setNoDelete
member functions:
class Shared { public: Shared() : _ref(0), _noDelete(false) {} virtual ~Shared() {} void __incRef(); void __decRef(); void __getRef(); void __setNoDelete(bool b) { _noDelete = b; } private: int _ref; // Exact type varies with platform bool _noDelete; };
The __getRef
member function returns the value of the _ref member
and is useful mainly for debugging. The implementation of __incRef
increments the value of the _ref
member, and __decRef
decrements the value. Ignoring synchronization issues, __decRef
is implemented something like this:
void Shared::__decRef() { if(--_ref == 0 && !_noDelete) { delete this; } }
In other words, when the smart pointer calls __decRef
on the pointed-at instance and the reference count reaches zero (which happens when the last smart pointer for a class instance goes out of scope), the instance self-destructs by calling delete this
.
However, as you can see, the instance self-destructs only if _noDelete
is false
(which it is by default, because the constructor initializes it to false
). You can call __setNoDelete(true)
to prevent this self-destruction and, later, call __setNoDelete(false)
to enable it again. Why would you want to do this? As it turns out, doing this is necessary if, for example, a class in its constructor needs to pass this
to another function:
void someFunction(const Handle<SomeClass>& h) { // ... } // SomeClass derives from IceUtil::Shared SomeClass::SomeClass() { someFunction(this); // Trouble looming here! }
At first glance, this code looks innocuous enough. While SomeClass
is being constructed, it passes this
to someFunction
, which expects a smart pointer. No problem with that: the compiler constructs a temporary smart pointer at the point of call (because Handle
has a single-argument constructor that accepts a pointer to a heap-allocated instance, so the constructor acts a conversion function). However, this code fails badly. The SomeClass
instance is constructed with a statement such as:
SomeClassPtr p = new SomeClass;
The constructor of SomeClass
is called by operator new
and, when the constructor starts executing, the reference count of the instance is zero (because that is what the reference count is initialized to by the Shared
base class of SomeClass
). When the constructor calls someFunction
, the compiler creates a temporary smart pointer, which increments the reference count of the instance and, once someFunction
completes, the compiler dutifully destroys that temporary smart pointer again. But, of course, that drops the reference count back to zero and causes the SomeClass
instance to self-destruct by calling delete this
. The net effect is that the call to new SomeClass
returns a pointer to an already deleted object! Of course, this is likely to cause havoc — a core dump is the most likely outcome, but the program may exhibit various difficult-to-diagnose forms of insanity before that happens.
To get around the problem, you can call __setNoDelete
:
SomeClass::SomeClass() { __setNoDelete(true); someFunction(this); // OK __setNoDelete(false); }
The code disables self-destruction while someFunction
uses its temporary smart pointer by calling __setNoDelete(true)
. By doing this, the reference count of the instance is incremented before someFunction
is called and decremented back to zero when someFunction
completes without causing the object to self-destruct. The constructor then re-enables self-destruction by calling __setNoDelete(false)
before returning, so the statement:
SomeClassPtr p = new SomeClass;
does the usual thing, namely to increment the reference count of the object to 1, despite the fact that a temporary smart pointer existed while the constructor ran.
Note that you may also come across a similar scenario if a derived class constructor throws an exception. This can happen, for example, if you use the listener pattern, for which a base class constructor passes this
to the listener class, and an exception in the derived constructor can lead to double deallocation. You can use the same __setNoDelete
mechanism to temporarily disable self-destruction in this case.
In general, whenever a class constructor passes this
to a function or another class that accepts a smart pointer, you must temporarily disable self-destruction.