|
|||||
9:
Inline Functions
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.
393
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.
Preprocessor
pitfalls
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)
394
Thinking
in C++
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
9:
Inline Functions
395
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)
396
Thinking
in C++
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
9:
Inline Functions
397
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++)1
.
Macros
and access
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
1Andrew Koenig goes
into more detail in his
book C
Traps & Pitfalls (Addison-
Wesley,
1989).
398
Thinking
in C++
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.
Inline
functions
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; }
9:
Inline Functions
399
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).
Inlines
inside classes
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 {
400
Thinking
in C++
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).
Access
functions
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
9:
Inline Functions
401
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
402
Thinking
in C++
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.
Accessors
and mutators
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());
9:
Inline Functions
403
}
///:~
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.
You
may choose instead to use "get" and
"set" to indicate
accessors
and
mutators:
//:
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:
404
Thinking
in C++
//:
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();
9:
Inline Functions
405
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
tis
used as
the
base representation, and the tm
localand ASCII
character
representation
asciiRepeach
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.
406
Thinking
in C++
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(
) the
use
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);
}
///:~
9:
Inline Functions
407
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.
Stash
& Stack with inlines
Armed
with inlines, we can now convert
the Stash
and
Stack
classes
to be more efficient:
//:
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;
}
408
Thinking
in C++
};
#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:
9:
Inline Functions
409
//:
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.
The
Stack
class
makes even better use of
inlines:
//:
C09:Stack4.h
//
With inlines
#ifndef
STACK4_H
#define
STACK4_H
#include
"../require.h"
class
Stack {
struct
Link {
410
Thinking
in C++
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
oldHeadsimply
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.
9:
Inline Functions
411
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.
Inlines
& the compiler
To
understand when inlining is effective,
it's helpful to know what
the
compiler does when it encounters an
inline. As with any
412
Thinking
in C++
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.
Limitations
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.
9:
Inline Functions
413
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.
Forward
references
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
414
Thinking
in C++
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.)
Hidden
activities in constructors & destructors
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
9:
Inline Functions
415
//
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.
416
Thinking
in C++
Reducing
clutter
In
a book like this, the
simplicity and terseness of putting
inline
definitions
inside classes is very useful because
more fits on a page
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.cppexample
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;
2
Co-author with Tom
Plum of C++
Programming Guidelines, Plum
Hall, 1991.
9:
Inline Functions
417
}
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.
More
preprocessor features
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
418
Thinking
in C++
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
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.
9:
Inline Functions
419
Improved
error checking
The
require.hfunctions
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);
}
420
Thinking
in C++
}
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 ///:~
9:
Inline Functions
421
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.hto
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:
422
Thinking
in C++
//!
assure(nofile); // The default
argument
ofstream
out("tmp.txt");
assure(out);
}
///:~
You
might be tempted to go one step further
for opening files and
add
a macro to require.h
:
#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.
Summary
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.
9:
Inline Functions
423
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."
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 .
1.
Write
a program that uses the
F(
) macro
shown at the
beginning
of the chapter and demonstrates
that it does
not
expand properly, as described in
the text. Repair
the
macro
and show that it works correctly.
2.
Write
a program that uses the
FLOOR(
)macro
shown at
the
beginning of the chapter.
Show the conditions
under
which
it does not work properly.
3.
Modify
MacroSideEffects.cpp
that
BAND(
) works
so
properly.
4.
Create
two identical functions, f1(
) and
f2(
).
Inline f1(
)
and
leave f2(
) as
an non-inline function. Use
the
Standard
C Library function clock(
)that
is found in
<ctime>
to
mark the starting point and
ending points
and
compare the two functions to
see which one is
faster.
You
may need to make repeated
calls to the
functions
inside
your timing loop in order to get
useful numbers.
5.
Experiment
with the size and complexity of
the code
inside
the functions in Exercise 4 to
see if you can find a
break-even
point where the inline
function and the non-
inline
function take the same
amount of time. If you have
them
available, try this with different
compilers and note
the
differences.
6.
Prove
that inline functions
default to internal
linkage.
424
Thinking
in C++
7.
Create
a class that contains an
array of char.
Add an
inline
constructor that uses the
Standard C library
function
memset(
)to
initialize the array to
the
constructor
argument (default this to `
'), and an inline
member
function called print(
)to
print out all the
characters
in the array.
8.
Take
the NestFriend.cppexample
from Chapter 5 and
replace
all the member functions with
inlines. Make them
non-in
situ inline
functions. Also change the
initialize(
)
functions
to constructors.
9.
Modify
StringStack.cppfrom
Chapter 8 to use
inline
functions.
10.
Create
an enum
called
Hue
containing
red,
blue and
,
yellow.
Now create a class called
Color
containing
a data
member
of type Hue
and
a constructor that sets the
Hue
from
its argument. Add access
functions to "get" and
"set"
the Hue.
Make all of the functions
inlines.
11.
Modify
Exercise 10 to use the
"accessor" and "mutator"
approach.
12.
Modify
Cpptime.cppso
that it measures the time
from
the
time that the program
begins running to the
time
when
the user presses the
"Enter" or "Return" key.
13.
Create
a class with two inline member
functions, such
that
the first function that's
defined in the class calls
the
second
function, without the need for a
forward
declaration.
Write a main that creates an
object of the
class
and calls the first
function.
14.
Create
a class A
with
an inline default constructor
that
announces
itself. Now make a new class
B
and
put an
object
of A
as
a member of B,
and give B
an
inline
constructor.
Create an array of B
objects
and see what
happens.
15.
Create
a large quantity of the
objects from the
previous
Exercise,
and use the Time
class
to time the
difference
9:
Inline Functions
425
between
non-inline constructors and inline
constructors.
(If
you have a profiler, also try
using that.)
16.
Write
a program that takes a
string
as
the command-line
argument.
Write a for
loop
that removes one
character
from
the string
with
each pass, and use the
DEBUG(
)
macro
from this chapter to print the
string
each
time.
17.
Correct
the TRACE(
)macro
as specified in this
chapter,
and
prove that it works
correctly.
18.
Modify
the FIELD(
)macro
so that it also contains
an
index
number.
Create a class whose members
are
composed
of calls to the FIELD(
)macro.
Add a member
function
that allows you to look up a
field using its
index
number.
Write a main(
) to
test the class.
19.
Modify
the FIELD(
)macro
so that it automatically
generates
access functions for each
field (the data
should
still
be private, however). Create a
class whose members
are
composed of calls to the
FIELD(
)macro.
Write a
main(
) to
test the class.
20.
Write
a program that takes two
command-line
arguments:
the first is an int
and
the second is a file
name.
Use require.hto
ensure that you have the
right
number
of arguments, that the
int
is
between 5 and 10,
and
that the file can
successfully be opened.
21.
Write
a program that uses the
IFOPEN(
)macro
to open
a
file as an input stream. Note
the creation of the
ifstream
object
and its scope.
22.
(Challenging)
Determine how to get your compiler
to
generate
assembly code. Create a file
containing a very
small
function and a main(
) that
calls the function.
Generate
assembly code when the
function is inlined and
not
inlined, and demonstrate that
the inlined version
does
not have the function call
overhead.
426
Thinking
in C++
Table of Contents:
|
|||||