Item 18: Making interfaces (Interface) Easy to use correctly and difficult to use incorrectly
By Scott Meyers
Translator: fatalerror99 (itepub's nirvana)
Release: http://blog.csdn.net/fatalerror99/
C ++ is drowned in interfaces (interface. Function interfaces, class interfaces, and template interfaces ). Every interface is a way for customers to interact with your code. If you are dealing with reasonable people, those customers also want to do a good job. They want to use your interfaces (Interface) correctly ). In this case, if they use it incorrectly, it means that at least some of your interfaces are incomplete. Ideally, if an attempt to use an interface (Interface) does not meet the customer's expectation, the Code cannot be compiled. In turn, if the code can be compiled, what it does is what the customer wants.
The interfaces (Interface), which is easy to use and difficult to use, requires you to consider various errors that may be caused by customers. For example, suppose you are designing a constructor (constructor) for a class that represents a date ):
Class Date {
Public:
Date (INT month, int day, int year);
...
};
At first glance, this interface seems reasonable (at least in the United States), but the customer may easily cause two kinds of errors. First, they may pass parameters (parameters) in the wrong order ):
Date D (30,3, 1995); // oops! Shocould be "3, 30", not "30, 3"
Second, they may pass an invalid monthly or daily number:
Date D (3,40, 1995); // oops! Shocould be "3, 30", not "3, 40"
(The following example looks silly, but think about the keyboard, 4 is next to 3. This "off by one" type error is not uncommon .) (The original text in the above example is incorrect. It is changed according to the author's website Errata-Translator's note .)
Many customer errors can be prevented by introducing new types. Indeed, type system is the main supporter of your defense against unreasonable code compilation. In the current situation, we can introduce a simple wrapper types (packaging type) to differentiate the day, month, and year, and use these types for data Constructor (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;
};};};
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
Using day, month, and year to encapsulate data is better than simply using structs (see item 22 ), however, even structs is enough to prove that the wise Introduction of new types can work very well in blocking incorrect use of interfaces.
As long as you place the correct type in the appropriate position, it can usually reasonably limit the values of those types. For example, the month has only 12 valid values, so the month type should reflect this. One way to do this is to use an Enum (enumeration) to represent the month, but enums (enumeration) is not as type-safe as we want. For example, enums (enumeration) can be used as ints (see item 2 ). A safer solution is to pre-determine all valid months collections:
Class month {
Public:
Static month Jan () {return month (1);} // functions returning all valid
Static month FEB () {return month (2);} // month values; see below
... // 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 you are surprised to use functions instead of objects to represent the idea of a specific month, it may be because you forgot non-local static objects (non-local static object) the reliability of initialization is questionable. Item 4 can arouse your memory.
Another way to prevent possible customer errors is to limit what a type can do. A common method to apply restrictions is to add Const. For example, item 3 explains how to make operator *'s return type const-qualifying (qualified as const) prevent customers from making such errors against user-defined types:
If (A * B = C)... // oops, meant to do a comparison!
In fact, this is just a manifestation of another universal policy that makes the type easy to use and difficult to use incorrectly: Unless you have great reasons, otherwise, make your type Behavior consistent with built-in types (built-in type. Customers already know what kind of behavior is like int, so you should try to make your type of behavior equally reasonable at any time. For example, if a and B are ints, it is invalid to assign a value to a * B. So unless there is a great reason to deviate from this behavior, it should be illegal for your type. If you have no idea, you can do it just like ints.
The real reason for avoiding incompatibilities (incompatible with built-in types (built-in types) for no reason is to provide interfaces (interfaces) with consistent behavior ). Few features are more likely to export interfaces (interfaces) that are easier to use than consistency (consistency), and few are more likely to export interfaces (interfaces) than inconsistency (inconsistency) it is more likely to export depressing interfaces (interfaces ). The interfaces (interfaces) of STL containers (STL containers) are largely consistent (although not perfect), which makes them quite easy to use. For example, each STL containers (STL container) has a member function (member function) named size to know the number of objects in the container (container. In contrast, Java, where you use length for the arrays (array)Property(Attribute), use length for stringsMethod(Method), but use size for listsMethod(Method). In. net, arrays has a property named length, while arraylists has a property named count ). Some developers think that integrated development environments (IDES) can compensate these segments for inconsistencies (inconsistencies), but they are wrong. Inconsistency (inconsistency) is a mental burden that cannot be completely eliminated by any IDE.
Any interface (Interface) that requires the customer to remember certain things tends to be used incorrectly, because the customer may forget to do those things. For example, item 13 introduces a factory function, which returns a pointer to the objects (object) in the dynamically assigned investment hierarchy (inheritance system.
Investment * createinvestment (); // from item 13; Parameters omitted
// For simplicity
To avoid resource leaks (resource leakage), the pointer returned from createinvestment must be deleted at the end, but this creates an opportunity for at least two types of customer errors: no pointer is deleted, or delete the same pointer more than once.
Item 13 shows how customers can save the returned values of createinvestment to a smart pointer (smart pointer) similar to auto_ptr or tr1: shared_ptr, in this way, the responsibility for using Delete is assigned to smart pointer ). But what if the customer forgets to use smart pointer? In many cases, a better interface policy will solve the problem in advance by making the factory function first return a smart pointer:
STD: tr1: shared_ptr <investment>Createinvestment ();
This basically forces the customer to store the returned value in a tr1: shared_ptr, almost completely eliminating the possibility of forgetting to delete the underlying investment object when it is no longer used.
In fact, the returned tr1: shared_ptr makes it possible for an interface (Interface) designer to prevent many other customer errors related to resource leakage and release, as described in item 14: when a smart pointer is created, tr1: shared_ptr allows a Resource-release function) -- a "deleter" -- bound to this smart pointer (smart pointer. (Auto_ptr does not have this capability .)
Assume that the customer who obtains an investment * pointer from createinvestment expects to pass this pointer to a function named getridofinvestment instead of using delete for it. Such an interface opens the door for a new customer error. This means that the customer may use the wrong resource-destruction mechanism (resource analysis mechanism) (that is, use Delete instead of getridofinvestment ). Createinvestment returns a tr1: shared_ptr bound to getridofinvestment on its deleter to prevent such problems.
Tr1: shared_ptr provides a constructor (constructor) that gets two arguments (real parameters) (pointers to be managed and deleter to be called when the reference count turns to zero ). The following shows how to create a null tr1: shared_ptr using getridofinvestment as deleter:
STD: tr1: shared_ptr <investment> // attempt to create a null
Pinv (0, getridofinvestment); // Shared_ptr with a custom deleter;
//This won't compile
Sorry, this is not a legal C ++. Tr1: shared_ptr Constructor (constructor) insists that its first parameter is a pointer, and 0 is not a pointer, it is an int. Of course, itConvertible(Can be converted) to a pointer, but that is not good enough in the current situation, tr1: shared_ptr resolutely requires a real pointer. Use cast to solve a problem:
STD: tr1: shared_ptr <investment> // create a null shared_ptr
Pinv (Static_cast <investment *> (0), // Getridofinvestment as its
Getridofinvestment); // deleter; see item 27 for info on
// Static_cast
Based on this, the implementation returns a code that uses getridofinvestment as its deleter's tr1: shared_ptr createinvestment, which looks like this:
STD: tr1: shared_ptr <investment> createinvestment ()
{
STD: tr1: shared_ptr <investment> retval (static_cast <investment *> (0 ),
Getridofinvestment );
Retval =...; // make retval point to
// Correct object
Return retval;
}
Of course, if raw pointer (bare pointer) managed by retval can be determined when retval is created, it is best to pass this raw pointer (bare pointer) to the constructor (constructor) of retval ), instead of initializing retval to null and then assigning it. For details about the method, refer to item 26. (If the original text is incorrect, it is changed based on the author's website Errata-the Translator's note .)
Tr1: a particularly good feature of shared_ptr is that it automatically uses deleter per-pointer (pointer by Pointer) to eliminate another potential customer error-"cross-DLL problem ." This problem occurs when an object is created through new in a dynamically linked library (DLL) and deleted from another DLL. On many platforms, such cross-DLL new/delete pairs may cause runtime errors. Tr1: shared_ptr avoids this problem because its default deleter only applies Delete to the same DLL created by tr1: shared_ptr. This means that, for example, if stock is a class inherited from investment, and createinvestment is implemented as follows,
STD: tr1: shared_ptr <investment> createinvestment ()
{
Return STD: tr1: shared_ptr <investment> (new stock );
}
The returned tr1: shared_ptr can be passed between DLLs without concern about cross-DLL. Tr1: shared_ptrs pointing to this stock will keep the trace of "which DLLs Delete should be used when the reference count of this stock changes to zero.
This item is not about tr1: shared_ptr -- it is about making the interface Easy to use correctly and difficult to use incorrectly -- but tr1: shared_ptr is such a simple method to eliminate some customer errors, it basically returns the cost of using it. The most common tr1: shared_ptr implementation comes from boost (see item 55 ). The shared_ptr size of boost is twice the size of a raw pointer (bare pointer). It uses dynamic memory allocation for data in bookkeeping and deleter-specific (er-specific, when er is called, a virtual function (virtual function) is used for calling. In an application that it deems to be multithreaded (multi-thread), the reference count is changed whenever, thread Synchronization (thread synchronization) overhead. (You can define a Preprocessor symbol to invalidate multithreading support .) In terms of disadvantages, it is larger than a raw pointer (bare pointer), slower than a raw pointer (bare pointer), and uses auxiliary dynamic memory. In many applications, the additional runtime overhead is not significant, but the reduction in customer errors is visible to everyone.
Things to remember
- Good interfaces (interfaces) are easy to use correctly and difficult to use incorrectly. You should work on this feature in all your interfaces (interfaces.
- Methods that are easy to use include keeping interfaces (Interface) and behavioral compatibility consistent with built-in types (built-in type.
- Methods to prevent errors include creating new types, limiting type operations, limiting object values, and eliminating customers' resource management responsibilities.
- Tr1: shared_ptr supports custom deleter. This prevents cross-DLL issues and can be used to automatically unlock mutex (see item 14.