Am avut 371701 vizite de la lansarea siteului.




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)) // ...

the macro will expand to

=
if(a&0x0f>=0x07?0:1)

The precedence of & is lower than that of >=, so the macro evaluation will surprise you. Once you discover the problem, you can solve it by putting parentheses around everything in the macro definition. (This is a good practice to use when creating preprocessor macros.) Thus,

=
#define FLOOR(x,b) ((x)>=(b)?0:1)

Discovering the problem may be difficult, however, and you may not find it until after you’ve taken the proper macro behavior for granted. In the un-parenthesized version of the preceding macro, most expressions will work correctly because the precedence of >= is lower than most of the operators like +, /, – –, and even the bitwise shift operators. So you can easily begin to think that it works with all expressions, including those using bitwise logical operators.

The preceding problem can be solved with careful programming practice: parenthesize everything in a macro. However, the second difficulty is subtler. Unlike a normal function, every time you use an argument in a macro, that argument is evaluated. As long as the macro is called only with ordinary variables, this evaluation is benign, but if the evaluation of an argument has side effects, then the results can be surprising and will definitely not mimic function behavior.

For example, this macro determines whether its argument falls within a certain range:

=
#define BAND(x) (((x)>5 && (x)<10) ? (x) : 0)

As long as you use an “ordinary” argument, the macro works very much like a real function. But as soon as you relax and start believing it is a real function, the problems start. Thus:

=
//: C09:MacroSideEffects.cpp
#include "../require.h"
#include <fstream>
using namespace std;

#define BAND(x) (((x)>5 && (x)<10) ? (x) : 0)

int main() {
  ofstream out("macro.out");
  assure(out, "macro.out");
  for(int i = 4; i < 11; i++) {
    int a = i;
    out << "a = " << a << endl << '\t';
    out << "BAND(++a)=" << BAND(++a) << endl;
    out << "\t a = " << a << endl;
  }
} ///:~

Notice the use of all upper-case characters in the name of the macro. This is a helpful practice because it tells the reader this is a macro and not a function, so if there are problems, it acts as a little reminder.

Here’s the output produced by the program, which is not at all what you would have expected from a true function:

=
a = 4
  BAND(++a)=0
   a = 5
a = 5
  BAND(++a)=8
   a = 8
a = 6
  BAND(++a)=9
   a = 9
a = 7
  BAND(++a)=10
   a = 10
a = 8
  BAND(++a)=0
   a = 10
a = 9
  BAND(++a)=0
   a = 11
a = 10
  BAND(++a)=0
   a = 12

When a is four, only the first part of the conditional occurs, so the expression is evaluated only once, and the side effect of the macro call is that a becomes five, which is what you would expect from a normal function call in the same situation. However, when the number is within the band, both conditionals are tested, which results in two increments. The result is produced by evaluating the argument again, which results in a third increment. Once the number gets out of the band, both conditionals are still tested so you get two increments. The side effects are different, depending on the argument.

This is clearly not the kind of behavior you want from a macro that looks like a function call. In this case, the obvious solution is to make it a true function, which of course adds the extra overhead and may reduce efficiency if you call that function a lot. Unfortunately, the problem may not always be so obvious, and you can unknowingly get a library that contains functions and macros mixed together, so a problem like this can hide some very difficult-to-find bugs. For example, the putc( ) macro in cstdio may evaluate its second argument twice. This is specified in Standard C. Also, careless implementations of toupper( ) as a macro may evaluate the argument more than once, which will give you unexpected results with toupper(*p++).[45]

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.

Home   |   Web Faq   |   Radio Online   |   About   |   Products   |   Webmaster Login

The quality software developer.™
© 2003-2004 ruben|labs corp. All Rights Reserved.
Timp de generare a paginii: 17583 secunde
Versiune site: 1.8 SP3 (build 2305-rtm.88542-10.2004)