Functions as Class Methods

This section looks at functions which are part of classes—that is, class methods. Classes are a concept of C++ and cannot be used in C; consequently this section will probably not be of great interest to C-only programmers.

Classes make application design and code reuse easier by allowing more intuitive and orderly grouping of source code functionality into reusable objects. However, with the added ease of use come also additional performance and footprint considerations. These considerations are different depending on the way in which class methods are actually used. This section looks in detail at the implications of different techniques of using class methods and provides guidelines on when and where to use which technique.

Inheritance

Inheritance can be a very good way of safely reusing existing functionality. Typically, inheritance is used where related objects share some common functionality. Consider two objects that retrieve information from the Internet. The first object retrieves pictures to display on screen, and the second retrieves music to play over audio speakers. The data manipulation done by these objects will be quite different but they have some common functionality: Internet access. By moving this common functionality into a base class, it can be used in both objects but the actual code for Internet access needs to be written, tested, and, most importantly, maintained, only in one place. This is a great advantage where development time is concerned. The design becomes more transparent and the problems tackled by the base class need to be solved only once. Moreover, when a bug is found and fixed, there is no danger of overlooking a similar bug in similar functionality. Listing 8.17 shows a class with which we will demonstrate different ways of using inherited functionality.

Code Listing 8.17. Base Class Definition of an Example Class
#define unknownOS  0
#define OS2        1
#define WINDOWS    2

#define LINUX      3


class ComputerUser
{
public:

  // constructor
  ComputerUser()
  {
    favouriteOS = unknownOS;
  }

  ComputerUser(char *n, int OS)
  {
    strcpy(name, n);
    favouriteOS = OS;
  }

  // destructor
  ~ComputerUser()
  {
  }

  // interface
  void SetOS(int OS)
  {
    favouriteOS = OS;
  }

  void SetName(const char *s)
  {
    strcpy(name, s);
  }

private:

  // data
  int favouriteOS;
  char 
							
							
							
							name[50];
} ;

The ComputerUser class will be our base class for storing information about a computer user. Each computer user can have a name and a favorite OS. The class implementation is pretty straightforward, but note that the constructor is overloaded. When a computer user's name and favorite OS are known at construction time, this information will be initialized immediately.

Now let's assume that for a specific type of computer user, you want to store more information. For instance, of computer users who have access to the Internet you want to know what their favorite browser is. Not all computer users have Internet access but all people with access to the Internet use a computer; therefore, it is possible to derive an Internetter from a ComputerUser. Listing 8.18 shows how this can be done.

Code Listing 8.18. Deriving from the Example Base Class
#define unknownBROWSER  0
#define NETSCAPE        1
#define EXPLORER        2

class Internetter: public ComputerUser
{
public:

  // constructor
  Internetter()
  {
     favouriteBrowser = unknownBROWSER;
  }

  Internetter(char *n, int OS, int browser)
  {
    SetName(n);
    SetOS(OS);
    favouriteBrowser
							
							
							 = browser;
  }

  // destructor
  ~Internetter()
  { }

  // interface
  void SetBrowser(int browser)
  {
    favouriteBrowser = browser;
  }

private:

  // data
  int
							
							
							
							 favouriteBrowser;
} ;

The Internetter class also has an overloaded constructor for passing initialization data. Because Internetter is a derived class, it needs to initialize not only its own data but also that of its base class. This is can be seen in its second constructor.

Now that you have seen an example of a base class and a derived class, let's look at some different techniques of using their member functions.

Inefficient Object Initialization

One way of creating an instance of the Internetter class could be

Internetter *p = new Internetter;

p->SetName("John");	
p->SetOS(LINUX);
p->SetBrowser(NETSCAPE);
.........
delete p;

Though this construction of an Internetter is functionally correct, it is far from efficient. In fact this example of class method use would not have been shown were it not for the fact that it is used in the field often in some form or other. What happens in this example is that a class is first instantiated, after which (three) separate methods are called to initialize it. This can be done sometimes because initializing data needs to be retrieved from different places, but it would be more efficient to gather the data first and then create the class instantiation with a single constructor call:

Internetter *p = new Internetter("John", LINUX, NETSCAPE);

