• Tidak ada hasil yang ditemukan

Make interfaces easy to use correctly and hard to use incorrectly

Dalam dokumen Book Praise for Effective C++, Third Edition (Halaman 99-105)

ptg7544714

Designs and Declarations

Software designs — approaches to getting the software to do what you want it to do — typically begin as fairly general ideas, but they eventu- ally become detailed enough to allow for the development of specific interfaces. These interfaces must then be translated into C++ declara- tions. In this chapter, we attack the problem of designing and declar- ing good C++ interfaces. We begin with perhaps the most important guideline about designing interfaces of any kind: that they should be easy to use correctly and hard to use incorrectly. That sets the stage for a number of more specific guidelines addressing a wide range of topics, including correctness, efficiency, encapsulation, maintainabil- ity, extensibility, and conformance to convention.

The material that follows isn’t everything you need to know about good interface design, but it highlights some of the most important considerations, warns about some of the most frequent errors, and provides solutions to problems often encountered by class, function, and template designers.

Item 18: Make interfaces easy to use correctly and

ptg7544714 ents might make. For example, suppose you’re designing the con-

structor for a class representing dates in time:

class Date { public:

Date(int month, int day, int year);

...

};

At first glance, this interface may seem reasonable (at least in the USA), but there are at least two errors that clients might easily make.

First, they might pass parameters in the wrong order:

Date d(30, 3, 1995); // Oops! Should be “3, 30” , not “30, 3”

Second, they might pass an invalid month or day number:

Date d(3, 40, 1995); // Oops! Should be “3, 30” , not “3, 40”

(This last example may look silly, but remember that on a keyboard, 4 is next to 3. Such “off by one” typing errors are not uncommon.) Many client errors can be prevented by the introduction of new types.

Indeed, the type system is your primary ally in preventing undesirable code from compiling. In this case, we can introduce simple wrapper types to distinguish days, months, and years, then use these types in the Date constructor:

struct Day { struct Month { struct Year {

explicit Day(int d) explicit Month(int m) explicit Year(int y)

: val(d) {} : val(m) {} : val(y){}

int val; int val; int val;

}; }; };

class Date { public:

Date(const Month& m, const Day& d, const Year& y);

...

};

Date d(30, 3, 1995); // error! wrong types

Date d(Day(30), Month(3), Year(1995)); // error! wrong types Date d(Month(3), Day(30), Year(1995)); // okay, types are correct Making Day, Month, and Year full-fledged classes with encapsulated data would be better than the simple use of structs above (see Item 22), but even structs suffice to demonstrate that the judicious introduction of new types can work wonders for the prevention of interface usage errors.

ptg7544714 Once the right types are in place, it can sometimes be reasonable to

restrict the values of those types. For example, there are only 12 valid month values, so the Month type should reflect that. One way to do this would be to use an enum to represent the month, but enums are not as type-safe as we might like. For example, enums can be used like ints (see Item 2). A safer solution is to predefine the set of all valid Months:

