Developing interfaces that are easy to use correctly and hard to use incorrectly requires that you consider the kinds of mistakes that clients might make. For example, suppose you’re designing the constructor for a class representing dates in time:
class Date { public: Date(int month, int day, int year); // ... };
The way to prevent likely client errors is:
class Month { public: static Month Jan() { return Month(1); } // functions returning all valid Month values static Month Feb() { return Month(2); } // see below for why these are functions, not objects // ... static Month Dec() { return Month(12); } // ... // other member functions private: explicit Month(int m); // prevent creation of new Month values month-specific data // ... }; Date d(Month::Mar(), Day(30), Year(1995));
Another way to prevent likely client errors is to restrict what can be done with a type. A common way to impose restrictions is to add const.
Another general guideline for making types easy to use correctly and hard to use incorrectly:
unless there’s a good reason not to, have your types behave consistently with the built-in types.
Any interface that requires that clients remember to do something is prone to incorrect use, because clients can forget to do it. For example, Item 13 introduces a factory function that returns pointers to dynamically allocated objects in an Investment hierarchy:
Investment* createInvestment(); // from Item 13; parameters omitted for simplicity
But what if clients forget to use the smart pointer? In many cases, a better interface decision would be to preempt the problem by having the factory function return a smart pointer in the first place:
std::tr1::shared_ptr<Investment> createInvestment();
Suppose clients who get an Investment* pointer from createInvestment are expected to pass that pointer to a function called getRidOfInvestment instead of using delete on it. Such an interface would open the door to a new kind of client error, one where clients use the wrong resource-destruction mechanism (i.e., delete instead of getRidOfInvestment). The implementer of createInvestment can forestall such problems by returning a tr1::shared_ptr with getRidOfInvestment bound to it as its deleter.
This means that the code for implementing createInvestment to return a tr1::shared_ptr with getRidOfInvestment as its deleter would look something like this:
std::tr1::shared_ptr<Investment> createInvestment() { std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment); // retVal = ... ; // make retVal point to the correct object return retVal; }
An especially nice feature of tr1::shared_ptr is that it automatically uses its per-pointer deleter to eliminate another potential client error, the "cross-DLL problem." This problem crops up when an object is created using new in one dynamically linked library (DLL) but is deleted in a different DLL. On many platforms, such cross-DLL new/delete pairs lead to runtime errors. tr1::shared_ptr avoids the problem, because its default deleter uses delete from the same DLL where the tr1::shared_ptr is created.
For example, that if Stock is a class derived from Investment and createInvestment is implemented like this:
std::tr1::shared_ptr<Investment> createInvestment() { return std::tr1::shared_ptr<Investment>(new Stock); }
The returned tr1::shared_ptr can be passed among DLLs without concern for the cross-DLL problem. The tr1::shared_ptrs pointing to the Stock keep track of which DLL’s delete should be used when the reference count for the Stock becomes zero.
tr1::shared_ptr is such an easy way to eliminate some client errors, it's worth an overview of the cost of using it. The most common implementation of tr1::shared_ptr comes from Boost (see Item 55). Boost’s shared_ptr is twice the size of a raw pointer, uses dynamically allocated memory for bookkeeping and deleter-specific data, uses a virtual function call when invoking its deleter, and incurs thread synchronization overhead when modifying the reference count in an application it believes is multithreaded. (You can disable multithreading support by defining a preprocessor symbol.) In short, it’s bigger than a raw pointer, slower than a raw pointer, and uses auxiliary dynamic memory. In many applications, these additional runtime costs will be unnoticeable, but the reduction in client errors will be apparent to everyone.
Things to Remember