Inefficient Base Class Initialization

More inefficiencies lurk in Listings 8.17 and 8.18. To find them, you have to take a closer look at what happens when you create an Internetter instantiation. To create an Internetter , you first need to create a ComputerUser; the constructor of the ComputerUser is thus called. Similarly, the destructor of the ComputerUser will be called when the Internetter is deleted; however, as constructors and destructors are called in a stacklike fashion, first the Internetter destructor is called and then the ComputerUser destructor. As the Internetter constructors do not explicitly call any ComputerUser constructor, the default constructor is used. Again several method calls are needed to initialize the class. In this case, two calls are made to ComputerUser, namely SetName() and SetOS(). It would be better to have the Internetter class explicitly use the ComputerUser constructor, which allows member initialization. Listing 8.19 shows such a class.

Code Listing 8.19. Better Use of Base Class Construction
class Internetter2: public ComputerUser
{
public:
  // constructor
  Internetter2()
  {
    favouriteBrowser = unknownBROWSER;
  }

  Internetter2(char *n, int OS, int browser)
  : ComputerUser(n, OS)             // base class construction.
  {
    favouriteBrowser = browser;
  }

  // destructor
  ~Internetter2()
  {
  }

  // interface
  void SetBrowser(int browser)
  {
    favouriteBrowser = browser;
  }

private:

  // data
  int
								
								
								
								
								
								
								
								
								
								
								 favouriteBrowser;
} ;

Without Inheritance Overhead

Before looking at actual timing data of the different techniques, you should examine what happens when you do not use inheritance. Certainly in this example, where so few members are found in the base class, it is worth considering coding the base class functionality directly into the derived class. For objects of which the use is time critical (which are used/accessed often during search actions and so on), it might prove to be a good idea to do just this. Listing 8.20 shows how Internetter functionality can be combined with ComputerUser functionality.

Code Listing 8.20. Not Using Inheritance
class ExtendedComputerUser
{
public:

  // constructor
  ExtendedComputerUser()
  {
    favouriteOS = unknownOS;
  }

  ExtendedComputerUser(char *n, int OS, int browser)
  {
    strcpy(name, n);
    favouriteOS = OS;
    favouriteBrowser = browser;
  }

  // destructor
  ~ExtendedComputerUser()   { }

  // interface
  void SetOS(int OS)
  {
      favouriteOS
								
								
								 = OS;
  }

  void SetBrowser(int browser)
  {
      favouriteBrowser = browser;
  }

  void SetName(const char *s)
  {
      strcpy(name, s);
  }

private:

    // data
  int favouriteOS;
  int favouriteBrowser;
  char
								
								
								
								 name[40];
} ;

Timing Data

The timing data for Table 8.2 was gathered using a program which can be found in the file 08Source01.cpp on the Web site. The result of the program is a list of relative numbers indicating the execution speed of the various object construction methods described in this section:

Table 8.2. Timing Data on Different Uses of Inheritance
3200 Inefficient initialization of Internetter (1)
3180 Efficient initialization of Internetter (2)
2480 Efficient initialization of base class by Internetter2 (3)
2150 Without inheritance overhead by ExpandedComputerUser (4)

Note that the first two results are quite close; this is because the overhead incurred in initializing the derived class in test 1 is still incurred in initializing the base class in test 2. Test 3 initializes both the derived class and the base class more efficiently and is noticeably faster. Not using inheritance at all is, unsurprisingly, the fastest method of the four. It should be noted also that slowdown increases as more levels of inheritance are added. Each time a class is derived from another, a layer of constructor and destructor calls is added. However, when constructors and other member functions are actually inlined by the compiler the overhead of derived constructors is, of course, negated.

Summarizing, it can be said that for time-critical classes the following questions should be asked concerning the use of inheritance:

  • Is using inheritance actually necessary?

  • What is the actual price to be paid when avoiding inheritance (not only where application footprint is concerned but also in terms of application maintainability)?

  • When inheritance is used, is it used as efficiently as possible?

Virtual Functions

Before looking at the technical implications of using virtual functions, a small set of samples will demonstrate what exactly virtual functions are used for.

