One of the important features C++
inherits from C is efficiency. If the efficiency of C++
were dramatically
less than C
there
would be a significant contingent of programmers who couldn’t justify its
use.
In C
one of the ways to preserve
efficiency is through the use of
macros
which allow you
to make what looks like a function call without the normal
function call overhead. The
macro is implemented with the preprocessor instead of the compiler proper
and
the preprocessor replaces all macro calls directly with the macro code
so
there’s no cost involved from pushing arguments
making an
assembly-language CALL
returning arguments
and performing an assembly-language
RETURN. All the work is performed by the preprocessor
so you have the
convenience and readability of a function call but it doesn’t cost you
anything.
There are two problems with the use of
preprocessor macros in C++. The first is also true with
C: a macro looks like a function call
but doesn’t always act like one.
This can bury difficult-to-find bugs. The second problem is specific to C++: the
preprocessor has no permission to access class member data. This means
preprocessor macros cannot be used as class member functions.
To retain the efficiency of the
preprocessor macro
but to add the safety and class scoping of true functions
C++ has the inline
function. In this chapter
we’ll look at the problems of preprocessor macros in C++
how these
problems are solved with inline functions
and guidelines and insights on the
way inlines
work.
The key to the problems of preprocessor
macros is that you can be fooled into thinking that the behavior of the
preprocessor is the same as the behavior of the compiler. Of course
it was
intended that a macro look and act like a function call
so it’s quite
easy to fall into this fiction. The difficulties begin when the subtle
differences appear.
As a simple example
consider the
following:
#define F (x) (x + 1)
Now
if a call is made to F like
this
F(1)
the preprocessor expands it
somewhat
unexpectedly
to the following:
(x) (x + 1)(1)
The problem occurs because of the gap
between F and its opening parenthesis in the macro definition. When this
gap is removed
you can actually call the macro with the
gap
F (1)
and it will still expand properly
to
(1 + 1)
The example above is fairly trivial and
the problem will make itself evident right away. The real difficulties occur
when using expressions as arguments in macro calls.
There are two problems. The first is that
expressions may expand inside the macro so that their evaluation precedence is
different from what you expect. For example
#define FLOOR(x b) x>=b?0:1
Now
if expressions are used for the
arguments
if(FLOOR(a&0x0f 0x07)) // ...
the macro will expand to
if(a&0x0f>=0x07?0:1)
The precedence of & is lower
than that of >=
so the macro evaluation will surprise you. Once you
discover the problem
you can solve it by putting parentheses around everything
in the macro definition. (This is a good practice to use when creating
preprocessor macros.) Thus
#define FLOOR(x b) ((x)>=(b)?0:1)
Discovering the problem may be difficult
however
and you may not find it until after you’ve taken the proper macro
behavior for granted. In the un-parenthesized version of the preceding macro
most expressions will work correctly because the precedence of
>= is lower than most of the operators like +
/
–
–
and even the bitwise shift operators. So you can easily begin to
think that it works with all expressions
including those using bitwise logical
operators.
The preceding problem can be solved with
careful programming practice: parenthesize everything in a macro. However
the
second difficulty is subtler. Unlike a normal function
every time you use an
argument in a macro
that
argument is evaluated. As long as the macro is called only with ordinary
variables
this evaluation is benign
but if the evaluation of an argument has
side effects
then the results can be surprising and will definitely not mimic
function behavior.
For example
this macro determines
whether its argument falls within a certain range:
#define BAND(x) (((x)>5 && (x)<10) ? (x) : 0)
As long as you use an
“ordinary” argument
the macro works very much like a real function.
But as soon as you relax and start believing it is a real function
the
problems start. Thus:
//: C09:MacroSideEffects.cpp
#include "../require.h"
#include <fstream>
using namespace std;
#define BAND(x) (((x)>5 && (x)<10) ? (x) : 0)
int main() {
ofstream out("macro.out");
assure(out
"macro.out");
for(int i = 4; i < 11; i++) {
int a = i;
out << "a = " << a << endl << '\t';
out << "BAND(++a)=" << BAND(++a) << endl;
out << "\t a = " << a << endl;
}
} ///:~
Notice the use of all upper-case
characters in the name of the macro. This is a helpful practice because it tells
the reader this is a macro and not a function
so if there are problems
it acts
as a little reminder.
Here’s the output produced by the
program
which is not at all what you would have expected from a true
function:
a = 4 BAND(++a)=0 a = 5 a = 5 BAND(++a)=8 a = 8 a = 6 BAND(++a)=9 a = 9 a = 7 BAND(++a)=10 a = 10 a = 8 BAND(++a)=0 a = 10 a = 9 BAND(++a)=0 a = 11 a = 10 BAND(++a)=0 a = 12
When a is four
only the first
part of the conditional occurs
so the expression is evaluated only once
and
the side effect of the macro call is that a becomes five
which is what
you would expect from a normal function call in the same situation. However
when the number is within the band
both conditionals are tested
which results
in two increments. The result is produced by evaluating the argument again
which results in a third increment. Once the number gets out of the band
both
conditionals are still tested so you get two increments. The side effects are
different
depending on the argument.
This is clearly not the kind of behavior
you want from a macro that looks like a function call. In this case
the obvious
solution is to make it a true function
which of course adds the extra overhead
and may reduce efficiency if you call that function a lot. Unfortunately
the
problem may not always be so obvious
and you can unknowingly get a library that
contains functions and macros mixed together
so a problem like this can hide
some very difficult-to-find bugs. For example
the
putc( ) macro in cstdio may evaluate
its second argument twice. This is specified in Standard C. Also
careless
implementations of toupper( ) as a macro may
evaluate the argument more than once
which will give you unexpected results
with
toupper(*p++).[45]
Of course
careful coding and use of
preprocessor macros is required with C
and we could certainly get away with the
same thing in C++ if it weren’t for one problem: a macro has no concept of
the scoping required with member functions. The
preprocessor simply performs text substitution
so you
cannot say something like
class X {
int i;
public:
#define VAL(X::i) // Error
or anything even close. In addition
there would be no indication of which object you were referring to. There is
simply no way to express class scope in a macro. Without some alternative to
preprocessor macros
programmers will be tempted to make some data members
public for the sake of efficiency
thus exposing the underlying
implementation and preventing changes in that implementation
as well as
eliminating the guarding that private
provides.
In solving the C++ problem of a macro
with access to private class members
all
the problems associated with preprocessor macros were eliminated. This was done
by bringing the concept of macros under the control of the compiler where they
belong. C++ implements the macro as inline
function
which is a true
function in every sense. Any behavior you expect from an ordinary function
you
get from an inline function. The only difference is that an inline function is
expanded in place
like a preprocessor macro
so the overhead of the function
call is eliminated. Thus
you
should (almost) never use macros
only inline functions.
Any function defined within a class body
is automatically inline
but you can also make a non-class function inline by
preceding it with the inline keyword. However
for it to have any effect
you must include the function body with the declaration
otherwise the compiler
will treat it as an ordinary function declaration. Thus
inline int plusOne(int x);
has no effect at all other than declaring
the function (which may or may not get an inline definition sometime later). The
successful approach provides the function body:
inline int plusOne(int x) { return ++x; }
Notice that the compiler will check (as
it always does) for the proper use of the function argument list and return
value (performing any necessary conversions)
something the preprocessor is
incapable of. Also
if you try to write the above as a preprocessor macro
you
get an unwanted side effect.
You’ll almost always want to put
inline definitions in a header
file. When the compiler sees
such a definition
it puts the function type (the signature combined with the
return value) and the function body in its symbol table. When you use the
function
the compiler checks to ensure the call is correct and the return value
is being used correctly
and then substitutes the function body for the function
call
thus eliminating the overhead. The inline code does occupy space
but if
the function is small
this can actually take less space than the code generated
to do an ordinary function call (pushing arguments on the stack and doing the
CALL).
An inline function in a header file has a
special status
since you must include the header file containing the function
and its definition in every file where the function is used
but you
don’t end up with multiple definition errors (however
the definition must
be identical in all places where the inline function is
included).
To define an inline function
you must
ordinarily precede the function definition with the inline keyword.
However
this is not necessary inside a class
definition. Any function you
define inside a class definition is automatically an inline. For
example:
//: C09:Inline.cpp
// Inlines inside classes
#include <iostream>
#include <string>
using namespace std;
class Point {
int i
j
k;
public:
Point(): i(0)
j(0)
k(0) {}
Point(int ii
int jj
int kk)
: i(ii)
j(jj)
k(kk) {}
void print(const string& msg = "") const {
if(msg.size() != 0) cout << msg << endl;
cout << "i = " << i << "
<<
j = " << j << "
<<
k = " << k << endl;
}
};
int main() {
Point p
q(1
2
3);
p.print("value of p");
q.print("value of q");
} ///:~
Here
the two constructors and the
print( ) function are all inlines by default. Notice in
main( ) that the fact you are using inline functions is transparent
as it should be. The logical behavior of a function must be identical regardless
of whether it’s an inline (otherwise your compiler is broken). The only
difference you’ll see is in performance.
Of course
the temptation is to use
inlines everywhere inside class declarations because they save you the extra
step of making the external member function definition. Keep in mind
however
that the idea of an inline is to provide improved opportunities for
optimization by the compiler. But inlining a big
function will cause that code to be duplicated everywhere the function is
called
producing code bloat that may mitigate the speed benefit (the only
reliable course of action is to experiment to discover the effects of inlining
on your program with your
compiler).
One of the most important uses of inlines
inside classes is the access
function. This is a small
function that allows you to read or change part of the state of an object
– that is
an internal variable or variables. The reason inlines are so
important for access functions can be seen in the following
example:
//: C09:Access.cpp
// Inline access functions
class Access {
int i;
public:
int read() const { return i; }
void set(int ii) { i = ii; }
};
int main() {
Access A;
A.set(100);
int x = A.read();
} ///:~
Here
the class user never has direct
contact with the state variables inside the class
and they can be kept
private
under the
control of the class designer. All the access to the private data members
can be controlled through the member function interface. In addition
access is
remarkably efficient. Consider the read( )
for example. Without
inlines
the code generated for the call to read( ) would typically
include pushing this on
the stack and making an assembly language CALL. With most machines
the size of
this code would be larger than the code created by the inline
and the execution
time would certainly be longer.
Without inline functions
an
efficiency-conscious class designer will be tempted to simply make i a
public member
eliminating the overhead by allowing the user to directly access
i. From a design standpoint
this is disastrous
because i then becomes part of the public interface
which means the
class designer can never change it. You’re stuck with an int called
i. This is a problem because you may learn sometime later that it would
be much more useful to represent the state information as a float rather
than an int
but because int i is part of the public interface
you can’t change it. Or you may want to perform some additional
calculation as part of reading or setting i
which you can’t do if
it’s public. If
on the other hand
you’ve always used
member functions to read and change the state information of an object
you can
modify the underlying representation of the object to your heart’s
content.
In addition
the use of member functions
to control data members allows you to add code to the member function to detect
when that data is being changed
which can be very useful during debugging. If a
data member is public
anyone can change it anytime without you knowing
about it.
Some people further divide the concept of
access functions into accessors (to read state
information from an object) and mutators (to
change the state of an object). In addition
function overloading may be used to
provide the same function name for both the accessor and mutator; how you call
the function determines whether you’re reading or modifying state
information. Thus
//: C09:Rectangle.cpp
// Accessors & mutators
class Rectangle {
int wide
high;
public:
Rectangle(int w = 0
int h = 0)
: wide(w)
high(h) {}
int width() const { return wide; } // Read
void width(int w) { wide = w; } // Set
int height() const { return high; } // Read
void height(int h) { high = h; } // Set
};
int main() {
Rectangle r(19
47);
// Change width & height:
r.height(2 * r.width());
r.width(2 * r.height());
} ///:~
The constructor uses the constructor
initializer list (briefly introduced in Chapter 8 and covered fully in Chapter
14) to initialize the values of wide and high (using the
pseudoconstructor form for
built-in types).
You cannot have member function names
using the same identifiers as data members
so you might be tempted to
distinguish the data members with a
leading underscore. However
identifiers with leading underscores are reserved so you should not use them.
//: C09:Rectangle2.cpp
// Accessors & mutators with "get" and "set"
class Rectangle {
int width
height;
public:
Rectangle(int w = 0
int h = 0)
: width(w)
height(h) {}
int getWidth() const { return width; }
void setWidth(int w) { width = w; }
int getHeight() const { return height; }
void setHeight(int h) { height = h; }
};
int main() {
Rectangle r(19
47);
// Change width & height:
r.setHeight(2 * r.getWidth());
r.setWidth(2 * r.getHeight());
} ///:~
Of course
accessors and mutators
don’t have to be simple pipelines to an internal variable. Sometimes they
can perform more sophisticated calculations. The following example uses the
Standard C library time functions to produce a simple Time
class:
//: C09:Cpptime.h
// A simple time class
#ifndef CPPTIME_H
#define CPPTIME_H
#include <ctime>
#include <cstring>
class Time {
std::time_t t;
std::tm local;
char asciiRep[26];
unsigned char lflag
aflag;
void updateLocal() {
if(!lflag) {
local = *std::localtime(&t);
lflag++;
}
}
void updateAscii() {
if(!aflag) {
updateLocal();
std::strcpy(asciiRep
std::asctime(&local));
aflag++;
}
}
public:
Time() { mark(); }
void mark() {
lflag = aflag = 0;
std::time(&t);
}
const char* ascii() {
updateAscii();
return asciiRep;
}
// Difference in seconds:
int delta(Time* dt) const {
return int(std::difftime(t
dt->t));
}
int daylightSavings() {
updateLocal();
return local.tm_isdst;
}
int dayOfYear() { // Since January 1
updateLocal();
return local.tm_yday;
}
int dayOfWeek() { // Since Sunday
updateLocal();
return local.tm_wday;
}
int since1900() { // Years since 1900
updateLocal();
return local.tm_year;
}
int month() { // Since January
updateLocal();
return local.tm_mon;
}
int dayOfMonth() {
updateLocal();
return local.tm_mday;
}
int hour() { // Since midnight
24-hour clock
updateLocal();
return local.tm_hour;
}
int minute() {
updateLocal();
return local.tm_min;
}
int second() {
updateLocal();
return local.tm_sec;
}
};
#endif // CPPTIME_H ///:~
The Standard C library
functions have multiple representations for time
and
these are all part of the Time class. However
it isn’t necessary
to update all of them
so instead the time_t t is
used as the base representation
and the tm local and ASCII character
representation asciiRep each have flags to indicate if they’ve been
updated to the current time_t. The two private functions
updateLocal( ) and updateAscii( ) check the flags and
conditionally perform the update.
The constructor calls the
mark( ) function (which the user can also call to force the object
to represent the current time)
and this clears the two flags to indicate that
the local time and ASCII representation are now invalid. The
ascii( ) function calls updateAscii( )
which copies the
result of the Standard C library function
asctime( ) into a local buffer because
asctime( ) uses a static data area that is overwritten if the
function is called elsewhere. The ascii( ) function return value is
the address of this local buffer.
All the functions starting with
daylightSavings( ) use the updateLocal( ) function
which causes the resulting composite inlines to be fairly large. This
doesn’t seem worthwhile
especially considering you probably won’t
call the functions very much. However
this doesn’t mean all the functions
should be made non-inline. If you make other functions non-inline
at least keep
updateLocal( ) inline so that its code will be duplicated in the
non-inline functions
eliminating extra function-call overhead.
Here’s a small test
program:
//: C09:Cpptime.cpp
// Testing a simple time class
#include "Cpptime.h"
#include <iostream>
using namespace std;
int main() {
Time start;
for(int i = 1; i < 1000; i++) {
cout << i << ' ';
if(i%10 == 0) cout << endl;
}
Time end;
cout << endl;
cout << "start = " << start.ascii();
cout << "end = " << end.ascii();
cout << "delta = " << end.delta(&start);
} ///:~
A Time object is created
then
some time-consuming activity is performed
then a second Time object is
created to mark the ending time. These are used to show starting
ending
and
elapsed
times.
//: C09:Stash4.h
// Inline functions
#ifndef STASH4_H
#define STASH4_H
#include "../require.h"
class Stash {
int size; // Size of each space
int quantity; // Number of storage spaces
int next; // Next empty space
// Dynamically allocated array of bytes:
unsigned char* storage;
void inflate(int increase);
public:
Stash(int sz) : size(sz)
quantity(0)
next(0)
storage(0) {}
Stash(int sz
int initQuantity) : size(sz)
quantity(0)
next(0)
storage(0) {
inflate(initQuantity);
}
Stash::~Stash() {
if(storage != 0)
delete []storage;
}
int add(void* element);
void* fetch(int index) const {
require(0 <= index
"Stash::fetch (-)index");
if(index >= next)
return 0; // To indicate the end
// Produce pointer to desired element:
return &(storage[index * size]);
}
int count() const { return next; }
};
#endif // STASH4_H ///:~
The small functions obviously work well
as inlines
but notice that the two largest functions are still left as
non-inlines
since inlining them probably wouldn’t cause any performance
gains:
//: C09:Stash4.cpp {O}
#include "Stash4.h"
#include <iostream>
#include <cassert>
using namespace std;
const int increment = 100;
int Stash::add(void* element) {
if(next >= quantity) // Enough space left?
inflate(increment);
// Copy element into storage
// starting at next empty space:
int startBytes = next * size;
unsigned char* e = (unsigned char*)element;
for(int i = 0; i < size; i++)
storage[startBytes + i] = e[i];
next++;
return(next - 1); // Index number
}
void Stash::inflate(int increase) {
assert(increase >= 0);
if(increase == 0) return;
int newQuantity = quantity + increase;
int newBytes = newQuantity * size;
int oldBytes = quantity * size;
unsigned char* b = new unsigned char[newBytes];
for(int i = 0; i < oldBytes; i++)
b[i] = storage[i]; // Copy old to new
delete [](storage); // Release old storage
storage = b; // Point to new memory
quantity = newQuantity; // Adjust the size
} ///:~
Once again
the test program verifies
that everything is working correctly:
//: C09:Stash4Test.cpp
//{L} Stash4
#include "Stash4.h"
#include "../require.h"
#include <fstream>
#include <iostream>
#include <string>
using namespace std;
int main() {
Stash intStash(sizeof(int));
for(int i = 0; i < 100; i++)
intStash.add(&i);
for(int j = 0; j < intStash.count(); j++)
cout << "intStash.fetch(" << j << ") = "
<< *(int*)intStash.fetch(j)
<< endl;
const int bufsize = 80;
Stash stringStash(sizeof(char) * bufsize
100);
ifstream in("Stash4Test.cpp");
assure(in
"Stash4Test.cpp");
string line;
while(getline(in
line))
stringStash.add((char*)line.c_str());
int k = 0;
char* cp;
while((cp = (char*)stringStash.fetch(k++))!=0)
cout << "stringStash.fetch(" << k << ") = "
<< cp << endl;
} ///:~
This is the same test program that was
used before
so the output should be basically the same.
//: C09:Stack4.h
// With inlines
#ifndef STACK4_H
#define STACK4_H
#include "../require.h"
class Stack {
struct Link {
void* data;
Link* next;
Link(void* dat
Link* nxt):
data(dat)
next(nxt) {}
}* head;
public:
Stack() : head(0) {}
~Stack() {
require(head == 0
"Stack not empty");
}
void push(void* dat) {
head = new Link(dat
head);
}
void* peek() const {
return head ? head->data : 0;
}
void* pop() {
if(head == 0) return 0;
void* result = head->data;
Link* oldHead = head;
head = head->next;
delete oldHead;
return result;
}
};
#endif // STACK4_H ///:~
Notice that the Link destructor
that was present but empty in the previous version of Stack has been
removed. In pop( )
the expression delete oldHead simply
releases the memory used by that Link (it does not destroy the data
object pointed to by the Link).
Most of the functions inline quite nicely
and obviously
especially for Link. Even pop( ) seems
legitimate
although anytime you have conditionals or local variables it’s
not clear that inlines will be that beneficial. Here
the function is small
enough that it probably won’t hurt anything.
If all your functions are inlined
using the library becomes quite simple because there’s no linking
necessary
as you can see in the test example (notice that there’s no
Stack4.cpp):
//: C09:Stack4Test.cpp
//{T} Stack4Test.cpp
#include "Stack4.h"
#include "../require.h"
#include <fstream>
#include <iostream>
#include <string>
using namespace std;
int main(int argc
char* argv[]) {
requireArgs(argc
1); // File name is argument
ifstream in(argv[1]);
assure(in
argv[1]);
Stack textlines;
string line;
// Read file and store lines in the stack:
while(getline(in
line))
textlines.push(new string(line));
// Pop the lines from the stack and print them:
string* s;
while((s = (string*)textlines.pop()) != 0) {
cout << *s << endl;
delete s;
}
} ///:~
People will sometimes write classes with
all inline functions so that the whole class will be in the header file
(you’ll see in this book that I step over the line myself). During program
development this is probably harmless
although sometimes it can make for longer
compilations. Once the program stabilizes a bit
you’ll probably want to
go back and make functions non-inline where
appropriate.
To understand when inlining
is effective
it’s helpful to know what the
compiler does when it encounters an inline. As with any function
the compiler
holds the function type
(that is
the function prototype including the name and argument types
in
combination with the function return value) in its symbol table. In addition
when the compiler sees that the inline’s function type and the
function body parses without error
the code for the function body is also
brought into the symbol table. Whether the code is stored in source form
compiled assembly instructions
or some other representation is up to the
compiler.
When you make a call to an inline
function
the compiler first ensures that the call can be correctly made. That
is
all the argument types must either be the exact types in the
function’s argument list
or the compiler must be able to make a type
conversion to the proper types and the return value must be the correct type (or
convertible to the correct type) in the destination expression. This
of course
is exactly what the compiler does for any function and is markedly different
from what the preprocessor does because the preprocessor cannot check types or
make conversions.
If all the function type information fits
the context of the call
then the inline code is substituted directly for the
function call
eliminating the call overhead and allowing for further
optimizations by the compiler. Also
if the inline is a member function
the
address of the object (this) is put in the appropriate place(s)
which of
course is another action the preprocessor is unable to
perform.
There are two situations in which the
compiler cannot perform inlining. In these cases
it simply reverts to the
ordinary form of a function by taking the inline definition and creating storage
for the function just as it does for a non-inline. If it must do this in
multiple translation units (which would normally cause a multiple definition
error)
the linker is told to ignore the multiple definitions.
The compiler cannot perform inlining if
the function is too complicated. This depends upon the particular compiler
but
at the point most compilers give up
the inline probably wouldn’t gain you
any efficiency. In general
any sort of looping is considered too complicated to
expand as an inline
and if you think about it
looping probably entails much
more time inside the function than what is required for the function call
overhead. If the function is just a collection of simple statements
the
compiler probably won’t have any trouble inlining it
but if there are a
lot of statements
the overhead of the function call will be much less than the
cost of executing the body. And remember
every time you call a big inline
function
the entire function body is inserted in place of each call
so you can
easily get code bloat without
any noticeable performance improvement. (Note that some of the examples in this
book may exceed reasonable inline sizes in favor of conserving screen real
estate.)
The compiler also cannot perform inlining
if the address of the function
is taken implicitly or explicitly. If the compiler must produce an address
then
it will allocate storage for the function code and use the resulting address.
However
where an address is not required
the compiler will probably still
inline the code.
It is important to understand that an
inline is just a suggestion to the compiler; the compiler is not forced to
inline anything at all. A good compiler will inline small
simple functions
while intelligently ignoring inlines that are too complicated. This will give
you the results you want – the true semantics of a function call with the
efficiency of a macro.
If you’re imagining what the
compiler is doing to implement inlines
you can confuse yourself into thinking
there are more limitations than actually exist. In particular
if an inline
makes a forward reference
to a function that hasn’t yet been declared in the
class (whether that function is inline or not)
it can seem like the compiler
won’t be able to handle it:
//: C09:EvaluationOrder.cpp
// Inline evaluation order
class Forward {
int i;
public:
Forward() : i(0) {}
// Call to undeclared function:
int f() const { return g() + 1; }
int g() const { return i; }
};
int main() {
Forward frwd;
frwd.f();
} ///:~
In f( )
a call is made to
g( )
although g( ) has not yet been declared. This
works because the language definition states that no inline functions in a class
shall be evaluated until the closing brace of the class
declaration.
Of course
if g( ) in turn
called f( )
you’d end up with a set of recursive calls
which
are too complicated for the compiler to inline. (Also
you’d have to
perform some test in f( ) or g( ) to force one of them
to “bottom out
” or the recursion would be
infinite.)
Constructors
and destructors
are two places where you can be
fooled into thinking that an inline
is more efficient than it
actually is. Constructors and destructors may have hidden activities
because
the class can contain subobjects whose constructors and destructors must be
called. These subobjects may be member objects
or they may exist because of
inheritance (covered in Chapter 14). As an example of a class with member
objects:
//: C09:Hidden.cpp
// Hidden activities in inlines
#include <iostream>
using namespace std;
class Member {
int i
j
k;
public:
Member(int x = 0) : i(x)
j(x)
k(x) {}
~Member() { cout << "~Member" << endl; }
};
class WithMembers {
Member q
r
s; // Have constructors
int i;
public:
WithMembers(int ii) : i(ii) {} // Trivial?
~WithMembers() {
cout << "~WithMembers" << endl;
}
};
int main() {
WithMembers wm(1);
} ///:~
The constructor for Member is
simple enough to inline
since there’s nothing special going on – no
inheritance or member objects are causing extra hidden activities. But in
class WithMembers there’s more going on than meets the eye. The
constructors and destructors for the member objects q
r
and
s are being called automatically
and those constructors and
destructors are also inline
so the difference is significant from normal member
functions. This doesn’t necessarily mean that you should always make
constructor and destructor definitions non-inline; there are cases in which it
makes sense. Also
when you’re making an initial “sketch” of a
program by quickly writing code
it’s often more
convenient to use inlines. But if you’re concerned
about efficiency
it’s a place to
look.
In a book like this
the simplicity and
terseness of putting inline definitions inside classes is very useful because
more fits on a page or screen (in a seminar). However
Dan
Saks[46]
has pointed out that in a real project this has the effect of needlessly
cluttering the class interface and thereby making the class harder to use. He
refers to member functions defined within classes using the Latin in situ
(in place) and maintains that
all definitions should be placed outside the class to keep the interface clean.
Optimization
he argues
is a separate issue. If you want to optimize
use the
inline keyword. Using
this approach
the earlier Rectangle.cpp example
becomes:
//: C09:Noinsitu.cpp
// Removing in situ functions
class Rectangle {
int width
height;
public:
Rectangle(int w = 0
int h = 0);
int getWidth() const;
void setWidth(int w);
int getHeight() const;
void setHeight(int h);
};
inline Rectangle::Rectangle(int w
int h)
: width(w)
height(h) {}
inline int Rectangle::getWidth() const {
return width;
}
inline void Rectangle::setWidth(int w) {
width = w;
}
inline int Rectangle::getHeight() const {
return height;
}
inline void Rectangle::setHeight(int h) {
height = h;
}
int main() {
Rectangle r(19
47);
// Transpose width & height:
int iHeight = r.getHeight();
r.setHeight(r.getWidth());
r.setWidth(iHeight);
} ///:~
Now if you want to compare the effect of
inline functions to non-inline functions
you can simply remove the
inline keyword. (Inline functions should normally be put in header files
however
while non-inline functions must reside in their own translation unit.)
If you want to put the functions into documentation
it’s a simple
cut-and-paste operation. In situ functions require more work and have
greater potential for errors. Another argument for this approach is that you can
always produce a consistent formatting style for function definitions
something
that doesn’t always occur with in situ
functions.
Earlier
I said that you almost
always want to use inline functions instead of preprocessor macros. The
exceptions are when you need to use three special features in the C preprocessor
(which is also the C++ preprocessor):
stringizing
string
concatenation
and token
pasting. Stringizing
introduced
earlier in the book
is performed with the # directive and allows you to
take an identifier and turn it into a character array. String concatenation
takes place when two adjacent character arrays have no intervening punctuation
in which case they are combined. These two features are especially useful when
writing debug code. Thus
#define DEBUG(x) cout << #x " = " << x << endl
This prints the value of any variable.
You can also get a trace that prints out the statements as they
execute:
#define TRACE(s) cerr << #s << endl; s
The #s stringizes the statement
for output
and the second s reiterates the statement so it is executed.
Of course
this kind of thing can cause problems
especially in one-line
for loops:
for(int i = 0; i < 100; i++) TRACE(f(i));
Because there are actually two statements
in the TRACE( ) macro
the one-line for loop executes only
the first one. The solution is to replace the semicolon with a comma in the
macro.
Token pasting
implemented with the
## directive
is very useful when you are manufacturing code. It allows
you to take two identifiers and paste them together to automatically create a
new identifier. For example
#define FIELD(a) char* a##_string; int a##_size
class Record {
FIELD(one);
FIELD(two);
FIELD(three);
// ...
};
Each call to the FIELD( )
macro creates an identifier to hold a character array and another to hold the
length of that array. Not only is it easier to read
it can eliminate coding
errors and make maintenance easier.
The
require.h functions have
been used up to this point without defining them (although
assert( ) has also been used to help detect
programmer errors where it’s appropriate). Now it’s time to define
this header file. Inline
functions are convenient here because they allow everything to be placed in a
header file
which simplifies the process of using the package. You just include
the header file and you don’t need to worry about linking an
implementation file.
You should note that exceptions
(presented in detail in Volume 2 of this book) provide a much more effective way
of handling many kinds of errors – especially those that you’d like
to recover from – instead of just halting the program. The conditions that
require.h handles
however
are ones which prevent the continuation of
the program
such as if the user doesn’t provide enough command-line
arguments or if a file cannot be opened. Thus
it’s acceptable that they
call the Standard C Library function
exit( ).
The following header file is placed in
the book’s root directory so it’s easily accessed from all
chapters.
//: :require.h
// Test for error conditions in programs
// Local "using namespace std" for old compilers
#ifndef REQUIRE_H
#define REQUIRE_H
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <string>
inline void require(bool requirement
const std::string& msg = "Requirement failed"){
using namespace std;
if (!requirement) {
fputs(msg.c_str()
stderr);
fputs("\n"
stderr);
exit(1);
}
}
inline void requireArgs(int argc
int args
const std::string& msg =
"Must use %d arguments") {
using namespace std;
if (argc != args + 1) {
fprintf(stderr
msg.c_str()
args);
fputs("\n"
stderr);
exit(1);
}
}
inline void requireMinArgs(int argc
int minArgs
const std::string& msg =
"Must use at least %d arguments") {
using namespace std;
if(argc < minArgs + 1) {
fprintf(stderr
msg.c_str()
minArgs);
fputs("\n"
stderr);
exit(1);
}
}
inline void assure(std::ifstream& in
const std::string& filename = "") {
using namespace std;
if(!in) {
fprintf(stderr
"Could not open file %s\n"
filename.c_str());
exit(1);
}
}
inline void assure(std::ofstream& out
const std::string& filename = "") {
using namespace std;
if(!out) {
fprintf(stderr
"Could not open file %s\n"
filename.c_str());
exit(1);
}
}
#endif // REQUIRE_H ///:~
The default values provide reasonable
messages that can be changed if necessary.
You’ll notice that instead of using
char* arguments
const string& arguments are used. This allows
both char* and strings as arguments to these functions
and thus
is more generally useful (you may want to follow this form in your own
coding).
In the definitions for
requireArgs( ) and requireMinArgs( )
one is added to
the number of arguments you need on the command line because argc always
includes the name of the program being executed as argument zero
and so always
has a value that is one more than the number of actual arguments on the command
line.
Note the use of local “using
namespace std” declarations within each function. This is because some
compilers at the time of this writing incorrectly did not include the C standard
library functions in namespace std
so explicit
qualification would cause a compile-time error. The local declaration allows
require.h to work with both correct and incorrect libraries without
opening up the namespace std for anyone who includes this header
file.
Here’s a simple program to test
require.h:
//: C09:ErrTest.cpp
//{T} ErrTest.cpp
// Testing require.h
#include "../require.h"
#include <fstream>
using namespace std;
int main(int argc
char* argv[]) {
int i = 1;
require(i
"value must be nonzero");
requireArgs(argc
1);
requireMinArgs(argc
1);
ifstream in(argv[1]);
assure(in
argv[1]); // Use the file name
ifstream nofile("nofile.xxx");
// Fails:
//! assure(nofile); // The default argument
ofstream out("tmp.txt");
assure(out);
} ///:~
#define IFOPEN(VAR NAME) \ ifstream VAR(NAME); \ assure(VAR NAME);
Which could then be used like
this:
IFOPEN(in argv[1])
At first
this might seem appealing since
it means there’s less to type. It’s not terribly unsafe
but
it’s a road best avoided. Note that
once again
a macro looks like a
function but behaves differently; it’s actually creating an object
(in) whose scope persists beyond the macro. You may understand this
but
for new programmers and code maintainers it’s just one more thing they
have to puzzle out. C++ is complicated enough without adding to the confusion
so try to talk yourself out of using preprocessor macros whenever you
can.
It’s critical that you be able to
hide the underlying implementation of a class because you may want to change
that implementation sometime later. You’ll make these changes for
efficiency
or because you get a better understanding of the problem
or because
some new class becomes available that you want to use in the implementation.
Anything that jeopardizes the privacy of the underlying implementation reduces
the flexibility of the language. Thus
the inline function is very important
because it virtually eliminates the need for preprocessor macros and their
attendant problems. With inlines
member functions can be as efficient as
preprocessor macros.
The inline function can be overused in
class definitions
of course. The programmer is tempted to do so because
it’s easier
so it will happen. However
it’s not that big of an
issue because later
when looking for size reductions
you can always change the
functions to non-inlines with no effect on their functionality. The development
guideline should be “First make it work
then optimize
it.”
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.
[45]Andrew
Koenig goes into more detail in his book C Traps & Pitfalls
(Addison-Wesley
1989).
[46]
Co-author with Tom Plum of C++ Programming Guidelines
Plum Hall
1991.