Home Learning Designs and Chapter 4: Designs and Declarations Declarations
Designs and Chapter 4: Designs and Declarations Declarations

Designs and Chapter 4: Designs and Declarations Declarations


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 eventually become detailed enough to allow for the development of specific
interfaces. These interfaces must then be translated into C++ declarations. In this chapter, we attack the problem of designing and declaring 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, maintainability, 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 hard to use incorrectly.
C++ is awash in interfaces. Function interfaces. Class interfaces.
Template interfaces. Each interface is a means by which clients interact with your code. Assuming you’re dealing with reasonable people,
those clients are trying to do a good job. They want to use your interfaces correctly. That being the case, if they use one incorrectly, your
interface is at least partially to blame. Ideally, if an attempted use of
an interface won’t do what the client expects, the code won’t compile;
and if the code does compile, it will do what the client wants.
Developing interfaces that are easy to use correctly and hard to use
incorrectly requires that you consider the kinds of mistakes that cliDesigns and Chapter 4: Designs and Declarations
Designs and Declarations Item 18 79
ents might make. For example, suppose you’re designing the constructor for a class representing dates in time:
class Date {
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 {
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.
80 Item 18 Chapter 4
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 {
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
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 userdefined 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 characteristics lead to interfaces that are easy to use correctly as much as
consistency, and few characteristics lead to aggravating interfaces as
Designs and Declarations Item 18 81
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 developer’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 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
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 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();
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 resourcerelease 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 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
82 Item 18 Chapter 4
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.
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
with getRidOfInvestment 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
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),
… // make retVal point to the
// correct object
return retVal;

Leave a Reply