1.11 Compatible and Incompatible Policies
Suppose you create two instantiations of SmartPtr: FastWidgetPtr, a pointer with out checking, and SafeWidgetPtr, a pointer with checking before dereference. An interesting question is, Should you be able to assign FastWidgetPtr objects to SafeWidgetPtr objects? Should you be able to assign them the other way around? If you want to allow such conversions, how can you implement that?
Starting from the reasoning that SafeWidgetPtr is more restrictive than FastWidgetPtr, it is natural to accept the conversion from FastWidgetPtr to SafeWidgetPtr. This is because C++ already supports implicit conversions that increase restrictions—namely, from non-const to const types.
On the other hand, freely converting SafeWidgetPtr objects to FastWidgetPtr objects is dangerous. This is because in an application, the majority of code would use SafeWidgetPtr and only a small, speed-critical core would use FastWidgetPtr. Allowing only explicit, controlled conversions to FastWidgetPtr would help keep FastWidgetPtr's usage to a minimum.
The best, most scalable way to implement conversions between policies is to initialize and copy SmartPtr objects policy by policy, as shown below. (Let's simplify the code by getting back to only one policy—the Checking policy.)
template
<
class T,
template <class> class CheckingPolicy
>
class SmartPtr : public CheckingPolicy<T>
{
...
template
<
class T1,
template <class> class CP1,
>
SmartPtr(const SmartPtr<T1, CP1>& other)
: pointee_(other.pointee_), CheckingPolicy<T>(other)
{ ... }
};
SmartPtr implements a templated copy constructor, which accepts any other instantiation of SmartPtr. The code in bold initializes the components of SmartPtr with the components of the other SmartPtr<T1, CP1> received as arguments.
Here's how it works. (Follow the constructor code.) Assume you have a class ExtendedWidget, derived from Widget. If you initialize a SmartPtr<Widget, NoChecking> with a SmartPtr<ExtendedWidget, NoChecking>, the compiler attempts to initialize a Widget* with an ExtendedWiget* (which works), and a NoChecking with a SmartPtrExtended <Widget, NoChecking>. This might look suspicious, but don't forget that SmartPtr derives from its policy, so in essence the compiler will easily figure out that you initialize a NoChecking with a NoChecking. The whole initialization works.
Now for the interesting part. Say you initialize a SmartPtr<Widget, EnforceNotNull> with a SmartPtr<ExtendedWidget, NoChecking>. The ExtendedWidget* to Widget* conversion works just as before. Then the compiler tries to match SmartPtr<ExtendedWidget, NoChecking> to EnforceNotNull's constructors.
If EnforceNotNull implements a constructor that accepts a NoChecking object, then the compiler matches that constructor. If NoChecking implements a conversion operator to EnforceNotNull, that conversion is invoked. In any other case, the code fails to compile.
As you can see, you have two-sided flexibility in implementing conversions between policies. You can implement a conversion constructor on the left-hand side, or you can implement a conversion operator on the right-hand side.
The assignment operator looks like an equally tricky problem, but fortunately, Sutter (2000) describes a very nifty technique that allows you to implement the assignment operator in terms of the copy constructor. (It's so nifty, you have to read about it. You can see the technique at work in Loki's SmartPtr implementation.)
Although conversions from NoChecking to EnforceNotNull and even vice versa are quite sensible, some conversions don't make any sense at all. Imagine converting a reference-counted pointer to a pointer that supports another ownership strategy, such as destructive copy (à la std::auto_ptr). Such a conversion is semantically wrong. The definition of reference counting is that all pointers to the same object are known and tracked by a unique counter. As soon as you try to confine a pointer to another ownership policy, you break the invariant that makes reference counting work.
In conclusion, conversions that change the ownership policy should not be allowed implicitly and should be treated with maximum care. At best, you can change the ownership policy of a reference-counted pointer by explicitly calling a function. That function succeeds if and only if the reference count of the source pointer is 1.
|