Tải bản đầy đủ (.pdf) (50 trang)

Thinking in C plus plus(P17) pptx

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (183.03 KB, 50 trang )

780 Thinking in C++ www.BruceEckel.com
much 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.

16: Introduction to Templates 781
7. 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
782 Thinking in C++ www.BruceEckel.com
the 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.

16: Introduction to Templates 783
25. (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.

785









A: 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.
786 Thinking in C++ www.BruceEckel.com
Although 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.

A: Coding Style 787
The 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
788 Thinking in C++ www.BruceEckel.com
simply 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

A: Coding Style 789
a 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.
790 Thinking in C++ www.BruceEckel.com
Everyone 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:

A: Coding Style 791
class Thing;

is a class name declaration, and
class Thing {

is a class definition. You can tell by looking at the single line in all
cases whether it’s a declaration or definition. And of course,
putting the opening brace on the same line, instead of a line by
itself, allows you to fit more lines on a page.
So 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):
void bar()
int x;
float y;
{
/* body here */
}

Here, it would be quite ungainly to put the opening brace on the
same line, so no one did it. However, they did make various
792 Thinking in C++ www.BruceEckel.com
decisions 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:
if(cond1
&& cond2
&& cond3) {
statement;
}

The above [asserts the reader] has poor scannability. However,
if (cond1
&& cond2
&& cond3)
{
statement;
}

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.]

A: Coding Style 793
Finally, 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,
794 Thinking in C++ www.BruceEckel.com
argument 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.

A: Coding Style 795
Order 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:
// IncludeGuard.h
#ifndef INCLUDEGUARD_H
#define INCLUDEGUARD_H
// Body of header file here
#endif // INCLUDEGUARD_H
796 Thinking in C++ www.BruceEckel.com

The 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.
797



















































B: 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
798 Thinking in C++ www.BruceEckel.com
also 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++
, 2
nd
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.

B: Programming Guidelines 799
However, 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.
800 Thinking in C++ www.BruceEckel.com
9. 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 –

B: Programming Guidelines 801
these features or constraints don’t always appear during
analysis and design.
13. Remember a fundamental rule of software engineering
1
:
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.
802 Thinking in C++ www.BruceEckel.com
18. 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.

B: Programming Guidelines 803
22. 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
804 Thinking in C++ www.BruceEckel.com
make 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

×