ZeePedia

Inline Functions:Preprocessor pitfalls, Stash & Stack with inlines, Reducing clutter

<< Constants:Function arguments & return values, Classes, volatile
Name Control:Static elements from C, Static initialization dependency, specifications >>
img
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
img
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++
img
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
img
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++
img
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
img
.
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++
img
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
img
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++
img
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
img
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++
img
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
img
} ///:~
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++
img
//: 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
img
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++
img
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
img
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++
img
};
#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
img
//: 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++
img
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
img
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++
img
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
img
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++
img
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
img
// 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++
img
.
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
or screen (in a seminar). However, Dan Saks2 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.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
img
}
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++
img
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
img
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++
img
}
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
img
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++
img
//!  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
img
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++
img
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
img
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++