- 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.3 Storage of Smart Pointers
To start, let's ask a fundamental question about smart pointers. Is pointee_'s type necessarily T*? If not, what else could it be? In generic programming, you should always ask yourself questions like these. Each type that's hardcoded in a piece of generic code decreases the genericity of the code. Hardcoded types are to generic code what magic constants are to regular code.
In several situations, it is worthwhile to allow customizing the pointee type. One situation is when you deal with nonstandard pointer modifiers. In the 16-bit Intel 80x86 days, you could qualify pointers with modifiers like __near, __far, and __huge. Other segmented memory architectures use similar modifiers.
Another situation is when you want to layer smart pointers. What if you have a LegacySmartPtr<T> smart pointer implemented by someone else, and you want to enhance it? Would you derive from it? That's a risky decision. It's better to wrap the legacy smart pointer into a smart pointer of your own. This is possible because the inner smart pointer supports pointer syntax. From the outer smart pointer's viewpoint, the pointee type is not T* but LegacySmartPtr<T>.
There are interesting applications of smart pointer layering, mainly because of the mechanics of operator->. When you apply operator-> to a type that's not a built-in pointer, the compiler does an interesting thing. After looking up and applying the user-defined operator-> to that type, it applies operator-> again to the result. The compiler keeps doing this recursively until it reaches a pointer to a built-in type, and only then proceeds with member access. It follows that a smart pointer's operator-> does not have to return a pointer. It can return an object that in turn implements operator->, without changing the use syntax.
This leads to a very interesting idiom: pre-and postfunction calls (Stroustrup 2000). If you return an object of type PointerType by value from operator->, the sequence of execution is as follows:
Constructor of PointerType
PointerType::operator-> called; likely returns a pointer to an object of type PointeeType
Member access for PointeeType—likely a function call
Destructor of PointerType
In a nutshell, you have a nifty way of implementing locked function calls. This idiom has broad uses with multithreading and locked resource access. You can have PointerType's constructor lock the resource, and then you can access the resource; finally, Pointer Type's destructor unlocks the resource.
The generalization doesn't stop here. The syntax-oriented “pointer” part might sometimes pale in comparison with the powerful resource management techniques that are included in smart pointers. It follows that, in rare cases, smart pointers could drop the pointer syntax. An object that does not define operator-> and operator* violates the definition of a smart pointer, but there are objects that do deserve smart pointer–like treatment, although they are not, strictly speaking, smart pointers.
Look at real-world APIs and applications. Many operating systems foster handles as accessors to certain internal resources, such as windows, mutexes, or devices. Handles are intentionally obfuscated pointers; one of their purposes is to prevent their users from manipulating critical operating system resources directly. Most of the time, handles are integral values that are indices in a hidden table of pointers. The table provides the additional level of indirection that protects the inner system from the application programmers. Although they don't provide an operator->, handles resemble pointers in semantics and in the way they are managed.
For such a smart resource, it does not make sense to provide operator-> or operator*. However, you do take advantage of all the resource management techniques that are specific to smart pointers.
To generalize the type universe of smart pointers, we distinguish three potentially distinct types in a smart pointer:
- The storage type. This is the type of pointee_. By “default”—in regular smart pointers—it is a raw pointer.
- The pointer type. This is the type returned by operator->. It can be different from the storage type if you want to return a proxy object instead of just a pointer. (You will find an example of using proxy objects later in this chapter.)
- The reference type. This is the type returned by operator*.
It would be useful if SmartPtr supported this generalization in a flexible way. Thus, the three types mentioned here ought to be abstracted in a policy called Storage.
In conclusion, smart pointers can, and should, generalize their pointee type. To do this, SmartPtr abstracts three types in a Storage policy: the stored type, the pointer type, and the reference type. Not all types necessarily make sense for a given SmartPtr instantiation. Therefore, in rare cases (handles), a policy might disable access to operator-> or operator* or both.