To the classes of Listing 8.19 is added another class describing Internetters who also happen to be multiplayer gamers. As not all Internetters are multiplayer gamers and certainly not all computer users are multiplayer gamers, the MultiPlayer class will be a derivative of the Internetter2 class. Of a multiplayer gamer we are interested in his or her favorite game and the nickname used when playing. Listing 8.21 shows what the MultiPlayer class looks like.

Code Listing 8.21. MultiPlayer Class
#define unknownGAME     0
#define QUAKE       1
#define DESCENT     2
#define HALFLIFE    3

class MultiPlayer: public Internetter2
{
public:

      // constructor
    MultiPlayer()
    {
        favouriteGame = unknownGAME;
    } ;

    MultiPlayer(char *n, int OS, int browser, int game, char *nName)
    : Internetter2(n, OS, browser)
    {
        favouriteGame = game;
        strcpy(nickName, nName);
    }

      // destructor
    ~MultiPlayer()
    { }

      // interface
    void SetGame(int game)
    {
        favouriteGame = game;
    }

private:

      // data
    char nickName[50];
    int
							
							
							 favouriteGame;
} ;

Now let's assume you have a group of computer users of which you want to know the names. To this end you can add a PrintName function to the base class ComputerUser

void ComputerUser::PrintName() {  cout << name << endl;}

and create an array of ComputerUsers:

ComputerUser *p[4];
p[0] = new ComputerUser("John", WINDOWS);
p[1] = new Internetter("Jane", LINUX, NETSCAPE);
p[2] = new Internetter2("Gary", LINUX, EXPLORER);
p[3] = new MultiPlayer("Joe", LINUX, NETSCAPE, QUAKE, "MightyJoe");

The following fragment of code will print the names of the ComputerUsers in the array:

for (int i =0; i < 4; i++)
    p[i]->PrintName();

However, multiplayers decide they do not like this approach one bit and demand to be addressed by their nicknames. This implies creating a separate PrintName function for the MultiPlayer class which will print the nickname instead of the computer user name:

void MultiPlayer::PrintName() {  cout << nickName << endl;}

Sadly, the print loop does not recognize this function. It takes pointers to ComputerUsers and calls only their print methods. The compiler needs to be told that we are thinking about overriding certain base class functions with a different implementation. This is done with the keyword virtual. Listing 8.22 shows the new and improved base class, which allows us to override the print method.

Code Listing 8.22. Virtual PrintName Function
class ComputerUser
{
public:

  // constructor
  ComputerUser()
  {
    favouriteOS = unknownOS;
  }

  ComputerUser(char *n, int OS)
  {
    strcpy(name, n);
    favouriteOS = OS;
  }

  // destructor
  ~ComputerUser()
  {

  }

  // interface
  void SetOS(int OS)
  {
    favouriteOS = OS;
  }

  void SetName(const char *s)
  {
    strncpy(name, s, 40);
  }

  virtual void PrintName()
  {
    cout << name << endl;
  }

private:

  // data
  int favouriteOS;
  char 
							
							
							name[50];
} ;

Now when you run the printing loop once more, the multiplayer gamer will be identified by his nickname.

By adding the keyword virtual you tell the compiler that you want to use something called late binding for a specific function. This means that it is no longer known at compile time which function will actually be called at runtime. Depending on the kind of class being dealt with, a certain function will be called. Note that

  • By specifying a function as virtual in the base class, it is automatically virtual for all derived classes.

  • When calling a virtual function via a pointer to a class instantiation, the implementation that is used is from the latest derived class that implements the function for that class instantiation.

The second bullet describes something called polymorphism . The example print loop uses pointers to ComputerUsers. When it becomes time to print the name of the Joe instantiation of MultiPlayer, it takes the PrintName() implemented by Multiplayer. This means that at runtime the program can see it is handling a MultiPlayer and not just a normal ComputerUser and calls the appropriate function implementation.

So how does this actually work? Two things are done at compile time to allow the program to make a runtime decision about which implementation of a virtual function to actually call. First, virtual tables (VT) and virtual table pointers (VPTR) are added to the generated code. Second, code to use the VTs and VPTRs at runtime is added.

Virtual Table (VT)

