Item 45: Use member function templates to accept "all compatible types."Smart pointers are objects that act much like pointers but add functionality pointers don't provide. For example, Item 13 explains how the standard auto_ptr and tr1::shared_ptr can be used to automatically delete heap-based resources at the right time. Iterators into STL containers are almost always smart pointers; certainly you couldn't expect to move a built-in pointer from one node in a linked list to the next by using "++," yet that works for list::iterators. One of the things that real pointers do well is support implicit conversions. Derived class pointers implicitly convert into base class pointers, pointers to non-const objects convert into pointers to const objects, etc. For example, consider some conversions that can occur in a three-level hierarchy: class Top { ... }; class Middle: public Top { ... }; class Bottom: public Middle { ... }; Top *pt1 = new Middle; // convert Middle* Top* Top *pt2 = new Bottom; // convert Bottom* Top* const Top *pct2 = pt1; // convert Top* const Top* Emulating such conversions in user-defined smart pointer classes is tricky. We'd need the following code to compile: template<typename T> class SmartPtr { public: // smart pointers are typically explicit SmartPtr(T *realPtr); // initialized by built-in pointers ... }; SmartPtr<Top> pt1 = // convert SmartPtr<Middle> SmartPtr<Middle>(new Middle); // SmartPtr<Top> SmartPtr<Top> pt2 = // convert SmartPtr<Bottom> SmartPtr<Bottom>(new Bottom); // SmartPtr<Top> SmartPtr<const Top> pct2 = pt1; // convert SmartPtr<Top> // SmartPtr<const Top> There is no inherent relationship among different instantiations of the same template, so compilers view SmartPtr<Middle> and SmartPtr<Top> as completely different classes, no more closely related than, say, vector<float> and Widget. To get the conversions among SmartPtr classes that we want, we have to program them explicitly. In the smart pointer sample code above, each statement creates a new smart pointer object, so for now we'll focus on how to write smart pointer constructors that behave the way we want. A key observation is that there is no way to write out all the constructors we need. In the hierarchy above, we can construct a SmartPtr<Top> from a SmartPtr<Middle> or a SmartPtr<Bottom>, but if the hierarchy is extended in the future, SmartPtr<Top> objects will have to be constructible from other smart pointer types. For example, if we later add class BelowBottom: public Bottom { ... }; we'll need to support the creation of SmartPtr<Top> objects from SmartPtr<BelowBottom> objects, and we certainly won't want to have to modify the SmartPtr template to do it. In principle, the number of constructors we need is unlimited. Since a template can be instantiated to generate an unlimited number of functions, it seems that we don't need a constructor function for SmartPtr, we need a constructor template. Such templates are examples of member function templates (often just known as member templates) templates that generate member functions of a class: template<typename T> class SmartPtr { public: template<typename U> // member template SmartPtr(const SmartPtr<U>& other); // for a "generalized ... // copy constructor" }; This says that for every type T and every type U, a SmartPtr<T> can be created from a SmartPtr<U>, because SmartPtr<T> has a constructor that takes a SmartPtr<U> parameter. Constructors like this ones that create one object from another object whose type is a different instantiation of the same template (e.g., create a SmartPtr<T> from a SmartPtr<U>) are sometimes known as generalized copy constructors. The generalized copy constructor above is not declared explicit. That's deliberate. Type conversions among built-in pointer types (e.g., from derived to base class pointers) are implicit and require no cast, so it's reasonable for smart pointers to emulate that behavior. Omitting explicit on the templatized constructor does just that. As declared, the generalized copy constructor for SmartPtr offers more than we want. Yes, we want to be able to create a SmartPtr<Top> from a SmartPtr<Bottom>, but we don't want to be able to create a SmartPtr<Bottom> from a SmartPtr<Top>, as that's contrary to the meaning of public inheritance (see Item 32). We also don't want to be able to create a SmartPtr<int> from a SmartPtr<double>, because there is no corresponding implicit conversion from int* to double*. Somehow, we have to cull the herd of member functions that this member template will generate. Assuming that SmartPtr follows the lead of auto_ptr and TR1::shared_ptr by offering a get member function that returns a copy of the built-in pointer held by the smart pointer object (see Item 15), we can use the implementation of the constructor template to restrict the conversions to those we want: template<typename T> class SmartPtr { public: template<typename U> SmartPtr(const SmartPtr<U>& other) // initialize this held ptr : heldPtr(other.get()) { ... } // with other's held ptr T* get() const { return heldPtr; } ... private: // built-in pointer held T *heldPtr; // by the SmartPtr }; We use the member initialization list to initialize SmartPtr<T>'s data member of type T* with the pointer of type U* held by the SmartPtr<U>. This will compile only if there is an implicit conversion from a U* pointer to a T* pointer, and that's precisely what we want. The net effect is that SmartPtr<T> now has a generalized copy constructor that will compile only if passed a parameter of a compatible type. The utility of member function templates isn't limited to constructors. Another common role for them is in support for assignment. For example, TR1's shared_ptr (again, see Item 13) supports construction from all compatible built-in pointers, tr1::shared_ptrs, auto_ptrs, and tr1::weak_ptrs (see Item 54), as well as assignment from all of those except tr1::weak_ptrs. Here's an excerpt from TR1's specification for TR1::shared_ptr, including its penchant for using class instead of typename when declaring template parameters. (As Item 42 explains, they mean exactly the same thing in this context.) template<class T> class shared_ptr { public: template<class Y> // construct from explicit shared_ptr(Y * p); // any compatible template<class Y> // built-in pointer, shared_ptr(shared_ptr<Y> const& r); // shared_ptr, template<class Y> // weak_ptr, or explicit shared_ptr(weak_ptr<Y> const& r); // auto_ptr template<class Y> explicit shared_ptr(auto_ptr<Y>& r); template<class Y> // assign from shared_ptr& operator=(shared_ptr<Y> const& r); // any compatible template<class Y> // shared_ptr or shared_ptr& operator=(auto_ptr<Y>& r); // auto_ptr ... }; All these constructors are explicit, except the generalized copy constructor. That means that implicit conversion from one type of shared_ptr to another is allowed, but implicit conversion from a built-in pointer or other smart pointer type is not permitted. (Explicit conversion e.g., via a cast is okay.) Also interesting is how the auto_ptrs passed to TR1::shared_ptr constructors and assignment operators aren't declared const, in contrast to how the TR1::shared_ptrs and tr1::weak_ptrs are passed. That's a consequence of the fact that auto_ptrs stand alone in being modified when they're copied (see Item 13). Member function templates are wonderful things, but they don't alter the basic rules of the language. Item 5 explains that two of the four member functions that compilers may generate are the copy constructor and the copy assignment operator. tr1::shared_ptr declares a generalized copy constructor, and it's clear that when the types T and Y are the same, the generalized copy constructor could be instantiated to create the "normal" copy constructor. So will compilers generate a copy constructor for TR1::shared_ptr, or will they instantiate the generalized copy constructor template when one TR1::shared_ptr object is constructed from another tr1::shared_ptr object of the same type? As I said, member templates don't change the rules of the language, and the rules state that if a copy constructor is needed and you don't declare one, one will be generated for you automatically. Declaring a generalized copy constructor (a member template) in a class doesn't keep compilers from generating their own copy constructor (a non-template), so if you want to control all aspects of copy construction, you must declare both a generalized copy constructor as well as the "normal" copy constructor. The same applies to assignment. Here's an excerpt from tr1::shared_ptr's definition that exemplifies this: template<class T> class shared_ptr { public: shared_ptr(shared_ptr const& r); // copy constructor template<class Y> // generalized shared_ptr(shared_ptr<Y> const& r); // copy constructor shared_ptr& operator=(shared_ptr const& r); // copy assignment template<class Y> // generalized shared_ptr& operator=(shared_ptr<Y> const& r); // copy assignment ... }; Things to Remember
|