- 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
- Arrays
- Smart Pointers and Multithreading
- Putting It All Together
- Summary
7.7 Implicit Conversion to Raw Pointer Types
Consider this code:
void Fun(Something* p); ... SmartPtr<Something> sp(new Something); Fun(sp); // OK or error?
Should this code compile or not? Following the “maximum compatibility” line of thought, the answer is yes.
Technically, it is very simple to render the previous code compilable by introducing a user-defined conversion:
template <class T> class SmartPtr { public: operator T*() // user-defined conversion to T* { return pointee_; } ... };
However, this is not the end of the story.
User-defined conversions in C++ have an interesting history. Back in the 1980s, when user-defined conversions were introduced, most programmers considered them a great invention. User-defined conversions promised a more unified type system, expressive semantics, and the ability to define new types that were indistinguishable from built-in ones. With time, however, user-defined conversions revealed themselves as awkward and potentially dangerous. They might become dangerous especially when they expose handles to internal data (Meyers 1998a, Item 29), which is precisely the case with the operator T* in the previous code. That's why you should think carefully before allowing automatic conversions for the smart pointers you design.
One potential danger comes inherently from giving the user unattended access to the raw pointer that the smart pointer wraps. Passing the raw pointer around defeats the inner workings of the smart pointer. Once unleashed from the confines of its wrapper, the raw pointer can easily become a threat to program sanity again, just as it was before smart pointers were introduced.
Another danger is that user-defined conversions pop up unexpectedly, even when you don't need them. Consider the following code:
SmartPtr<Something> sp; ... // A gross semantic error // However, it goes undetected at compile time delete sp;
The compiler matches operator delete with the user-defined conversion to T*. At runtime, operator T* is called, and delete is applied to its result. This is certainly not what you want to do to a smart pointer, because it is supposed to manage ownership itself. An extra unwitting delete call throws out the window all the careful ownership management that the smart pointer performs under the covers.
There are quite a few ways to prevent the delete call from compiling. Some of them are very ingenious (Meyers 1996). One that's very effective and easy to implement is to make the call to delete intentionally ambiguous. You can achieve this by providing two automatic conversions to types that are susceptible to a call to delete. One type is T* itself, and the other can be void*.
template <class T> class SmartPtr { public: operator T*() // User-defined conversion to T* { return pointee_; } operator void*() // Added conversion to void* { return pointee_; } ... };
A call to delete against such a smart pointer object is ambiguous. The compiler cannot decide which conversion to apply, and the trick above exploits this indecision to good advantage.
Don't forget that disabling the delete operator was only a part of the issue. Whether to provide an automatic conversion to a raw pointer remains an important decision in implementing a smart pointer. It's too dangerous just to let it in, yet too convenient to rule it out. The final SmartPtr implementation will give you a choice about that.
However, forbidding implicit conversion does not necessarily eliminate all access to the raw pointer; it is often necessary to gain such access. Therefore, all smart pointers do provide explicit access to their wrapped pointer via a call to a function:
void Fun(Something* p); ... SmartPtr<Something> sp; Fun(GetImpl(sp)); // OK, explicit conversion always allowed
It's not a matter of whether you can get to the wrapped pointer; it's how easy it is. This may seem like a minor difference, but it's actually very important. An implicit conversion happens without the programmer or the maintainer noticing or even knowing it. An explicit conversion—as is the call to GetImpl—passes through the mind, the understanding, and the fingers of the programmer and remains written in the code for everybody to see it.
Implicit conversion from the smart pointer type to the raw pointer type is desirable, but sometimes dangerous. SmartPtr provides this implicit conversion as a choice. The default is on the safe side—no implicit conversions. Explicit access is always available through the GetImpl function.