For every class that is defined and that either contains a virtual function or inherits a virtual function, a VT is created. A VT contains pointers to function implementations. For each virtual function there will be exactly one corresponding pointer. This pointer points to the implementation that is valid for that class.

In our previous example classes, the VTs of ComputerUser , Internetter , Internetter2 , and Multiplayer all contain a single function pointer that points to an implementation of PrintName . As neither Internetter nor Internetter2 define their own PrintName() , their VT entry will point to the base class implementation (ComputerUser::PrintName() ). MultiPlayer does have its own PrintName() function and thus its VT entry points to a different function address (MultiPlayer::PrintName() ).

The footprint implications are clear: The more virtual functions are defined within a class, the larger become the VTs of all derived classes. Note also that derived classes can define additional virtual functions; by doing this they again increase VT size for their own derivatives. However, when different classes have the same VT (as Internetter and Internetter2 do in the examples) the compiler can decide to create only a single VT to be shared by the classes.

Virtual Table Pointer (VPTR)

When an instantiation of a class is made, it needs to be able to access the VT defined for its class. To this end, an extra member is added to the class which points to the VT. This member is called the virtual table pointer.

For example, the instantiation Joe contains a VPTR that points to the VT of Multiplayer . Through it, it can access the PrintName() function of Multiplayer . Similarly, Jane, an Internetter , contains a VPTR that points to the VT of Internetter , which in turn contains a pointer to ComputerUser::PrintName() .

The footprint implications are clear: To every instantiation made of a class that has a virtual table, an extra member is added—that is, a pointer that is automatically initiated with the address of the VT of the class at construction time. Figure 8.2 shows how virtual functions are called.

Note that Figure 8.2 depicts a possible VTable implementation. Depending on which compiler is used, the actual implementation may differ somewhat.

  • Code to use the virtual table and virtual table pointer at runtime.

Figure 8.2. Calling virtual functions.


To call a virtual function at runtime, the VPTR of the instantiation needs to be dereferenced to find the class VT. Within this VT an index is used to find the pointer to the actual function implementation. This is where performance implications become clear. The following piece of pseudo-code shows how the MultiPlayer MightyJoe has his PrintName() function called:

// get vptr from class instance.
vtable *vptr = p[3]->vptr;

// get function pointer from vtable.
void (*functionPtr)() = vptr[PrintNameIndex];

// call the function.
functionPtr();

This may look like quite a bit of overhead but depending on the system used, it could easily translate into two or three machine instructions, which makes the virtual function overhead as small as the overhead incurred when you add an extra parameter of a base type (int for example) to a function call.

A program for testing calling overhead is included in the file 08Source02.cpp that can be found on the Web site. The result of the program is a list of relative timing values for

  • Calling a non-virtual member function

  • Calling a virtual member function

  • Calling a non-virtual member function with an extra parameter

  • Calling a member function from a base class via a derived class

  • Calling a non-virtual member function via a pointer

  • Calling a global function

Templates

Templates are used to describe a piece of functionality in a generic way. There are class templates and function templates. With function templates you can generically describe a function without identifying the types which are used by the function, whereas with class templates you can generically describe a class without identifying the types which are used by the class. These generic descriptions become specific when the templates are actually used; the types with which the templates are used determine the implementation. Listing 8.23 contains a function template.

Code Listing 8.23. Function Template "Count"
template <class Type, class Size>
Size Count(Type *array, Type object, Size size)
{
    Size counter = 0;
    for (Size i = 0; i < size; i++)
    {
        if (array[i] == object)
        {
            counter++;
        }
    }
    return
							
							 counter;
}

This template can be used to count the number of occurrences of a specific object, in an array of objects of the same type. To do this it receives a pointer to the array, an object to look for, and the size of the array. Note that the type of the object (and that of the array) has not been specified. The symbol Type has been used as a generic identifier. Note also that the type for holding the size of the array has not been specified. The symbol Size has been used as a generic identifier. With this template, you can count the number of occurrences of a character in a character string, but, just as easily, you can count the number of occurrences of number in a number sequence:

// Type = char, Size = short.

char a[] = "This is a test string";

short cnt = Count(a,'s',(short) strlen(a));


// Type = int, Size = long.