class Month { public:

static Month Jan() { return Month(1); } // functions returning all valid static Month Feb() { return Month(2); } // Month values; see below for

... // why these are functions, not

static Month Dec() { return Month(12); } // objects

... // 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));

If the idea of using functions instead of objects to represent specific months strikes you as odd, it may be because you have forgotten that reliable initialization of non-local static objects can be problematic.

Item 4 can refresh your memory.

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. For example, Item 3 explains how const-qualifying the return type from operator* can prevent clients from making this error for user- defined types:

if (a * b = c) ... // oops, meant to do a comparison!

In fact, this is just a manifestation of 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. Clients already know how types like int behave, so you should strive to have your types behave the same way whenever reasonable. For example, assignment to a*b isn’t legal if a and b are ints, so unless there’s a good reason to diverge from this behavior, it should be illegal for your types, too. When in doubt, do as the ints do.

The real reason for avoiding gratuitous incompatibilities with the built-in types is to offer interfaces that behave consistently. Few char- acteristics lead to interfaces that are easy to use correctly as much as consistency, and few characteristics lead to aggravating interfaces as

ptg7544714 much as inconsistency. The interfaces to STL containers are largely

(though not perfectly) consistent, and this helps make them fairly easy to use. For example, every STL container has a member function named size that tells how many objects are in the container. Contrast this with Java, where you use the length property for arrays, the length method for Strings, and the size method for Lists; and with .NET, where Arrays have a property named Length, while ArrayLists have a property named Count. Some developers think that integrated development environments (IDEs) render such inconsistencies unimportant, but they are mistaken. Inconsistency imposes mental friction into a devel- oper’s work that no IDE can fully remove.

Any interface that requires that clients remember to do something is prone to incorrect use, because clients can forget to do it. For exam- ple, 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

To avoid resource leaks, the pointers returned from createInvestment must eventually be deleted, but that creates an opportunity for at least two types of client errors: failure to delete a pointer, and deletion of the same pointer more than once.

Item 13 shows how clients can store createInvestment’s return value in a smart pointer like auto_ptr or tr1::shared_ptr, thus turning over to the smart pointer the responsibility for using delete. But what if clients forget to use the smart pointer? In many cases, a better interface deci- sion 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();

This essentially forces clients to store the return value in a tr1::shared_ptr, all but eliminating the possibility of forgetting to delete the underlying Investment object when it’s no longer being used.

In fact, returning a tr1::shared_ptr makes it possible for an interface designer to prevent a host of other client errors regarding resource release, because, as Item 14 explains, tr1::shared_ptr allows a resource- release function — a “deleter” — to be bound to the smart pointer when the smart pointer is created. (auto_ptr has no such capability.) Suppose clients who get an Investment* pointer from createInvestment are expected to pass that pointer to a function called getRidOfInvest- ment 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

ptg7544714 resource-destruction mechanism (i.e., delete instead of getRidOfInvest-

ment). The implementer of createInvestment can forestall such prob- lems by returning a tr1::shared_ptr with getRidOfInvestment bound to it as its deleter.

tr1::shared_ptr offers a constructor taking two arguments: the pointer to be managed and the deleter to be called when the reference count goes to zero. This suggests that the way to create a null tr1::shared_ptr withgetRidOfInvestment as its deleter is this:

std::tr1::shared_ptr<Investment> // attempt to create a null

pInv(0,getRidOfInvestment); // shared_ptr with a custom deleter;

//this won’t compile

Alas, this isn’t valid C++. The tr1::shared_ptr constructor insists on its first parameter being a pointer, and 0 isn’t a pointer, it’s an int. Yes, it’s convertible to a pointer, but that’s not good enough in this case;

tr1::shared_ptr insists on an actual pointer. A cast solves the problem:

std::tr1::shared_ptr<Investment> // create a null shared_ptr with pInv(static_cast<Investment*>(0), // getRidOfInvestment as its

getRidOfInvestment); // deleter; see Item 27 for info on // static_cast

This means that the code for implementing createInvestment to return atr1::shared_ptr with getRidOfInvestment as its deleter would look some- thing like this:

std::tr1::shared_ptr<Investment> createInvestment() {

std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment);

... // make retVal point to the

// correct object return retVal;

}

Of course, if the raw pointer to be managed by retVal could be deter- mined prior to creating retVal, it would be better to pass the raw pointer to retVal’s constructor instead of initializing retVal to null and then making an assignment to it. For details on why, consult Item 26.

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 cre- ated 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

ptg7544714 tr1::shared_ptr is created. This means, 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 con- cern 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 refer- ence count for the Stock becomes zero.

This Item isn’t about tr1::shared_ptr — it’s about making interfaces easy to use correctly and hard to use incorrectly — but tr1::shared_ptr is such an easy way to eliminate some client errors, it’s worth an over- view 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

Good interfaces are easy to use correctly and hard to use incorrectly.

You should strive for these characteristics in all your interfaces.

Ways to facilitate correct use include consistency in interfaces and behavioral compatibility with built-in types.

Ways to prevent errors include creating new types, restricting opera- tions on types, constraining object values, and eliminating client re- source management responsibilities.

tr1::shared_ptr supports custom deleters. This prevents the cross- DLL problem, can be used to automatically unlock mutexes (see Item 14), etc.

ptg7544714

Dalam dokumen Book Praise for Effective C++, Third Edition (Halaman 99-105)

Dokumen terkait