- Smart Pointers 101
- The Deal
- Storage of Smart Pointers
- Smart Pointer Member Functions
- Ownership-Handling Strategies
- The Address-of Operator
- Implicit Conversion to Raw Pointer Types
- Equality and Inequality
- Ordering Comparisons
- Checking and Error Reporting
- Smart Pointers to const and const Smart Pointers
- Smart Pointers and Multithreading
- Putting It All Together
7.4 Smart Pointer Member Functions
Many existing smart pointer implementations allow operations through member functions, such as Get for accessing the pointee object, Set for changing it, and Release for taking over ownership. This is the obvious and natural way of encapsulating SmartPtr's functionality.
However, experience has proven that member functions are not very suitable for smart pointers. The reason is that the interaction between member function calls for the smart pointer for the pointed-to object can be extremely confusing.
Suppose, for instance, that you have a Printer class with member functions such as Acquire and Release. With Acquire you take ownership of the printer so that no other application prints to it, and with Release you relinquish ownership. As you use a smart pointer to Printer, you may notice a strange syntactical closeness to things that are very far apart semantically.
SmartPtr<Printer> spRes = ...; spRes->Acquire(); // acquire the printer ... print a document ... spRes->Release(); // release the printer spRes.Release(); // release the pointer to the printer
The user of SmartPtr now has access to two totally different worlds: the world of the pointed-to object members and the world of the smart pointer members. A matter of a dot or an arrow thinly separates the two worlds.
On the face of it, C++ does force you routinely to observe certain slight differences in syntax. A Pascal programmer learning C++ might even feel that the slight syntactic difference between & and && is an abomination. Yet C++ programmers don't even blink at it. They are trained by habit to distinguish such syntax matters easily.
However, smart pointer member functions defeat training by habit. Raw pointers don't have member functions, so C++ programmers' eyes are not habituated to detect and distinguish dot calls from arrow calls. The compiler does a good job at that: If you use a dot after a raw pointer, the compiler will yield an error. Therefore, it is easy to imagine, and experience proves, that even seasoned C++ programmers find it extremely disturbing that both sp.Release() and sp->Release() compile flag-free but do very different things. The cure is simple: A smart pointer should not use member functions. SmartPtr uses only nonmember functions. These functions become friends of the smart pointer class.
Overloaded functions can be just as confusing as member functions of smart pointers, but there is an important difference. C++ programmers already use overloaded functions. Overloading is an important part of the C++ language and is used routinely in library and application development. This means that C++ programmers do pay attention to differences in function call syntax—such as Release(*sp) versus Release(sp)—in writing and reviewing code.
The only functions that necessarily remain members of SmartPtr are the constructors, the destructor, operator=, operator->, and unary operator*. All other operations of SmartPtr are provided through named nonmember functions.
For reasons of clarity, SmartPtr does not have any named member functions. The only functions that access the pointee object are GetImpl, GetImplRef, Reset, and Release, which are defined at the namespace level.
template <class T> T* GetImpl(SmartPtr<T>& sp); template <class T> T*& GetImplRef(SmartPtr<T>& sp); template <class T> void Reset(SmartPtr<T>& sp, T* source); template <class T> void Release(SmartPtr<T>& sp, T*& destination);
GetImpl returns the pointer object stored by SmartPtr.
GetImplRef returns a reference to the pointer object stored by SmartPtr. GetImplRef allows you to change the underlying pointer, so it requires extreme care in use.
Reset resets the underlying pointer to another value, releasing the previous one.
Release releases ownership of the smart pointer, giving its user the responsibility of managing the pointee object's lifetime.
The actual declarations of these four functions in Loki are slightly more elaborate. They don't assume that the type of the pointer object stored by SmartPtr is T*. As discussed in Section 7.3, the Storage policy defines the pointer type. Most of the time, it's a straight pointer, except in exotic implementations of Storage, when it might be a handle or an elaborate type.