More Effective C++ | Item 28: Smart pointers Back to Item 27: Requiring or prohibiting heap-based objects Continue to Item 29: Reference counting Item 28: Smart pointers. Smart pointers are objects that are designed to look, act, and feel like built-in pointers, but to offer greater functionality. They have a variety of applications, including resource management (see Items 9, 10, 25, and 31) and the automation of repetitive coding tasks (see Items 17 and 29). When you use smart pointers in place of C++'s built-in pointers (i.e., dumb pointers), you gain control over the following aspects of pointer behavior: Construction and destruction. You determine what happens when a smart pointer is created and destroyed. It is common to give smart pointers a default value of 0 to avoid the headaches associated with uninitialized pointers. Some smart pointers are made responsible for deleting the object they point to when the last smart pointer pointing to the object is destroyed. This can go a long way toward eliminating resource leaks. Copying and assignment. You control what happens when a smart pointer is copied or is involved in an assignment. For some smart pointer types, the desired behavior is to automatically copy or make an assignment to what is pointed to, i.e., to perform a deep copy. For others, only the pointer itself should be copied or assigned. For still others, these operations should not be allowed at all. Regardless of what behavior you consider "right," the use of smart pointers lets you call the shots. Dereferencing. What should happen when a client refers to the object pointed to by a smart pointer? You get to decide. You could, for example, use smart pointers to help implement the lazy fetching strategy outlined in Item 17. Smart pointers are generated from templates because, like built-in pointers, they must be strongly typed; the template parameter specifies the type of object pointed to. Most smart pointer templates look something like this: template // template for smart class SmartPtr { // pointer objects public: SmartPtr(T* realPtr = 0); // create a smart ptr to an // obj given a dumb ptr to // it; uninitialized ptrs // default to 0 (null) SmartPtr(const SmartPtr& rhs); // copy a smart ptr ~SmartPtr(); // destroy a smart ptr // make an assignment to a smart ptr SmartPtr& operator=(const SmartPtr& rhs); T* operator->() const; // dereference a smart ptr // to get at a member of // what it points to T& operator*() const; // dereference a smart ptr private: T *pointee; // what the smart ptr }; // points to The copy constructor and assignment operator are both shown public here. For smart pointer classes where copying and assignment are not allowed, they would typically be declared private (see Item E27). The two dereferencing operators are declared const, because dereferencing a pointer doesn't modify it (though it may lead to modification of what the pointer points to). Finally, each smart pointer-to-T object is implemented by containing a dumb pointer-to-T within it. It is this dumb pointer that does the actual pointing. Before going into the details of smart pointer implementation, it's worth seeing how clients might use smart pointers. Consider a distributed system in which some objects are local and some are remote. Access to local objects is generally simpler and faster than access to remote objects, because remote access may require remote procedure calls or some other way of communicating with a distant machine. For clients writing application code, the need to handle local and remote objects differently is a nuisance. It is more convenient to have all objects appear to be located in the same place. Smart pointers allow a library to offer this illusion: template // template for smart ptrs class DBPtr { // to objects in a public: // distributed DB DBPtr(T *realPtr = 0); // create a smart ptr to a // DB object given a local // dumb pointer to it DBPtr(DataBaseID id); // create a smart ptr to a // DB object given its // unique DB identifier ... // other smart ptr }; // functions as above class Tuple { // class for database public: // tuples ... void displayEditDialog(); // present a graphical // dialog box allowing a // user to edit the tuple bool isValid() const; // return whether *this }; // passes validity check // class template for making log entries whenever a T // object is modified; see below for details template class LogEntry { public: LogEntry(const T& objectToBeModified); ~LogEntry(); }; void editTuple(DBPtr& pt) { LogEntry entry(*pt); // make log entry for this // editing operation; see // below for details // repeatedly display edit dialog until valid values // are provided do { pt->displayEditDialog(); } while (pt->isValid() == false); } The tuple to be edited inside editTuple may be physically located on a remote machine, but the programmer writing editTuple need not be concerned with such matters; the smart pointer class hides that aspect of the system. As far as the programmer is concerned, all tuples are accessed through objects that, except for how they're declared, act just like run-of-the-mill built-in pointers. Notice the use of a LogEntry object in editTuple. A more conventional design would have been to surround the call to displayEditDialog with calls to begin and end the log entry. In the approach shown here, the LogEntry's constructor begins the log entry and its destructor ends the log entry. As Item 9 explains, using an object to begin and end logging is more robust in the face of exceptions than explicitly calling functions, so you should accustom yourself to using classes like LogEntry. Besides, it's easier to create a single LogEntry object than to add separate calls to start and stop an entry. As you can see, using a smart pointer isn't much different from using the dumb pointer it replaces. That's testimony to the effectiveness of encapsulation. Clients of smart pointers are supposed to be able to treat them as dumb pointers. As we shall see, sometimes the substitution is more transparent than others. Construction, Assignment, and Destruction of Smart Pointers Construction of a smart pointer is usually straightforward: locate an object to point to (typically by using the smart pointer's constructor arguments), then make the smart pointer's internal dumb pointer point there. If no object can be located, set the internal pointer to 0 or signal an error (possibly by throwing an exception). Implementing a smart pointer's copy constructor, assignment operator(s) and destructor is complicated somewhat by the issue of ownership. If a smart pointer owns the object it points to, it is responsible for deleting that object when it (the smart pointer) is destroyed. This assumes the object pointed to by the smart pointer is dynamically allocated. Such an assumption is common when working with smart pointers. (For ideas on how to make sure the assumption is true, see Item 27.) Consider the auto_ptr template from the standard C++ library. As Item 9 explains, an auto_ptr object is a smart pointer that points to a heap-based object until it (the auto_ptr) is destroyed. When that happens, the auto_ptr's destructor deletes the pointed-to object. The auto_ptr template might be implemented like this: template class auto_ptr { public: auto_ptr(T *ptr = 0): pointee(ptr) {} ~auto_ptr() { delete pointee; } ... private: T *pointee; }; This works fine provided only one auto_ptr owns an object. But what should happen when an auto_ptr is copied or assigned? auto_ptr ptn1(new TreeNode); auto_ptr ptn2 = ptn1; // call to copy ctor; // what should happen? auto_ptr ptn3; ptn3 = ptn2; // call to operator=; // what should happen? If we just copied the internal dumb pointer, we'd end up with two auto_ptrs pointing to the same object. This would lead to grief, because each auto_ptr would delete what it pointed to when the auto_ptr was destroyed. That would mean we'd delete an object more than once. The results of such double-deletes are undefined (and are frequently disastrous). An alternative would be to create a new copy of what was pointed to by calling new. That would guarantee we didn't have too many auto_ptrs pointing to a single object, but it might engender an unacceptable performance hit for the creation (and later destruction) of the new object. Furthermore, we wouldn't necessarily know what type of object to create, because an auto_ptr object need not point to an object of type T; it might point to an object of a type derived from T. Virtual constructors (see Item 25) can help solve this problem, but it seems inappropriate to require their use in a general-purpose class like auto_ptr. The problems would vanish if auto_ptr prohibited copying and assignment, but a more flexible solution was adopted for the auto_ptr classes: object ownership is transferred when an auto_ptr is copied or assigned: template class auto_ptr { public: ... auto_ptr(auto_ptr& rhs); // copy constructor auto_ptr& // assignment operator=(auto_ptr& rhs); // operator ... }; template auto_ptr::auto_ptr(auto_ptr& rhs) { pointee = rhs.pointee; // transfer ownership of // *pointee to *this rhs.pointee = 0; // rhs no longer owns } // anything template auto_ptr& auto_ptr::operator=(auto_ptr& rhs) { if (this == &rhs) // do nothing if this return *this; // object is being assigned // to itself delete pointee; // delete currently owned // object pointee = rhs.pointee; // transfer ownership of rhs.pointee = 0; // *pointee from rhs to *this return *this; } Notice that the assignment operator must delete the object it owns before assuming ownership of a new object. If it failed to do this, the object would never be deleted. Remember, nobody but the auto_ptr object owns the object the auto_ptr points to. Because object ownership is transferred when auto_ptr's copy constructor is called, passing auto_ptrs by value is often a very bad idea. Here's why: // this function will often lead to disaster void printTreeNode(ostream& s, auto_ptr p) { s << *p; } int main() { auto_ptr ptn(new TreeNode); ... printTreeNode(cout, ptn); // pass auto_ptr by value ... } When printTreeNode's parameter p is initialized (by calling auto_ptr's copy constructor), ownership of the object pointed to by ptn is transferred to p. When printTreeNode finishes executing, p goes out of scope and its destructor deletes what it points to (which is what ptn used to point to). ptn, however, no longer points to anything (its underlying dumb pointer is null), so just about any attempt to use it after the call to printTreeNode will yield undefined behavior. Passing auto_ptrs by value, then, is something to be done only if you're sure you want to transfer ownership of an object to a (transient) function parameter. Only rarely will you want to do this. This doesn't mean you can't pass auto_ptrs as parameters, it just means that pass-by-value is not the way to do it. Pass-by-reference-to-const is: // this function behaves much more intuitively void printTreeNode(ostream& s, const auto_ptr& p) { s << *p; } In this function, p is a reference, not an object, so no constructor is called to initialize p. When ptn is passed to this version of printTreeNode, it retains ownership of the object it points to, and ptn can safely be used after the call to printTreeNode. Thus, passing auto_ptrs by reference-to-const avoids the hazards arising from pass-by-value. (For other reasons to prefer pass-by-reference to pass-by-value, check out Item E22.) The notion of transferring ownership from one smart pointer to another during copying and assignment is interesting, but you may have been at least as interested in the unconventional declarations of the copy constructor and assignment operator. These functions normally take const parameters, but above they do not. In fact, the code above changes these parameters during the copy or the assignment. In other words, auto_ptr objects are modified if they are copied or are the source of an assignment! Yes, that's exactly what's happening. Isn't it nice that C++ is flexible enough to let you do this? If the language required that copy constructors and assignment operators take const parameters, you'd probably have to cast away the parameters' constness (see Item E21) or play other games to implement ownership transferral. Instead, you get to say exactly what you want to say: when an object is copied or is the source of an assignment, that object is changed. This may not seem intuitive, but it's simple, direct, and, in this case, accurate. If you find this examination of auto_ptr member functions interesting, you may wish to see a complete implementation. You'll find one on pages 291-294, where you'll also see that the auto_ptr template in the standard C++ library has copy constructors and assignment operators that are more flexible than those described here. In the standard auto_ptr template, those functions are member function templates, not just member functions. (Member function templates are described later in this Item. You can also read about them in Item E25.) A smart pointer's destructor often looks like this: template SmartPtr::~SmartPtr() { if (*this owns *pointee) { delete pointee; } } Sometimes there is no need for the test. An auto_ptr always owns what it points to, for example. At other times the test is a bit more complicated. A smart pointer that employs reference counting (see Item 29) must adjust a reference count before determining whether it has the right to delete what it points to. Of course, some smart pointers are like dumb pointers: they have no effect on the object they point to when they themselves are destroyed. Implementing the Dereferencing Operators Let us now turn our attention to the very heart of smart pointers, the operator* and operator-> functions. The former returns the object pointed to. Conceptually, this is simple: template T& SmartPtr::operator*() const { perform "smart pointer" processing; return *pointee; } First the function does whatever processing is needed to initialize or otherwise make pointee valid. For example, if lazy fetching is being used (see Item 17), the function may have to conjure up a new object for pointee to point to. Once pointee is valid, the operator* function just returns a reference to the pointed-to object. Note that the return type is a reference. It would be disastrous to return an object instead, though compilers will let you do it. Bear in mind that pointee need not point to an object of type T; it may point to an object of a class derived from T. If that is the case and your operator* function returns a T object instead of a reference to the actual derived class object, your function will return an object of the wrong type! (This is the slicing problem. See Item E22 and Item 13.) Virtual functions invoked on the object returned from your star-crossed operator* will not invoke the function corresponding to the dynamic type of the pointed-to object. In essence, your smart pointer will not properly support virtual functions, and how smart is a pointer like that? Besides, returning a reference is more efficient anyway, because there is no need to construct a temporary object (see Item 19). This is one of those happy occasions when correctness and efficiency go hand in hand. If you're the kind who likes to worry, you may wonder what you should do if somebody invokes operator* on a null smart pointer, i.e., one whose embedded dumb pointer is null. Relax. You can do anything you want. The result of dereferencing a null pointer is undefined, so there is no "wrong" behavior. Wanna throw an exception? Go ahead, throw it. Wanna call abort (possibly by having an assert call fail)? Fine, call it. Wanna walk through memory setting every byte to your birth date modulo 256? That's okay, too. It's not nice, but as far as the language is concerned, you are completely unfettered. The story with operator-> is similar to that for operator*, but before examining operator->, let us remind ourselves of the unusual meaning of a call to this function. Consider again the editTuple function that uses a smart pointer-to-Tuple object: void editTuple(DBPtr& pt) { LogEntry entry(*pt); do { pt->displayEditDialog(); } while (pt->isValid() == false); } The statement pt->displayEditDialog(); is interpreted by compilers as: (pt.operator->())->displayEditDialog(); That means that whatever operator-> returns, it must be legal to apply the member-selection operator (->) to it. There are thus only two things operator-> can return: a dumb pointer to an object or another smart pointer object. Most of the time, you'll want to return an ordinary dumb pointer. In those cases, you implement operator-> as follows: template T* SmartPtr::operator->() const { perform "smart pointer" processing; return pointee; } This will work fine. Because this function returns a pointer, virtual function calls via operator-> will behave the way they're supposed to. For many applications, this is all you need to know about smart pointers. The reference-counting code of Item 29, for example, draws on no more functionality than we've discussed here. If you want to push your smart pointers further, however, you must know more about dumb pointer behavior and how smart pointers can and cannot emulate it. If your motto is "Most people stop at the Z but not me!", the material that follows is for you. Testing Smart Pointers for Nullness With the functions we have discussed so far, we can create, destroy, copy, assign, and dereference smart pointers. One of the things we cannot do, however, is find out if a smart pointer is null: SmartPtr ptn; ... if (ptn == 0) ... // error! if (ptn) ... // error! if (!ptn) ... // error! This is a serious limitation. It would be easy to add an isNull member function to our smart pointer classes, but that wouldn't address the problem that smart pointers don't act like dumb pointers when testing for nullness. A different approach is to provide an implicit conversion operator that allows the tests above to compile. The conversion traditionally employed for this purpose is to void*: template class SmartPtr { public: ... operator void*(); // returns 0 if the smart ... // ptr is null, nonzero }; // otherwise SmartPtr ptn; ... if (ptn == 0) ... // now fine if (ptn) ... // also fine if (!ptn) ... // fine This is similar to a conversion provided by the iostream classes, and it explains why it's possible to write code like this: ifstream inputFile("datafile.dat"); if (inputFile) ... // test to see if inputFile // was successfully // opened Like all type conversion functions, this one has the drawback of letting function calls succeed that most programmers would expect to fail (see Item 5). In particular, it allows comparisons of smart pointers of completely different types: SmartPtr pa; SmartPtr po; ... if (pa == po) ... // this compiles! Even if there is no operator== taking a SmartPtr and a SmartPtr, this compiles, because both smart pointers can be implicitly converted into void* pointers, and there is a built-in comparison function for built-in pointers. This kind of behavior makes implicit conversion functions dangerous. (Again, see Item 5, and keep seeing it over and over until you can see it in the dark.) There are variations on the conversion-to-void* motif. Some designers advocate conversion to const void*, others embrace conversion to bool. Neither of these variations eliminates the problem of allowing mixed-type comparisons. There is a middle ground that allows you to offer a reasonable syntactic form for testing for nullness while minimizing the chances of accidentally comparing smart pointers of different types. It is to overload operator! for your smart pointer classes so that operator! returns true if and only if the smart pointer on which it's invoked is null: template class SmartPtr { public: ... bool operator!() const; // returns true if and only ... // if the smart ptr is null }; This lets your clients program like this, SmartPtr ptn; ... if (!ptn) { // fine ... // ptn is null } else { ... // ptn is not null } but not like this: if (ptn == 0) ... // still an error if (ptn) ... // also an error The only risk for mixed-type comparisons is statements such as these: SmartPtr pa; SmartPtr po; ... if (!pa == !po) ... // alas, this compiles Fortunately, programmers don't write code like this very often. Interestingly, iostream library implementations provide an operator! in addition to the implicit conversion to void*, but these two functions typically test for slightly different stream states. (In the C++ library standard (see Item E49 and Item 35), the implicit conversion to void* has been replaced by an implicit conversion to bool, and operator bool always returns the negation of operator!.) Converting Smart Pointers to Dumb Pointers Sometimes you'd like to add smart pointers to an application or library that already uses dumb pointers. For example, your distributed database system may not originally have been distributed, so you may have some old library functions that aren't designed to use smart pointers: class Tuple { ... }; // as before void normalize(Tuple *pt); // put *pt into canonical // form; note use of dumb // pointer Consider what will happen if you try to call normalize with a smart pointer-to-Tuple: DBPtr pt; ... normalize(pt); // error! The call will fail to compile, because there is no way to convert a DBPtr to a Tuple*. You can make it work by doing this, normalize(&*pt); // gross, but legal but I hope you'll agree this is repugnant. The call can be made to succeed by adding to the smart pointer-to-T template an implicit conversion operator to a dumb pointer-to-T: template // as before class DBPtr { public: ... operator T*() { return pointee; } ... }; DBPtr pt; ... normalize(pt); // this now works Addition of this function also eliminates the problem of testing for nullness: if (pt == 0) ... // fine, converts pt to a // Tuple* if (pt) ... // ditto if (!pt) ... // ditto (reprise) However, there is a dark side to such conversion functions. (There almost always is. Have you been seeing Item 5?) They make it easy for clients to program directly with dumb pointers, thus bypassing the smarts your pointer-like objects are designed to provide: void processTuple(DBPtr& pt) { Tuple *rawTuplePtr = pt; // converts DBPtr to // Tuple* use rawTuplePtr to modify the tuple; } Usually, the "smart" behavior provided by a smart pointer is an essential component of your design, so allowing clients to use dumb pointers typically leads to disaster. For example, if DBPtr implements the reference-counting strategy of Item 29, allowing clients to manipulate dumb pointers directly will almost certainly lead to bookkeeping errors that corrupt the reference-counting data structures. Even if you provide an implicit conversion operator to go from a smart pointer to the dumb pointer it's built on, your smart pointer will never be truly interchangeable with the dumb pointer. That's because the conversion from a smart pointer to a dumb pointer is a user-defined conversion, and compilers are forbidden from applying more than one such conversion at a time. For example, suppose you have a class representing all the clients who have accessed a particular tuple: class TupleAccessors { public: TupleAccessors(const Tuple *pt); // pt identifies the ... // tuple whose accessors }; // we care about As usual, TupleAccessors' single-argument constructor also acts as a type-conversion operator from Tuple* to TupleAccessors (see Item 5). Now consider a function for merging the information in two TupleAccessors objects: TupleAccessors merge(const TupleAccessors& ta1, const TupleAccessors& ta2); Because a Tuple* may be implicitly converted to a TupleAccessors, calling merge with two dumb Tuple* pointers is fine: Tuple *pt1, *pt2; ... merge(pt1, pt2); // fine, both pointers are converted // to TupleAccessors objects The corresponding call with smart DBPtr pointers, however, fails to compile: DBPtr pt1, pt2; ... merge(pt1, pt2); // error! No way to convert pt1 and // pt2 to TupleAccessors objects That's because a conversion from DBPtr to TupleAccessors calls for two user-defined conversions (one from DBPtr to Tuple* and one from Tuple* to TupleAccessors), and such sequences of conversions are prohibited by the language. Smart pointer classes that provide an implicit conversion to a dumb pointer open the door to a particularly nasty bug. Consider this code: DBPtr pt = new Tuple; ... delete pt; This should not compile. After all, pt is not a pointer, it's an object, and you can't delete an object. Only pointers can be deleted, right? Right. But remember from Item 5 that compilers use implicit type conversions to make function calls succeed whenever they can, and recall from Item 8 that use of the delete operator leads to calls to a destructor and to operator delete, both of which are functions. Compilers want these function calls to succeed, so in the delete statement above, they implicitly convert pt to a Tuple*, then they delete that. This will almost certainly break your program. If pt owns the object it points to, that object is now deleted twice, once at the point where delete is called, a second time when pt's destructor is invoked. If pt doesn't own the object, somebody else does. That somebody may be the person who deleted pt, in which case all is well. If, however, the owner of the object pointed to by pt is not the person who deleted pt, we can expect the rightful owner to delete that object again later. The first and last of these scenarios leads to an object being deleted twice, and deleting an object more than once yields undefined behavior. This bug is especially pernicious because the whole idea behind smart pointers is to make them look and feel as much like dumb pointers as possible. The closer you get to this ideal, the more likely your clients are to forget they are using smart pointers. If they do, who can blame them if they continue to think that in order to avoid resource leaks, they must call delete if they called new? The bottom line is simple: don't provide implicit conversion operators to dumb pointers unless there is a compelling reason to do so. Smart Pointers and Inheritance-Based Type Conversions Suppose we have a public inheritance hierarchy modeling consumer products for storing music: class MusicProduct { public: MusicProduct(const string& title); virtual void play() const = 0; virtual void displayTitle() const = 0; ... }; class Cassette: public MusicProduct { public: Cassette(const string& title); virtual void play() const; virtual void displayTitle() const; ... }; class CD: public MusicProduct { public: CD(const string& title); virtual void play() const; virtual void displayTitle() const; ... }; Further suppose we have a function that, given a MusicProduct object, displays the title of the product and then plays it: void displayAndPlay(const MusicProduct* pmp, int numTimes) { for (int i = 1; i <= numTimes; ++i) { pmp->displayTitle(); pmp->play(); } } Such a function might be used like this: Cassette *funMusic = new Cassette("Alapalooza"); CD *nightmareMusic = new CD("Disco Hits of the 70s"); displayAndPlay(funMusic, 10); displayAndPlay(nightmareMusic, 0); There are no surprises here, but look what happens if we replace the dumb pointers with their allegedly smart counterparts: void displayAndPlay(const SmartPtr& pmp, int numTimes); SmartPtr funMusic(new Cassette("Alapalooza")); SmartPtr nightmareMusic(new CD("Disco Hits of the 70s")); displayAndPlay(funMusic, 10); // error! displayAndPlay(nightmareMusic, 0); // error! If smart pointers are so brainy, why won't these compile? They won't compile because there is no conversion from a SmartPtr or a SmartPtr to a SmartPtr. As far as compilers are concerned, these are three separate classes they have no relationship to one another. Why should compilers think otherwise? After all, it's not like SmartPtr or SmartPtr inherits from SmartPtr. With no inheritance relationship between these classes, we can hardly expect compilers to run around converting objects of one type to objects of other types. Fortunately, there is a way to get around this limitation, and the idea (if not the practice) is simple: give each smart pointer class an implicit type conversion operator (see Item 5) for each smart pointer class to which it should be implicitly convertible. For example, in the music hierarchy, you'd add an operator SmartPtr to the smart pointer classes for Cassette and CD: class SmartPtr { public: operator SmartPtr() { return SmartPtr(pointee); } ... private: Cassette *pointee; }; class SmartPtr { public: operator SmartPtr() { return SmartPtr(pointee); } ... private: CD *pointee; }; The drawbacks to this approach are twofold. First, you must manually specialize the SmartPtr class instantiations so you can add the necessary implicit type conversion operators, but that pretty much defeats the purpose of templates. Second, you may have to add many such conversion operators, because your pointed-to object may be deep in an inheritance hierarchy, and you must provide a conversion operator for each base class from which that object directly or indirectly inherits. (If you think you can get around this by providing only an implicit type conversion operator for each direct base class, think again. Because compilers are prohibited from employing more than one user-defined type conversion function at a time, they can't convert a smart pointer-to-T to a smart pointer-to-indirect-base-class-of-T unless they can do it in a single step.) It would be quite the time-saver if you could somehow get compilers to write all these implicit type conversion functions for you. Thanks to a recent language extension, you can. The extension in question is the ability to declare (nonvirtual) member function templates (usually just called member templates), and you use it to generate smart pointer conversion functions like this: template // template class for smart class SmartPtr { // pointers-to-T objects public: SmartPtr(T* realPtr = 0); T* operator->() const; T& operator*() const; template // template function for operator SmartPtr() // implicit conversion ops. { return SmartPtr(pointee); } ... }; Now hold on to your headlights, this isn't magic but it's close. It works as follows. (I'll give a specific example in a moment, so don't despair if the remainder of this paragraph reads like so much gobbledygook. After you've seen the example, it'll make more sense, I promise.) Suppose a compiler has a smart pointer-to-T object, and it's faced with the need to convert that object into a smart pointer-to-base-class-of-T. The compiler checks the class definition for SmartPtr to see if the requisite conversion operator is declared, but it is not. (It can't be: no conversion operators are declared in the template above.) The compiler then checks to see if there's a member function template it can instantiate that would let it perform the conversion it's looking for. It finds such a template (the one taking the formal type parameter newType), so it instantiates the template with newType bound to the base class of T that's the target of the conversion. At that point, the only question is whether the code for the instantiated member function will compile. In order for it to compile, it must be legal to pass the (dumb) pointer pointee to the constructor for the smart pointer-to-base-of-T. pointee is of type T, so it is certainly legal to convert it into a pointer to its (public or protected) base classes. Hence, the code for the type conversion operator will compile, and the implicit conversion from smart pointer-to-T to smart pointer-to-base-of-T will succeed. An example will help. Let us return to the music hierarchy of CDs, cassettes, and music products. We saw earlier that the following code wouldn't compile, because there was no way for compilers to convert the smart pointers to CDs or cassettes into smart pointers to music products: void displayAndPlay(const SmartPtr& pmp, int howMany); SmartPtr funMusic(new Cassette("Alapalooza")); SmartPtr nightmareMusic(new CD("Disco Hits of the 70s")); displayAndPlay(funMusic, 10); // used to be an error displayAndPlay(nightmareMusic, 0); // used to be an error With the revised smart pointer class containing the member function template for implicit type conversion operators, this code will succeed. To see why, look at this call: displayAndPlay(funMusic, 10); The object funMusic is of type SmartPtr. The function displayAndPlay expects a SmartPtr object. Compilers detect the type mismatch and seek a way to convert funMusic into a SmartPtr object. They look for a single-argument constructor (see Item 5) in the SmartPtr class that takes a SmartPtr, but they find none. They look for an implicit type conversion operator in the SmartPtr class that yields a SmartPtr class, but that search also fails. They then look for a member function template they can instantiate to yield one of these functions. They discover that the template inside SmartPtr, when instantiated with newType bound to MusicProduct, generates the necessary function. They instantiate the function, yielding the following code: SmartPtr:: operator SmartPtr() { return SmartPtr(pointee); } Will this compile? For all intents and purposes, nothing is happening here except the calling of the SmartPtr constructor with pointee as its argument, so the real question is whether one can construct a SmartPtr object with a Cassette* pointer. The SmartPtr constructor expects a MusicProduct* pointer, but now we're on the familiar ground of conversions between dumb pointer types, and it's clear that Cassette* can be passed in where a MusicProduct* is expected. The construction of the SmartPtr is therefore successful, and the conversion of the SmartPtr to SmartPtr is equally successful. Voil! Implicit conversion of smart pointer types. What could be simpler? Furthermore, what could be more powerful? Don't be misled by this example into assuming that this works only for pointer conversions up an inheritance hierarchy. The method shown succeeds for any legal implicit conversion between pointer types. If you've got a dumb pointer type T1* and another dumb pointer type T2*, you can implicitly convert a smart pointer-to-T1 to a smart pointer-to-T2 if and only if you can implicitly convert a T1* to a T2*. This technique gives you exactly the behavior you want almost. Suppose we augment our MusicProduct hierarchy with a new class, CasSingle, for representing cassette singles. The revised hierarchy looks like this: Now consider this code: template // as above, including member tem- class SmartPtr { ... }; // plate for conversion operators void displayAndPlay(const SmartPtr& pmp, int howMany); void displayAndPlay(const SmartPtr& pc, int howMany); SmartPtr dumbMusic(new CasSingle("Achy Breaky Heart")); displayAndPlay(dumbMusic, 1); // error! In this example, displayAndPlay is overloaded, with one function taking a SmartPtr object and the other taking a SmartPtr object. When we invoke displayAndPlay with a SmartPtr, we expect the SmartPtr function to be chosen, because CasSingle inherits directly from Cassette and only indirectly from MusicProduct. Certainly that's how it would work with dumb pointers. Alas, our smart pointers aren't that smart. They employ member functions as conversion operators, and as far as C++ compilers are concerned, all calls to conversion functions are equally good. As a result, the call to displayAndPlay is ambiguous, because the conversion from SmartPtr to SmartPtr is no better than the conversion to SmartPtr. Implementing smart pointer conversions through member templates has two additional drawbacks. First, support for member templates is rare, so this technique is currently anything but portable. In the future, that will change, but nobody knows just how far in the future that will be. Second, the mechanics of why this works are far from transparent, relying as they do on a detailed understanding of argument-matching rules for function calls, implicit type conversion functions, implicit instantiation of template functions, and the existence of member function templates. Pity the poor programmer who has never seen this trick before and is then asked to maintain or enhance code that relies on it. The technique is clever, that's for sure, but too much cleverness can be a dangerous thing. Let's stop beating around the bush. What we really want to know is how we can make smart pointer classes behave just like dumb pointers for purposes of inheritance-based type conversions. The answer is simple: we can't. As Daniel Edelson has noted, smart pointers are smart, but they're not pointers. The best we can do is to use member templates to generate conversion functions, then use casts (see Item 2) in those cases where ambiguity results. This isn't a perfect state of affairs, but it's pretty good, and having to cast away ambiguity in a few cases is a small price to pay for the sophisticated functionality smart pointers can provide. Smart Pointers and const Recall that for dumb pointers, const can refer to the thing pointed to, to the pointer itself, or both (see Item E21): CD goodCD("Flood"); const CD *p; // p is a non-const pointer // to a const CD object CD * const p = &goodCD; // p is a const pointer to // a non-const CD object; // because p is const, it // must be initialized const CD * const p = &goodCD; // p is a const pointer to // a const CD object Naturally, we'd like to have the same flexibility with smart pointers. Unfortunately, there's only one place to put the const, and there it applies to the pointer, not to the object pointed to: const SmartPtr p = // p is a const smart ptr &goodCD; // to a non-const CD object This seems simple enough to remedy just create a smart pointer to a const CD: SmartPtr p = // p is a non-const smart ptr &goodCD; // to a const CD object Now we can create the four combinations of const and non-const objects and pointers we seek: SmartPtr p; // non-const object, // non-const pointer SmartPtr p; // const object, // non-const pointer const SmartPtr p = &goodCD; // non-const object, // const pointer const SmartPtr p = &goodCD; // const object, // const pointer Alas, this ointment has a fly in it. Using dumb pointers, we can initialize const pointers with non-const pointers and we can initialize pointers to const objects with pointers to non-consts; the rules for assignments are analogous. For example: CD *pCD = new CD("Famous Movie Themes"); const CD * pConstCD = pCD; // fine But look what happens if we try the same thing with smart pointers: SmartPtr pCD = new CD("Famous Movie Themes"); SmartPtr pConstCD = pCD; // fine? SmartPtr and SmartPtr are completely different types. As far as your compilers know, they are unrelated, so they have no reason to believe they are assignment-compatible. In what must be an old story by now, the only way these two types will be considered assignment-compatible is if you've provided a function to convert objects of type SmartPtr to objects of type SmartPtr. If you've got a compiler that supports member templates, you can use the technique shown above for automatically generating the implicit type conversion operators you need. (I remarked earlier that the technique worked anytime the corresponding conversion for dumb pointers would work, and I wasn't kidding. Conversions involving const are no exception.) If you don't have such a compiler, you have to jump through one more hoop. Conversions involving const are a one-way street: it's safe to go from non-const to const, but it's not safe to go from const to non-const. Furthermore, anything you can do with a const pointer you can do with a non-const pointer, but with non-const pointers you can do other things, too (for example, assignment). Similarly, anything you can do with a pointer-to-const is legal for a pointer-to-non-const, but you can do some things (such as assignment) with pointers-to-non-consts that you can't do with pointers-to-consts. These rules sound like the rules for public inheritance (see Item E35). You can convert from a derived class object to a base class object, but not vice versa, and you can do anything to a derived class object you can do to a base class object, but you can typically do additional things to a derived class object, as well. We can take advantage of this similarity when implementing smart pointers by having each smart pointer-to-T class publicly inherit from a corresponding smart pointer-to-const-T class: template // smart pointers to const class SmartPtrToConst { // objects ... // the usual smart pointer // member functions protected: union { const T* constPointee; // for SmartPtrToConst access T* pointee; // for SmartPtr access }; }; template // smart pointers to class SmartPtr: // non-const objects public SmartPtrToConst { ... // no data members }; With this design, the smart pointer-to-non-const-T object needs to contain a dumb pointer-to-non-const-T, and the smart pointer-to-const-T needs to contain a dumb pointer-to-const-T. The naive way to handle this would be to put a dumb pointer-to-const-T in the base class and a dumb pointer-to-non-const-T in the derived class. That would be wasteful, however, because SmartPtr objects would contain two dumb pointers: the one they inherited from SmartPtrToConst and the one in SmartPtr itself. This problem is resolved by employing that old battle axe of the C world, a union, which can be as useful in C++ as it is in C. The union is protected, so both classes have access to it, and it contains both of the necessary dumb pointer types. SmartPtrToConst objects use the constPointee pointer, SmartPtr objects use the pointee pointer. We therefore get the advantages of two different pointers without having to allocate space for more than one. (See Item E10 for another example of this.) Such is the beauty of a union. Of course, the member functions of the two classes must constrain themselves to using only the appropriate pointer, and you'll get no help from compilers in enforcing that constraint. Such is the risk of a union. With this new design, we get the behavior we want: SmartPtr pCD = new CD("Famous Movie Themes"); SmartPtrToConst pConstCD = pCD; // fine Evaluation That wraps up the subject of smart pointers, but before we leave the topic, we should ask this question: are they worth the trouble, especially if your compilers lack support for member function templates? Often they are. The reference-counting code of Item 29, for example, is greatly simplified by using smart pointers. Furthermore, as that example demonstrates, some uses of smart pointers are sufficiently limited in scope that things like testing for nullness, conversion to dumb pointers, inheritance-based conversions, and support for pointers-to-consts are irrelevant. At the same time, smart pointers can be tricky to implement, understand, and maintain. Debugging code using smart pointers is more difficult than debugging code using dumb pointers. Try as you may, you will never succeed in designing a general-purpose smart pointer that can seamlessly replace its dumb pointer counterpart. Smart pointers nevertheless make it possible to achieve effects in your code that would otherwise be difficult to implement. Smart pointers should be used judiciously, but every C++ programmer will find them useful at one time or another. Back to Item 27: Requiring or prohibiting heap-based objects Continue to Item 29: Reference counting