Techniques for requiring or prohibiting heap-based objects
Requiring Heap-Based Objects
1. The straightforward way
The brutle way is to declare the constructors and the destructor private, but this is overkill. Either one of them need to be private to ensure objects only be created on the heap. Since there are usually many constructors but only one destructor per class, a more elegant way is to make the destructor private and the constructors public, prividing privileged pseudo-destructor function (which has access to the real destructor) for clients to call, as suggested in MECpp item 26.
Example:
|
|
Clients would program like this:
|
|
2. Inheritance-and-containment friendly
Restricting access to a class’s destructor or constructors prevents the creation of not only non-heap objects, but also both inheritance and containment. To work this out:
- To be friendly for inheritance, we declare
protectedforUPNumber’s destructor. - To be friendly for containment, classes that need objects of type
UPNumbercan be modified to contain pointers toUPNumberobject instead:
|
|
3. Determining Whether an Object is On The Heap
It’s hard to tell whether an object is on the heap. For example, given the class definition above, it’s ligal to define a non-heap NonNegativeUPNumber object, which will not construct its base UPNumber part on the heap. There is no way to detect whether a constructor is being invoked as the base class part of a heap-based object, which means for the following contexts, it is not possible for the UPNumber constructor to detect the difference:
|
|
Sadly, there’s no portable way to determine whether an object is on the heap, and there isn’t even a semi-portable way that works most of the time. Detailed dicussion on this topic could be found at Comments on Item 27 of More Effective C++.
We’ll have to turn to unportable, implementation-dependent system calls if we absolutely have to tell whether an address is on the heap. That being the case, we’d better off trying to redesign the software so we don’t need to determine whether an object is on the heap in the first place.
4. Determine whether it’s safe to delete a pointer
To answer this easier question, all we need to do is to create a collection of addresses that have been returned by operator new. One possible solution is to provide an abstract mixin base class that offers derived classes the ability to determine whether a pointer was allocated from operator new:
|
|
|
|
Note that dynamic_cast is applicable only to pointers to objects that have at least one virtual function, and dynamic_casting a pointer to void* (or const void* or volatile void* or const volatile void*) yields a pointer to the beginning of the memory for the object pointed to by the pointer. Here dynamic_casting this to const void* gives us a pointer to the beginning of the memory for the current object, which is the pointer previously returned by HeapTracked::operator new as long as the memory for the current object was allocated by HeapTracked::operator new in the first place.
To use this basic class, say we want to be able to determine whether a pointer to an Asset object points to a heap-based object, simply modify Asset’s class definition to specify HeapTracked as a base class:
|
|
And we could query Asset* pointers as follows:
|
|
Since built-in types such as int and char can’t inherit from anything, HeapTracked can’t be used with these built-in types. Still, the most common reason for wanting to use a class like HeapTracked is to determine whether it’s okay to delete this, and we don’t want to do that with a built-in type because such types have no this pointer.
Prohibiting Heap-Based Objects
To preventing objects from being allocated on the heap, there are three cases:
- objects that are directly instantiated
- objects instantiated as base class parts of derived class objects
- objects embedded inside other objects
1. Preventing directly instantiation on heap
To prevent clients from directly instantiating objects on the heap: we can declare operator new (and possibly operator new[]) as private:
|
|
|
|
Declaring operator new private often also prevents UPNumber objects from being instantiated as base class parts of heap-based derived class objects, because operator new and operator delete are inherited, so if these functions aren’t declared public in a derived class, that class inherits the private versions declared in its base(s):
|
|
2. Preventing base class parts instantiated on heap
However, if the derived class declares an operator new of its own, which will be called when allocating derived class objects on the heap, it is hard to prevent UPNumber base class parts from winding up there.
3. Preventing base class parts instantiated on heap
Similarly, the fact that UPNumber’s operator new is private has no effect on attempts to allocate objects containing UPNumber objects as members:
|
|
Just as there’s no portable way to determine if an address is on the heap, however, there is no portable way to determine that it is not on the heap, so we can’t throw an exception in the UPNumber constructors if a to-be-tested UPNumber object being constructed is on the heap. We’re out of luck.