int b[] = { 0,1,2,3,4,0,1,2,3,4,0,0} ;
long ct = Count(b,0,12);

In fact, you could use this Count() function for any type imaginable, even your own structures and classes, as long as you make sure that the used type has an appropriate == operator. The reason this is possible is that the compiler actually generates a different implementation to deal with each different type for which the template is used. This means that in the preceding example two different Count() functions are created at compile time: one for counting characters in a character string, and the other for counting integers in an integer string. Clearly the use of templates does not solve any footprint problems. Although the source files can become smaller because the different functionality instantiations do not all need to be typed in, the executable size does not (that is to say, some compilers may be able to optimize certain template usage for types which are very similar: long versus int, signed versus unsigned, and so on). Templates do, however, obscure their footprint implications as you cannot easily assess how much code will be generated at compile time just by looking at the template definition. It is a common misconception, though, that templates are footprint inefficient. No more code will be generated from a template than you would create yourself by coding each used function out by hand.

Another misconception concerning templates is that their use is slower than that of "hand-carved" functionality. This is equally untrue. An implementation of a template is as efficient as the template was written to be. In fact, inefficiencies are more likely to pop up when you try to write out a function to be as generic as possible, in order not to have to use templates. Sometimes it can be rewarding to try this though, for instance, when a high number of template instantiations can be replaced with a single generic function. Listing 8.24 demonstrates this.

Code Listing 8.24. Generic Functions and Templates
// Generic swap using a template.
template <class T>
inline void Swap(T &x,  T  &y)
{
  T w = x;
  x = y;
  y = w;
}

// Generic swap without template.

void UniversalSwap(void **x,  void  **y)
{
  void *w = *x;
  *x = *y;
  *y
							
							
							
							
							 = w;
}

Listing 8.24 contains two functions that swap around the values of their arguments. The template version (Swap()) will cause an implementation to be created for every type you want to be able to swap around, whereas the non-template version (UniversalSwap()) will generate a single implementation. The non-template version is harder to read and maintain though, and you should consider whether a shallow copy via object pointers as used here will always suffice.

Another possibility is capturing a generic piece of functionality within a template. This way you have all the benefits of a generic implementation but its use becomes easier:

template <class UST>
inline void TSwap(UST &x, UST &y)
{
    UniversalSwap((void **)&x, (void **)&y);
}

Now the casting activities are captured within a template. For larger functions and classes, this kind of template wrapper makes perfect sense; the only piece of functionality that is instantiated for every used type is the small part that does the casting.

When you think of what the template mechanism actually does—generating different instances of functionality at compile time—you might wonder if you can somehow misuse this mechanism to save some valuable typing time. And indeed, evil things are possible if you are inventive enough. How about some compile-time calculation or loop-unrolling? You have already seen the factorial function (n!) in this chapter. Listing 8.25 shows code which will generate a list of factorial answers at compile time.

Code Listing 8.25. Misusing Templates
template <int N>
class FactTemp
{
      public:
        enum {  retval = N * FactTemp<N-1>::retval } ;
} ;

class FactTemp<1>
{
  public:
    enum {  retval = 1 } ;
} ;

void main()
{
    long  x = 
							
							FactTemp<12>::retval;
}

Of course, the speed of this Fact<> function is unprecedented simply because it is not actually a function at all. This sort of template misuse is only possible when the compiler can determine all possible template instances at compile time. Listing 8.25 will probably no longer work when you ask it to produce the factorial of 69; the compiler will run into trouble and even if it did not, the answer would not even fit in a long. The interesting thing about using compile-time calculation is that you do not have to calculate a list of new values when, for instance, the resolution of your needed values changes. When you are using a list of cosine values and a newer version of your software suddenly needs one more decimal of precision, you can simply adjust the template to generate a different list.

In conclusion:

  • Templates are not necessarily slower than "hand-carved" functionality.

  • Templates can save a lot of development time as similar functionality for different types does not need to be written and tested separately.

  • Templates can obscure their actual footprint size.

  • Templates can be avoided by coding all different instances of functionality yourself, or writing generic functionality by using, for instance, pointers and references.

Look at the file 08Source03.cpp on the Web site for testing the speed of the different template techniques discussed in this section.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset