Inapoi
Inainte
Cuprins
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.
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)
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)) // ...
=if(a&0x0f>=0x07?0:1)
The precedence of & is lower
than that of >=, so the macro evaluation will surprise you. Once you
discover the problem, you can solve it by putting parentheses around everything
in the macro definition. (This is a good practice to use when creating
preprocessor macros.) Thus,
=#define FLOOR(x,b) ((x)>=(b)?0:1)
Discovering the problem may be difficult,
however, and you may not find it until after you’ve taken the proper macro
behavior for granted. In the un-parenthesized version of the preceding macro,
most expressions will work correctly because the precedence of
>= is lower than most of the operators like +, /, –
–, and even the bitwise shift operators. So you can easily begin to
think that it works with all expressions, including those using bitwise logical
operators.
The preceding problem can be solved with
careful programming practice: parenthesize everything in a macro. However, the
second difficulty is subtler. Unlike a normal function, every time you use an
argument in a macro, that
argument is evaluated. As long as the macro is called only with ordinary
variables, this evaluation is benign, but if the evaluation of an argument has
side effects, then the results can be surprising and will definitely not mimic
function behavior.
For example, this macro determines
whether its argument falls within a certain range:
=#define BAND(x) (((x)>5 && (x)<10) ? (x) : 0)
As long as you use an
“ordinary” argument, the macro works very much like a real function.
But as soon as you relax and start believing it is a real function, the
problems start. Thus:
=//: C09:MacroSideEffects.cpp
#include "../require.h"
#include <fstream>
using namespace std;
#define BAND(x) (((x)>5 && (x)<10) ? (x) : 0)
int main() {
ofstream out("macro.out");
assure(out, "macro.out");
for(int i = 4; i < 11; i++) {
int a = i;
out << "a = " << a << endl << '\t';
out << "BAND(++a)=" << BAND(++a) << endl;
out << "\t a = " << a << endl;
}
} ///:~
Notice the use of all upper-case
characters in the name of the macro. This is a helpful practice because it tells
the reader this is a macro and not a function, so if there are problems, it acts
as a little reminder.
Here’s the output produced by the
program, which is not at all what you would have expected from a true
function:
=a = 4
BAND(++a)=0
a = 5
a = 5
BAND(++a)=8
a = 8
a = 6
BAND(++a)=9
a = 9
a = 7
BAND(++a)=10
a = 10
a = 8
BAND(++a)=0
a = 10
a = 9
BAND(++a)=0
a = 11
a = 10
BAND(++a)=0
a = 12
When a is four, only the first
part of the conditional occurs, so the expression is evaluated only once, and
the side effect of the macro call is that a becomes five, which is what
you would expect from a normal function call in the same situation. However,
when the number is within the band, both conditionals are tested, which results
in two increments. The result is produced by evaluating the argument again,
which results in a third increment. Once the number gets out of the band, both
conditionals are still tested so you get two increments. The side effects are
different, depending on the argument.
This is clearly not the kind of behavior
you want from a macro that looks like a function call. In this case, the obvious
solution is to make it a true function, which of course adds the extra overhead
and may reduce efficiency if you call that function a lot. Unfortunately, the
problem may not always be so obvious, and you can unknowingly get a library that
contains functions and macros mixed together, so a problem like this can hide
some very difficult-to-find bugs. For example, the
putc( ) macro in cstdio may evaluate
its second argument twice. This is specified in Standard C. Also, careless
implementations of toupper( ) as a macro may
evaluate the argument more than once, which will give you unexpected results
with
toupper(*p++).[45]
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
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; }
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 {
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 part of the state of an object
– that is, an internal variable or variables. The reason inlines are so
important for access functions can be seen in the following
example:
=//: C09:Access.cpp
// Inline access functions
class Access {
int i;
public:
int read() const { return i; }
void set(int ii) { i = ii; }
};
int main() {
Access A;
A.set(100);
int x = A.read();
} ///:~
Here, the class user never has direct
contact with the state variables inside the class, and they can be kept
private, under the
control of the class designer. All the access to the private data members
can be controlled through the member function interface. In addition, access is
remarkably efficient. Consider the read( ), for example. Without
inlines, the code generated for the call to read( ) would typically
include pushing this on
the stack and making an assembly language CALL. With most machines, the size of
this code would be larger than the code created by the inline, and the execution
time would certainly be longer.
Without inline functions, an
efficiency-conscious class designer will be tempted to simply make i a
public member, eliminating the overhead by allowing the user to directly access
i. From a design standpoint, this is disastrous
because i then becomes part of the public interface, which means the
class designer can never change it. You’re stuck with an int called
i. This is a problem because you may learn sometime later that it would
be much more useful to represent the state information as a float rather
than an int, but because int i is part of the public interface,
you can’t change it. Or you may want to perform some additional
calculation as part of reading or setting i, which you can’t do if
it’s public. If, on the other hand, you’ve always used
member functions to read and change the state information of an object, you can
modify the underlying representation of the object to your heart’s
content.
In addition, the use of member functions
to control data members allows you to add code to the member function to detect
when that data is being changed, which can be very useful during debugging. If a
data member is public, anyone can change it anytime without you knowing
about it.
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());
} ///:~
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:
=//: C09:Cpptime.h
// A simple time class
#ifndef CPPTIME_H
#define CPPTIME_H
#include <ctime>
#include <cstring>
class Time {
std::time_t t;
std::tm local;
char asciiRep[26];
unsigned char lflag, aflag;
void updateLocal() {
if(!lflag) {
local = *std::localtime(&t);
lflag++;
}
}
void updateAscii() {
if(!aflag) {
updateLocal();
std::strcpy(asciiRep,std::asctime(&local));
aflag++;
}
}
public:
Time() { mark(); }
void mark() {
lflag = aflag = 0;
std::time(&t);
}
const char* ascii() {
updateAscii();
return asciiRep;
}
// Difference in seconds:
int delta(Time* dt) const {
return int(std::difftime(t, dt->t));
}
int daylightSavings() {
updateLocal();
return local.tm_isdst;
}
int dayOfYear() { // Since January 1
updateLocal();
return local.tm_yday;
}
int dayOfWeek() { // Since Sunday
updateLocal();
return local.tm_wday;
}
int since1900() { // Years since 1900
updateLocal();
return local.tm_year;
}
int month() { // Since January
updateLocal();
return local.tm_mon;
}
int dayOfMonth() {
updateLocal();
return local.tm_mday;
}
int hour() { // Since midnight, 24-hour clock
updateLocal();
return local.tm_hour;
}
int minute() {
updateLocal();
return local.tm_min;
}
int second() {
updateLocal();
return local.tm_sec;
}
};
#endif // CPPTIME_H ///:~
The Standard C library
functions have multiple representations for time, and
these are all part of the Time class. However, it isn’t necessary
to update all of them, so instead the time_t t is
used as the base representation, and the tm local and ASCII character
representation asciiRep each have flags to indicate if they’ve been
updated to the current time_t. The two private functions
updateLocal( ) and updateAscii( ) check the flags and
conditionally perform the update.
The constructor calls the
mark( ) function (which the user can also call to force the object
to represent the current time), and this clears the two flags to indicate that
the local time and ASCII representation are now invalid. The
ascii( ) function calls updateAscii( ), which copies the
result of the Standard C library function
asctime( ) into a local buffer because
asctime( ) uses a static data area that is overwritten if the
function is called elsewhere. The ascii( ) function return value is
the address of this local buffer.
All the functions starting with
daylightSavings( ) use the updateLocal( ) function,
which causes the resulting composite inlines to be fairly large. This
doesn’t seem worthwhile, especially considering you probably won’t
call the functions very much. However, this doesn’t mean all the functions
should be made non-inline. If you make other functions non-inline, at least keep
updateLocal( ) inline so that its code will be duplicated in the
non-inline functions, eliminating extra function-call overhead.
Here’s a small test
program:
=//: C09:Cpptime.cpp
// Testing a simple time class
#include "Cpptime.h"
#include <iostream>
using namespace std;
int main() {
Time start;
for(int i = 1; i < 1000; i++) {
cout << i << ' ';
if(i%10 == 0) cout << endl;
}
Time end;
cout << endl;
cout << "start = " << start.ascii();
cout << "end = " << end.ascii();
cout << "delta = " << end.delta(&start);
} ///:~
A Time object is created, then
some time-consuming activity is performed, then a second Time object is
created to mark the ending time. These are used to show starting, ending, and
elapsed
times.
 |
|