Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Professional C++ [eng].pdf
Скачиваний:
284
Добавлен:
16.08.2013
Размер:
11.09 Mб
Скачать

Discovering Inheritance Techniques

class Super

{

public:

Super();

~Super();

};

class Sub : public Super

{

public:

Sub() { mString = new char[30]; } ~Sub() { delete[] mString; }

protected:

char* mString;

};

int main(int argc, char** argv)

{

Super* ptr = new Sub(); // mString is allocated here.

delete ptr; // ~Super is called, but not ~Sub because the destructor // is not virtual!

}

Unless you have a specific reason not to, we highly recommend making all methods (except constructors) virtual. Constructors cannot and need not be virtual because you always specify the exact class being constructed when creating an object.

Runtime Type Facilities

Relative to other object-oriented languages, C++ is very compile-time oriented. Overriding methods, as you learned above, works because of a level of indirection between a method and its implementation, not because the object has built-in knowledge of its own class.

There are, however, features in C++ that provide a run-time view of an object. These features are commonly grouped together under a feature set called Runtime Type Identification, or RTTI. RTTI provides a number of useful features for working with information about an object’s class membership.

dynamic_cast

Way back in Chapter 1, you read about static_cast, one of C++’s mechanisms for converting between types. The static_cast operator is so-named because the conversion is built into the compiled code. A static downcast will always succeed, regardless of the runtime type of the object.

267

Chapter 10

As you read in the earlier section on downcasting, dynamic_cast provides a safer mechanism for converting between types within an OO hierarchy. To review, the syntax for dynamically casting an object is similar to a static cast. However, with a dynamic cast, an invalid cast will return NULL for a pointer or will throw an exception for a reference. The following example shows how to properly perform a dynamic cast to a reference.

SomeObject myObject = getSomeObject();

try {

SomeOtherObject& myRef = dynamic_cast<SomeOtherObject&>(myObject); } catch (std::bad_cast) {

cerr << “Could not convert the object into the desired type.” << endl;

}

typeid

The typeid operator lets you query an object at run time to find out its type. For the most part, you shouldn’t ever need to use typeid because any code that is conditionally run based on the type of the object would be better handled with virtual methods.

The following code uses typeid to print a message based on the type of the object.

#include <typeinfo>

void speak(const Animal& inAnimal)

