Write test code for your Set in main , and then substitute the Standard C++ Library set template to verify that the behavior is correct... Modify AutoCounter.h so that it can be used as
Trang 1much further A complicated container-class library may cover all
sorts of additional issues, including multithreading, persistence
and garbage collection
Exercises
Solutions to selected exercises can be found in the electronic document The Thinking in C++ Annotated
Solution Guide, available for a small fee from www.BruceEckel.com.
1 Implement the inheritance hierarchy in the OShape
diagram in this chapter
2 Modify the result of Exercise 1 from Chapter 15 to use the
Stack and iterator in TStack2.h instead of an array of Shape pointers Add destructors to the class hierarchy so you can see that the Shape objects are destroyed when the Stack goes out of scope
3 Modify TPStash.h so that the increment value used by
inflate( ) can be changed throughout the lifetime of a
particular container object
4 Modify TPStash.h so that the increment value used by
inflate( ) automatically resizes itself to reduce the
number of times it needs to be called For example, each time it is called it could double the increment value for use in the next call Demonstrate this functionality by
reporting whenever an inflate( ) is called, and write test code in main( )
5 Templatize the fibonacci( ) function on the type of value
that it produces (so it can produce long, float, etc instead
of just int)
6 Using the Standard C++ Library vector as an underlying
implementation, create a Set template class that accepts
only one of each type of object that you put into it Make
a nested iterator class that supports the “end sentinel”
concept in this chapter Write test code for your Set in main( ), and then substitute the Standard C++ Library set
template to verify that the behavior is correct
Trang 27 Modify AutoCounter.h so that it can be used as a
member object inside any class whose creation and
destruction you want to trace Add a string member to
hold the name of the class Test this tool inside a class of your own
8 Create a version of OwnerStack.h that uses a Standard
C++ Library vector as its underlying implementation
You may need to look up some of the member functions
of vector in order to do this (or just look at the <vector>
header file)
9 Modify ValueStack.h so that it dynamically expands as
you push( ) more objects and it runs out of space Change ValueStackTest.cpp to test the new functionality
10 Repeat Exercise 9 but use a Standard C++ Library vector
as the internal implementation of the ValueStack Notice
how much easier this is
11 Modify ValueStackTest.cpp so that it uses a Standard
C++ Library vector instead of a Stack in main( ) Notice the run-time behavior: Does the vector automatically
create a bunch of default objects when it is created?
12 Modify TStack2.h so that it uses a Standard C++ Library
vector as its underlying implementation Make sure that you don’t change the interface, so that TStack2Test.cpp
works unchanged
13 Repeat Exercise 12 using a Standard C++ Library stack
instead of a vector (you may need to look up information about the stack, or hunt through the <stack> header file)
14 Modify TPStash2.h so that it uses a Standard C++
Library vector as its underlying implementation Make
sure that you don’t change the interface, so that
TPStash2Test.cpp works unchanged
15 In IterIntStack.cpp, modify IntStackIter to give it an
“end sentinel” constructor, and add operator== and operator!= In main( ), use an iterator to move through
Trang 3the elements of the container until you reach the end
sentinel
16 Using TStack2.h, TPStash2.h, and Shape.h, instantiate
Stack and PStash containers for Shape*, fill them each
with an assortment of upcast Shape pointers, then use
iterators to move through each container and call draw( )
for each object
17 Templatize the Int class in TPStash2Test.cpp so that it
holds any type of object (feel free to change the name of
the class to something more appropriate)
18 Templatize the IntArray class in
IostreamOperatorOverloading.cpp from Chapter 12,
templatizing both the type of object that is contained and
the size of the internal array
19 Turn ObjContainer in NestedSmartPointer.cpp from
Chapter 12 into a template Test it with two different
classes
20 Modify C15:OStack.h and C15:OStackTest.cpp by
templatizing class Stack so that it automatically multiply
inherits from the contained class and from Object The
generated Stack should accept and produce only pointers
of the contained type
21 Repeat Exercise 20 using vector instead of Stack
22 Inherit a class StringVector from vector<void*> and
redefine the push_back( ) and operator[] member
functions to accept and produce only string* (and
perform the proper casting) Now create a template that
will automatically make a container class to do the same
thing for pointers to any type This technique is often
used to reduce code bloat from too many template
instantiations
23 In TPStash2.h, add and test an operator- to
PStash::iterator, following the logic of operator+
24 In Drawing.cpp, add and test a function template to call
erase( ) member functions
Trang 425 (Advanced) Modify the Stack class in TStack2.h to allow
full granularity of ownership: Add a flag to each link indicating whether that link owns the object it points to,
and support this information in the push( ) function and
destructor Add member functions to read and change the ownership for each link
26 (Advanced) Modify PointerToMemberOperator.cpp
from Chapter 12 so that the FunctionObject and
operator->* are templatized to work with any return type (for operator->*, you’ll have to use member templates,
described in Volume 2) Add and test support for zero,
one and two arguments in Dog member functions
Trang 6A: Coding Style
This appendix is not about indenting and placement of
parentheses and curly braces, although that will be
mentioned It is about the general guidelines used in
this book for organizing the code listings
Trang 7Although many of these issues have been introduced throughout
the book, this appendix appears at the end so it can be assumed
that every topic is fair game, and if you don’t understand
something you can look it up in the appropriate section
All the decisions about coding style in this book have been
deliberately considered and made, sometimes over a period of
years Of course, everyone has their reasons for organizing code the
way they do, and I’m just trying to tell you how I arrived at mine
and the constraints and environmental factors that brought me to
those decisions
General
In the text of this book, identifiers (function, variable, and class
names) are set in bold Most keywords will also be set in bold,
except for those keywords that are used so much that the bolding
can become tedious, such as “class” and “virtual.”
I use a particular coding style for the examples in this book It was
developed over a number of years, and was partially inspired by
Bjarne Stroustrup’s style in his original The C++ Programming
Language.1 The subject of formatting style is good for hours of hot
debate, so I’ll just say I’m not trying to dictate correct style via my
examples; I have my own motivation for using the style that I do
Because C++ is a free-form programming language, you can
continue to use whatever style you’re comfortable with
That said, I will note that it is important to have a consistent
formatting style within a project If you search the Internet, you will
find a number of tools that can be used to reformat all the code in
your project to achieve this valuable consistency
1 Ibid
Trang 8The programs in this book are files that are automatically extracted from the text of the book, which allows them to be tested to ensure that they work correctly Thus, the code files printed in the book should all work without compile-time errors when compiled with
an implementation that conforms to Standard C++ (note that not all compilers support all language features) The errors that should
cause compile-time error messages are commented out with the
comment //! so they can be easily discovered and tested using
automatic means Errors discovered and reported to the author will appear first in the electronic version of the book (at
www.BruceEckel.com) and later in updates of the book
One of the standards in this book is that all programs will compile and link without errors (although they will sometimes cause
warnings) To this end, some of the programs, which demonstrate only a coding example and don’t represent stand-alone programs,
will have empty main( ) functions, like this
int main() {}
This allows the linker to complete without an error
The standard for main( ) is to return an int, but Standard C++
states that if there is no return statement inside main( ), the
compiler will automatically generate code to return 0 This option (no return statement in main( )) will be used in this book (some
compilers may still generate warnings for this, but those are not compliant with Standard C++)
File names
In C, it has been traditional to name header files (containing
declarations) with an extension of h and implementation files (that
cause storage to be allocated and code to be generated) with an
extension of c C++ went through an evolution It was first
developed on Unix, where the operating system was aware of
upper and lower case in file names The original file names were
Trang 9simply capitalized versions of the C extensions: H and C This of
course didn’t work for operating systems that didn’t distinguish
upper and lower case, such as DOS DOS C++ vendors used
extensions of hxx and cxx for header files and implementation files,
respectively, or hpp and cpp Later, someone figured out that the
only reason you needed a different extension for a file was so the
compiler could determine whether to compile it as a C or C++ file
Because the compiler never compiled header files directly, only the
implementation file extension needed to be changed The custom,
across virtually all systems, has now become to use cpp for
implementation files and h for header files Note that when
including Standard C++ header files, the option of having no file
name extension is used, i.e.: #include <iostream>
Begin and end comment tags
A very important issue with this book is that all code that you see
in the book must be verified to be correct (with at least one
compiler) This is accomplished by automatically extracting the
files from the book To facilitate this, all code listings that are meant
to be compiled (as opposed to code fragments, of which there are
few) have comment tags at the beginning and end These tags are
used by the code-extraction tool ExtractCode.cpp in Volume 2 of
this book (which you can find on the Web site www.BruceEckel.com)
to pull each code listing out of the plain-ASCII text version of this
book
The end-listing tag simply tells ExtractCode.cpp that it’s the end of
the listing, but the begin-listing tag is followed by information
about what subdirectory the file belongs in (generally organized by
chapters, so a file that belongs in Chapter 8 would have a tag of
C08), followed by a colon and the name of the listing file
Because ExtractCode.cpp also creates a makefile for each
subdirectory, information about how a program is made and the
command-line used to test it is also incorporated into the listings If
Trang 10a program is stand-alone (it doesn’t need to be linked with
anything else) it has no extra information This is also true for
header files However, if it doesn’t contain a main( ) and is meant
to be linked with something else, then it has an {O} after the file
name If this listing is meant to be the main program but needs to
be linked with other components, there’s a separate line that begins
with //{L} and continues with all the files that need to be linked
(without extensions, since those can vary from platform to
platform)
You can find examples throughout the book
If a file should be extracted but the begin- and end-tags should not
be included in the extracted file (for example, if it’s a file of test
data) then the begin-tag is immediately followed by a ‘!’
Parentheses, braces, and indentation
You may notice the formatting style in this book is different from many traditional C styles Of course, everyone thinks their own
style is the most rational However, the style used here has a simple logic behind it, which will be presented here mixed in with ideas on why some of the other styles developed
The formatting style is motivated by one thing: presentation, both
in print and in live seminars You may feel your needs are different because you don’t make a lot of presentations However, working code is read much more than it is written, and so it should be easy for the reader to perceive My two most important criteria are
“scannability” (how easy it is for the reader to grasp the meaning of
a single line) and the number of lines that can fit on a page This
latter may sound funny, but when you are giving a live
presentation, it’s very distracting for the audience if the presenter must shuffle back and forth between slides, and a few wasted lines can cause this
Trang 11Everyone seems to agree that code inside braces should be
indented What people don’t agree on – and the place where there’s
the most inconsistency within formatting styles – is this: Where
does the opening brace go? This one question, I think, is what
causes such variations among coding styles (For an enumeration of
coding styles, see C++ Programming Guidelines, by Tom Plum and
Dan Saks, Plum Hall 1991.) I’ll try to convince you that many of
today’s coding styles come from pre-Standard C constraints (before
function prototypes) and are thus inappropriate now
First, my answer to that key question: the opening brace should
always go on the same line as the “precursor” (by which I mean
“whatever the body is about: a class, function, object definition, if
statement, etc.”) This is a single, consistent rule I apply to all of the
code I write, and it makes formatting much simpler It makes the
“scannability” easier – when you look at this line:
int func(int a);
you know, by the semicolon at the end of the line, that this is a
declaration and it goes no further, but when you see the line:
int func(int a) {
you immediately know it’s a definition because the line finishes
with an opening brace, not a semicolon By using this approach,
there’s no difference in where you place the opening parenthesis
for a multi-line definition:
int func(int a) {
int b = a + 1;
return b * 2;
}
and for a single-line definition that is often used for inlines:
int func(int a) { return (a + 1) * 2; }
Similarly, for a class:
Trang 12So why do we have so many other styles? In particular, you’ll
notice that most people create classes following the style above (which Stroustrup uses in all editions of his book The C++
Programming Language from Addison-Wesley) but create function
definitions by putting the opening brace on a single line by itself (which also engenders many different indentation styles)
Stroustrup does this except for short inline functions With the approach I describe here, everything is consistent – you name
whatever it is (class, function, enum, etc.) and on that same line
you put the opening brace to indicate that the body for this thing is about to follow Also, the opening brace is the same for short
inlines and ordinary function definitions
I assert that the style of function definition used by many folks comes from pre-function-prototyping C, in which you didn’t
declare the arguments inside the parentheses, but instead between the closing parenthesis and the opening curly brace (this shows C’s assembly-language roots):
Trang 13decisions about whether the braces should be indented with the
body of the code or whether they should be at the level of the
“precursor.” Thus, we got many different formatting styles
There are other arguments for placing the brace on the line
immediately following the declaration (of a class, struct, function,
etc.) The following came from a reader, and is presented here so
you know what the issues are:
Experienced ‘vi’ (vim) users know that typing the ‘]’ key twice
will take the user to the next occurrence of ‘{‘ (or ^L) in column
0 This feature is extremely useful in navigating code (jumping
to the next function or class definition) [My comment: when I
was initially working under Unix, GNU Emacs was just
appearing and I became enmeshed in that As a result, ‘vi’ has
never made sense to me, and thus I do not think in terms of
“column 0 locations.” However, there is a fair contingent of ‘vi’
users out there, and they are affected by this issue.]
Placing the ‘{‘ on the next line eliminates some confusing code
in complex conditionals, aiding in the scannability Example:
breaks up the ‘if’ from the body, resulting in better readability
[Your opinions on whether this is true will vary depending on
what you’re used to.]
Trang 14Finally, it’s much easier to visually align braces when they are aligned in the same column They visually "stick out" much better [End of reader comment]
The issue of where to put the opening curly brace is probably the most discordant issue I’ve learned to scan both forms, and in the end it comes down to what you’ve grown comfortable with
However, I note that the official Java coding standard (found on Sun’s Java Web site) is effectively the same as the one I present here – since more folks are beginning to program in both languages, the consistency between coding styles may be helpful
The approach I use removes all the exceptions and special cases, and logically produces a single style of indentation as well Even within a function body, the consistency holds, as in:
for(int i = 0; i < 100; i++) {
cout << i << endl;
cout << x * i << endl;
}
The style is easy to teach and to remember – you use a single,
consistent rule for all your formatting, not one for classes, two for functions (one-line inlines vs multi-line), and possibly others for
for loops, if statements, etc The consistency alone, I think, makes it
worthy of consideration Above all, C++ is a newer language than
C, and although we must make many concessions to C, we
shouldn’t be carrying too many artifacts with us that cause
problems in the future Small problems multiplied by many lines of code become big problems For a thorough examination of the subject, albeit in C, see C Style: Standards and Guidelines, by David
Straker (Prentice-Hall 1992)
The other constraint I must work under is the line width, since the book has a limitation of 50 characters What happens when
something is too long to fit on one line? Well, again I strive to have
a consistent policy for the way lines are broken up, so they can be easily viewed As long as something is part of a single definition,
Trang 15argument list, etc., continuation lines should be indented one level
in from the beginning of that definition, argument list, etc
Identifier names
Those familiar with Java will notice that I have switched to using
the standard Java style for all identifier names However, I cannot
be completely consistent here because identifiers in the Standard C
and C++ libraries do not follow this style
The style is quite straightforward The first letter of an identifier is
only capitalized if that identifier is a class If it is a function or
variable, then the first letter is lowercase The rest of the identifier
consists of one or more words, run together but distinguished by
capitalizing each word So a class looks like this:
class FrenchVanilla : public IceCream {
an object identifier looks like this:
FrenchVanilla myIceCreamCone(3);
and a function looks like this:
void eatIceCreamCone();
(for either a member function or a regular function)
The one exception is for compile-time constants (const or #define),
in which all of the letters in the identifier are uppercase
The value of the style is that capitalization has meaning – you can
see from the first letter whether you’re talking about a class or an
object/method This is especially useful when static class members
are accessed
Trang 16Order of header inclusion
Headers are included in order from “the most specific to the most general.” That is, any header files in the local directory are included
first, then any of my own “tool” headers, such as require.h, then
any third-party library headers, then the Standard C++ Library headers, and finally the C library headers
The justification for this comes from John Lakos in Large-Scale C++ Software Design (Addison-Wesley, 1996):
Latent usage errors can be avoided by ensuring that the h file of a component parses by itself – without externally-provided declarations
or definitions Including the h file as the very first line of the c file ensures that no critical piece of information intrinsic to the physical interface of the component is missing from the h file (or, if there is, that you will find out about it as soon as you try to compile the c file)
If the order of header inclusion goes “from most specific to most general,” then it’s more likely that if your header doesn’t parse by itself, you’ll find out about it sooner and prevent annoyances down the road
Include guards on header files
Include guards are always used inside header files to prevent
multiple inclusion of a header file during the compilation of a
single cpp file The include guards are implemented using a
preprocessor #define and checking to see that a name hasn’t
already been defined The name used for the guard is based on the name of the header file, with all letters of the file name uppercase
and replacing the ‘.’ with an underscore For example:
Trang 17The identifier on the last line is included for clarity Although some
preprocessors ignored any characters after an #endif, that isn’t
standard behavior and so the identifier is commented
Use of namespaces
In header files, any “pollution” of the namespace in which the
header is included must be scrupulously avoided That is, if you
change the namespace outside of a function or class, you will cause
that change to occur for any file that includes your header,
resulting in all kinds of problems No using declarations of any
kind are allowed outside of function definitions, and no global
using directives are allowed in header files
In cpp files, any global using directives will only affect that file,
and so in this book they are generally used to produce more
easily-readable code, especially in small programs
Use of require( ) and assure( )
The require( ) and assure( ) functions defined in require.h are used
consistently throughout most of the book, so that they may
properly report problems If you are familiar with the concepts of
preconditions and postconditions (introduced by Bertrand Meyer) you
will recognize that the use of require( ) and assure( ) more or less
provide preconditions (usually) and postconditions (occasionally)
Thus, at the beginning of a function, before any of the “core” of the
function is executed, the preconditions are checked to make sure
everything is proper and that all of the necessary conditions are
correct Then the “core” of the function is executed, and sometimes
some postconditions are checked to make sure that the new state of
the data is within defined parameters You’ll notice that the
postcondition checks are rare in this book, and assure( ) is
primarily used to make sure that files were opened successfully
Trang 18B: Programming Guidelines
This appendix is a collection of suggestions for C++
programming They’ve been assembled over the course
of my teaching and programming experience and
Trang 19also from the insights of friends including Dan Saks (co-author with
Tom Plum of C++ Programming Guidelines, Plum Hall, 1991), Scott
Meyers (author of Effective C++, 2nd edition, Addison-Wesley, 1998),
and Rob Murray (author of C++ Strategies & Tactics, Addison-Wesley,
1993) Also, many of the tips are summarized from the pages of
Thinking in C++
1 First make it work, then make it fast This is true even if you
are certain that a piece of code is really important and that it
will be a principal bottleneck in your system Don’t do it Get
the system going first with as simple a design as possible
Then if it isn’t going fast enough, profile it You’ll almost
always discover that “your” bottleneck isn’t the problem
Save your time for the really important stuff
2 Elegance always pays off It’s not a frivolous pursuit Not
only does it give you a program that’s easier to build and
debug, but it’s also easier to understand and maintain, and
that’s where the financial value lies This point can take some
experience to believe, because it can seem that while you’re
making a piece of code elegant, you’re not being productive
The productivity comes when the code seamlessly integrates
into your system, and even more so when the code or system
is modified
3 Remember the “divide and conquer” principle If the
problem you’re looking at is too confusing, try to imagine
what the basic operation of the program would be, given the
existence of a magic “piece” that handles the hard parts That
“piece” is an object – write the code that uses the object, then
look at the object and encapsulate its hard parts into other
objects, etc
4 Don’t automatically rewrite all your existing C code in C++
unless you need to significantly change its functionality (that
is, don’t fix it if it isn’t broken) Recompiling C in C++ is a
valuable activity because it may reveal hidden bugs
Trang 20However, taking C code that works fine and rewriting it in C++ may not be the best use of your time, unless the C++ version will provide a lot of opportunities for reuse as a class
5 If you do have a large body of C code that needs changing, first isolate the parts of the code that will not be modified, possibly wrapping those functions in an “API class” as static member functions Then focus on the code that will be
changed, refactoring it into classes to facilitate easy
modifications as your maintenance proceeds
6 Separate the class creator from the class user (client
programmer) The class user is the “customer” and doesn’t
need or want to know what’s going on behind the scenes of the class The class creator must be the expert in class design and write the class so that it can be used by the most novice programmer possible, yet still work robustly in the
application Library use will be easy only if it’s transparent
7 When you create a class, make your names as clear as
possible Your goal should be to make the client
programmer’s interface conceptually simple Attempt to make your names so clear that comments are unnecessary To this end, use function overloading and default arguments to create an intuitive, easy-to-use interface
8 Access control allows you (the class creator) to change as much as possible in the future without damaging client code
in which the class is used In this light, keep everything as
private as possible, and make only the class interface public, always using functions rather than data Make data public
only when forced If class users don’t need to access a
function, make it private If a part of your class must be
exposed to inheritors as protected, provide a function
interface rather than expose the actual data In this way, implementation changes will have minimal impact on
derived classes
Trang 219 Don’t fall into analysis paralysis There are some things that
you don’t learn until you start coding and get some kind of
system working C++ has built-in firewalls; let them work for
you Your mistakes in a class or set of classes won’t destroy
the integrity of the whole system
10 Your analysis and design must produce, at minimum, the
classes in your system, their public interfaces, and their
relationships to other classes, especially base classes If your
design methodology produces more than that, ask yourself if
all the pieces produced by that methodology have value over
the lifetime of the program If they do not, maintaining them
will cost you Members of development teams tend not to
maintain anything that does not contribute to their
productivity; this is a fact of life that many design methods
don’t account for
11 Write the test code first (before you write the class), and keep
it with the class Automate the running of your tests through
a makefile or similar tool This way, any changes can be
automatically verified by running the test code, and you’ll
immediately discover errors Because you know that you
have the safety net of your test framework, you will be bolder
about making sweeping changes when you discover the
need Remember that the greatest improvements in
languages come from the built-in testing that type checking,
exception handling, etc., provide, but those features take you
only so far You must go the rest of the way in creating a
robust system by filling in the tests that verify features that
are specific to your class or program
12 Write the test code first (before you write the class) in order
to verify that your class design is complete If you can’t write
test code, you don’t know what your class looks like In
addition, the act of writing the test code will often flush out
additional features or constraints that you need in the class –
Trang 22these features or constraints don’t always appear during analysis and design
13 Remember a fundamental rule of software engineering1: All software design problems can be simplified by introducing an extra level of conceptual indirection This one idea is the basis of
abstraction, the primary feature of object-oriented
programming
14 Make classes as atomic as possible; that is, give each class a single, clear purpose If your classes or your system design grows too complicated, break complex classes into simpler ones The most obvious indicator of this is sheer size: if a class is big, chances are it’s doing too much and should be broken up
15 Watch for long member function definitions A function that
is long and complicated is difficult and expensive to
maintain, and is probably trying to do too much all by itself
If you see such a function, it indicates that, at the least, it should be broken up into multiple functions It may also suggest the creation of a new class
16 Watch for long argument lists Function calls then become difficult to write, read and maintain Instead, try to move the member function to a class where it is (more) appropriate, and/or pass objects in as arguments
17 Don’t repeat yourself If a piece of code is recurring in many functions in derived classes, put that code into a single
function in the base class and call it from the derived-class functions Not only do you save code space, you provide for easy propagation of changes You can use an inline function for efficiency Sometimes the discovery of this common code will add valuable functionality to your interface
1 Explained to me by Andrew Koenig
Trang 2318 Watch for switch statements or chained if-else clauses This
is typically an indicator of type-check coding, which means you
are choosing what code to execute based on some kind of
type information (the exact type may not be obvious at first)
You can usually replace this kind of code with inheritance
and polymorphism; a polymorphic function call will perform
the type checking for you, and allow for more reliable and
easier extensibility
19 From a design standpoint, look for and separate things that
change from things that stay the same That is, search for the
elements in a system that you might want to change without
forcing a redesign, then encapsulate those elements in
classes You can learn significantly more about this concept in
the Design Patterns chapter in Volume 2 of this book,
available at www.BruceEckel.com
20 Watch out for variance Two semantically different objects
may have identical actions, or responsibilities, and there is a
natural temptation to try to make one a subclass of the other
just to benefit from inheritance This is called variance, but
there’s no real justification to force a superclass/subclass
relationship where it doesn’t exist A better solution is to
create a general base class that produces an interface for both
as derived classes – it requires a bit more space, but you still
benefit from inheritance and will probably make an
important discovery about the design
21 Watch out for limitation during inheritance The clearest
designs add new capabilities to inherited ones A suspicious
design removes old capabilities during inheritance without
adding new ones But rules are made to be broken, and if you
are working from an old class library, it may be more
efficient to restrict an existing class in its subclass than it
would be to restructure the hierarchy so your new class fits
in where it should, above the old class
Trang 2422 Don’t extend fundamental functionality by subclassing If an interface element is essential to a class it should be in the base class, not added during derivation If you’re adding member functions by inheriting, perhaps you should rethink the
design
23 Less is more Start with a minimal interface to a class, as
small and simple as you need to solve the problem at hand, but don’t try to anticipate all the ways that your class might
be used As the class is used, you’ll discover ways you must expand the interface However, once a class is in use you cannot shrink the interface without disturbing client code If you need to add more functions, that’s fine; it won’t disturb code, other than forcing recompiles But even if new member functions replace the functionality of old ones, leave the
existing interface alone (you can combine the functionality in the underlying implementation if you want) If you need to expand the interface of an existing function by adding more arguments, leave the existing arguments in their current order, and put default values on all of the new arguments; this way you won’t disturb any existing calls to that function
24 Read your classes aloud to make sure they’re logical,
referring to the relationship between a base class and derived class as “is-a” and member objects as “has-a.”
25 When deciding between inheritance and composition, ask if you need to upcast to the base type If not, prefer
composition (member objects) to inheritance This can
eliminate the perceived need for multiple inheritance If you inherit, users will think they are supposed to upcast
26 Sometimes you need to inherit in order to access protected
members of the base class This can lead to a perceived need for multiple inheritance If you don’t need to upcast, first derive a new class to perform the protected access Then
Trang 25make that new class a member object inside any class that
needs to use it, rather than inheriting
27 Typically, a base class will be used primarily to create an
interface to classes derived from it Thus, when you create a
base class, default to making the member functions pure
virtual The destructor can also be pure virtual (to force
inheritors to explicitly override it), but remember to give the
destructor a function body, because all destructors in a
hierarchy are always called
28 When you put a virtual function in a class, make all functions
in that class virtual, and put in a virtual destructor This
approach prevents surprises in the behavior of the interface
Only start removing the virtual keyword when you’re tuning
for efficiency and your profiler has pointed you in this
direction
29 Use data members for variation in value and virtual
functions for variation in behavior That is, if you find a class
that uses state variables along with member functions that
switch behavior based on those variables, you should
probably redesign it to express the differences in behavior
within subclasses and overridden virtual functions
30 If you must do something nonportable, make an abstraction
for that service and localize it within a class This extra level
of indirection prevents the non-portability from being
distributed throughout your program
31 Avoid multiple inheritance It’s for getting you out of bad
situations, especially repairing class interfaces in which you
don’t have control of the broken class (see Volume 2) You
should be an experienced programmer before designing
multiple inheritance into your system
32 Don’t use private inheritance Although it’s in the language
and seems to have occasional functionality, it introduces