{

if (typeid(inAnimal) == typeid(Dog&)) {

cout << “Woof!” << endl;

} else if (typeid(inAnimal) == typeid(Bird&) { cout << “Chirp!” << endl;

}

}

Anytime you see code like that shown above, you should immediately consider reimplementing the functionality as a virtual method. In this case, a better implementation would be to declare a virtual method called speak() in the Animal class. Dog would override the method to print “Woof!” and Bird would override the method to print “Chirp!”. This approach better fits object-oriented programming, where functionality related to objects is given to those objects.

The typeid functionality is sometimes handy in debugging, however. It is useful to print out the type of an object for logging and debugging purposes. The following code makes use of typeid for logging. The logObject function takes a “loggable” object as a parameter. The design is such that any object that can be logged subclasses the Loggable class and supports a method called getLogMessage(). In this way, Loggable is a mix-in class.

#include <typeinfo>

void logObject(Loggable& inLoggableObject)

{

logfile << typeid(inLoggableObject).name() << “ “; logfile << inLoggableObject.getLogMessage() << endl;

}

268

Discovering Inheritance Techniques

The logObject() function first writes the name of the object’s class to the file, followed by its log message. This way, when you read the log later, you can see which object was responsible for every line of the file.

Non-Public Inheritance

In all of the examples above, parent classes were always listed using the public keyword. You may be wondering if a parent can be private or protected. In fact it can, though neither is as common as public.

Declaring the relationship with the parent to be protected means that all public and protected methods and data members from the superclass become protected in the context of the subclass. Similarly, specifying private access means that all public, protected, and private methods and data members of the superclass become private in the subclass.

There are a handful of reasons why you might want to uniformly degrade the access level of the parent in this way, but most reasons imply flaws in the design of the hierarchy. Some programmers abuse this language feature, often in combination with multiple inheritance, to implement “components” of a class. Instead of making an Airplane class that contains an engine data member and a fuselage data member, they make an Airplane class that is a protected engine and a protected fuselage. In this way, the Airplane doesn’t look like an engine or a fuselage to client code (because everything is protected), but it is able to use all of that functionality internally.

Non-public inheritance is rare and we recommend using it cautiously, if for no other reason than because of most programmers’ unfamiliarity with it.

Virtual Base Classes

Earlier in this chapter, you learned about ambiguous base classes, a situation that arises when multiple parents each have a parent in common, as shown in Figure 10-9. The solution that we recommended was to make sure that the shared parent doesn’t have any functionality of its own. That way, its methods can never be called and there is no ambiguity problem.

C++ has another mechanism for addressing this problem in the event that you do want the shared parent to have its own functionality. If the shared parent is a virtual base class, there will not be any ambiguity. The following code adds a sleep() method to the Animal base class and modifies the Dog and Bird classes to inherit from Animal as a virtual base class. Without the virtual keyword, a call to sleep() on a DogBird object would be ambiguous because both Dog and Bird would have inherited versions of sleep() from Animal. However, when Animal is inherited virtually, only one copy of each method or member exists in its descendents.

class Animal

{

public:

virtual void eat() = 0;

virtual void sleep() { cout << “zzzzz....” << endl; }

};

269

Chapter 10

class Dog : public virtual Animal

{

public:

virtual void bark() { cout << “Woof!” << endl; }

virtual void eat() { cout << “The dog has eaten.” << endl; }

};

class Bird : public virtual Animal

{

public:

virtual void chirp() { cout << “Chirp!” << endl; }

virtual void eat() { cout << “The bird has eaten.” << endl; }

};

class DogBird : public Dog, public Bird

{

public:

virtual void eat() { Dog::eat(); }

};

int main(int argc, char** argv)

{

DogBird myConfusedAnimal;

myConfusedAnimal.sleep(); // Not ambiguous because Animal is virtual

}

Virtual base classes are a great way to avoid ambiguity in class hierarchies. The only drawback is that many C++ programmers are unfamiliar with the concept.

Summar y

This chapter has taken you through the myriad points of inheritance. You have learned about its many applications, including code reuse and polymorphism. You have also learned about its many abuses, including poorly designed multiple inheritance schemes. Along the way, you’ve uncovered some of the less common edge cases that are unlikely to come up on a daily basis but make for some gnarly bugs (and interview questions!).

Inheritance is a powerful language feature that takes some time to get used to. After you have worked with the examples of this chapter and experimented on your own, we hope that inheritance will become your tool of choice for object-oriented design.

270

Writing Generic Code

with Templates

C++ provides language support not only for object-oriented programming, but also for generic programming. As discussed in Chapter 5, the goal of generic programming is to write reusable code. The fundamental tools for generic programming in C++ are templates. Although not strictly an object-oriented feature, templates can be combined with object-oriented programming for powerful results. Unfortunately, many programmers consider templates to be the most difficult part of C++ and, for that reason, tend to avoid them. However, even if you never write your own templates, you need to understand their syntax and capabilities in order to use the C++ standard library.

This chapter provides the code details for fulfilling the design principle of generality discussed in Chapter 5 and prepares you to understand the standard template library, which is discussed further in Chapters 21 to 23. The chapter is divided into two halves. The first half presents the most commonly used template features, including:

How to write template classes

How the compiler processes templates

How to organize template source code

How to use nontype template parameters

How to write templates of individual class methods

How to write customizations of your class templates for specific types

How to combine templates and inheritance

How to write function templates

How to make template functions friends of template classes

Chapter 11

The second half of the chapter delves into some of the more obscure template features, including:

The three kinds of template parameters and their subtleties

Partial specialization

Function template deduction

How to exploit template recursion

Over view of Templates

The main programming unit in the procedural paradigm is the procedure or function. Functions are useful primarily because they allow you to write algorithms that are independent of specific values and can thus be reused for many different values. For example, the sqrt() function in C and C++ calculates the square root of a value supplied by the caller. A square root function that calculated only the square root of one number, like four, would not be particularly useful! The sqrt() function is written in terms of a parameter, which is a stand-in for whatever value the caller passes. Computer scientists say that functions parameterize values.

The object-oriented programming paradigm adds the concept of objects, which group related data and behaviors, but does not change the way functions and methods parameterize values.

Templates take the concept of parameterization a step further to allow you to parameterize on types as well as values. Recall that types in C++ include primatives such as int and double, as well as userdefined classes such as SpreadsheetCells and CherryTrees. With templates you can write code that is independent not only of the values it will be given, but of the types of those values as well! For example, instead of writing separate stack classes to store ints, Cars, and SpreadsheetCells, you can write one stack class definition that can be used for any of those types.

Although templates are an amazing language feature, templates in C++ are both conceptually and syntactically confusing, and many programmers overlook or avoid them. A committee designed template support in C++, and it sometimes seems as if the committee took an “everything but the kitchen sink” approach: the purpose of many template features might not be readily apparent. Even worse, compiler support for templates has historically been, and continues to be, spotty. Very few commercial compilers provide complete support for templates according to the C++ standard.

For these reasons, most C++ books only scratch the surface of templates. However, it is extremely important for you to understand C++ templates for one major reason: the C++ standard template library is, as its name suggests, built with templates. In order to take advantage of this library you must understand template fundamentals.

Thus, this chapter will teach you about template support in C++ with an emphasis on the aspects that arise in the standard template library. Along the way, you will learn about some nifty features that you can employ in your programs aside from using the standard library.

272