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

Discovering Inheritance Techniques

Interesting and Obscure Inheritance Issues

Extending a class opens up a variety of issues. What characteristics of the class can and cannot be changed? What does the mysterious virtual keyword really do? These questions, and many more, are answered in the following sections.

Changing the Overridden Method’s Characteristics

For the most part, the reason you override a method is to change its implementation. Sometimes, however, you may want to change other characteristics of the method.

Changing the Method Return Type

A good rule of thumb is to override a method with the exact method declaration, or method signature, that the superclass uses. The implementation can change, but the signature stays the same.

That does not have to be the case, however. In C++, an overriding method can change the return type as long as the original return type is a pointer or reference to a class, and the new return type is a pointer or reference to a descendent class. Such types are called covariant returns types. This feature sometimes comes in handy when the superclass and subclass work with objects in a parallel hierarchy. That is, another group of classes that is tangential, but related, to the first class hierarchy.

For example, consider a hypothetical cherry orchard simulator. You might have two hierarchies of classes that model different real-world objects but are obviously related. The first is the Cherry chain. The base class, Cherry, has a subclass called BingCherry. Similarly, there is another chain of classes with a base class called CherryTree and a subclass called BingCherryTree. Figure 10-10 shows the two class chains.

Cherry CherryTree

BingCherry BingCherryTree

Figure 10-10

Now assume that the CherryTree class has a method called pick() that will retrieve a single cherry from the tree:

Cherry* CherryTree::pick()

{

return new Cherry();

}

In the BingCherryTree subclass, you may want to override this method. Perhaps Bing Cherries need to be polished when they are picked (bear with us on this one). Because a BingCherry is a Cherry, you could leave the method signature as is and override the method as in the following example. The BingCherry pointer is automatically cast to a Cherry pointer.

253

Chapter 10

Cherry* BingCherryTree::pick()

{

BingCherry* theCherry = new BingCherry();

theCherry->polish();

return theCherry;

}

The implementation above is perfectly fine and is probably the way that the authors would write it. However, because you know that the BingCherryTree will always return BingCherry objects, you could indicate this fact to potential users of this class by changing the return type, as shown here:

BingCherry* BingCherryTree::pick()

{

BingCherry* theCherry = new BingCherry();

theCherry->polish();

return theCherry;

}

A good way to figure out whether you can change the return type of an overridden method is to consider whether existing code would still work. In the preceding example, changing the return type was fine because any code that assumed that the pick() method would always return a Cherry* would still compile and work correctly. Because a BingCherry is a Cherry, any methods that were called on the result of CherryTree’s version of pick() could still be called on the result of BingCherryTree’s version of pick().

You could not, for example, change the return type to something completely unrelated, such as void*. The following code will not compile because the compiler thinks that you are trying to overload pick(), but cannot distinguish BingCherryTree’s pick() method from CherryTree’s pick() method because return types are not used in method disambiguation.

void* BingCherryTree::pick() // BUG!

{

BingCherry* theCherry = new BingCherry();

theCherry->polish();

return theCherry;

}

Changing the Method Parameters

In general, if you try to change the parameters to an overridden method, you are no longer overriding it — you are creating a new method. Returning to the Super and Sub example from earlier in this chapter, you could attempt to override someMethod() in Sub with a new argument list, as shown here:

class Super

{

public:

Super();

virtual void someMethod();

254

Discovering Inheritance Techniques

};

class Sub : public Super

{

public:

Sub();

virtual void someMethod(int i); // Compiles, but doesn’t override virtual void someOtherMethod();

};

The implementation of this method is shown here:

void Sub::someMethod(int i)

{

cout << “This is Sub’s version of someMethod with argument “ << i << “.” << endl;

}

The preceding class definition will compile, but you have not overridden someMethod(). Because the arguments are different, you have created a new method that exists only on Sub. If you want a method called someMethod() that takes an int, and you want it to only work on objects of class Sub, the preceding code is correct. However, it is stylistically questionable to have a method that has the same name as a method in the superclass but no real relationship to that method.

In fact, the original method is now hidden as far as Sub is concerned. The following sample code will not compile because there is no longer a no-argument version of someMethod().

Sub mySub;

mySub.someMethod();

// BUG! Won’t compile because the original method is hidden.

There is a case where you actually can change the argument list for an overridden method. The trick is that the new argument list must be compatible with the old one. If we modified the above example to give the i parameter a default value, then Sub’s version of someMethod() would actually be overriding

Super’s version of someMethod():

class Sub : public Super

{

public:

Sub();

virtual void someMethod(int i = 2); // actually overrides virtual void someOtherMethod();

Why is this any different? The answer is that in order to override a method, other code needs to be able to call the method in the same way on either the superclass or the subclass. Just as in the earlier return type case, the acid test for changing method arguments is whether existing code would have to be modified. With a default argument, any code that called Super’s someMethod() could call Sub’s someMethod() without modification.

The following sample code shows that this time Sub has actually overridden Super’s version of someMethod(). Even a Super reference will correctly call Sub’s version.

255

Chapter 10

Sub mySub;

Super& ref = mySub;

mySub.someMethod();

// Calls Sub’s someMethod with default argument

mySub.someMethod(1);

//

Calls

Sub’s someMethod

ref.someMethod();

//

Calls

Sub’s someMethod with default argument

The output of this code is:

This is Sub’s version of someMethod with argument 2.

This is Sub’s version of someMethod with argument 1.

This is Sub’s version of someMethod with argument 2.

There is also a somewhat obscure technique you can use to have your cake and eat it too. That is, you can use this technique to effectively override a method in the subclass with a new signature but continue to inherit the superclass version. This technique uses the using keyword to explicitly include the superclass definition of the method within the subclass, as shown here:

class Super

{

public:

Super();

virtual void someMethod();

};

class Sub : public Super

{

public:

Sub();

using Super::someMethod; // Explicitly “inherits” the Super version

virtual void someMethod(int i); // Adds a new version of someMethod virtual void someOtherMethod();

};

It is rare to find an overridden method that changes the parameter list.

Special Cases in Overriding Methods

Several edge cases require special attention when overriding a method. In this section, we have outlined the cases that are likely to encounter.

If the Superclass Method Is Static

In C++, you cannot override a static method. For the most part, that’s all you need to know. There are, however, a few corollaries that you need to understand.

First of all, a method cannot be both static and virtual. This is the first clue that attempting to override a static method will not do what you intend for it to do. If you have a static method in your subclass with the same name as a static method in your superclass, you actually have two separate methods.

256

Discovering Inheritance Techniques

The following code shows two classes that both happen to have static methods called beStatic(). These two methods are in no way related.

class SuperStatic

{

public:

static void beStatic() { cout << “SuperStatic being static, yo.” << endl;

};

class SubStatic

{

public:

static void beStatic() { cout << “SubStatic keepin’ it static.” << endl;

};

Because a static method belongs to its class, calling the identically named methods on the two different classes will call their respective methods:

SuperStatic::beStatic();

SubStatic::beStatic();

Will output:

SuperStatic being static, yo.

SubStatic keepin’ it static.

Everything makes perfect sense as long as the methods are accessed by class. The behavior is less clear when objects are involved. In C++, you can call a static method on an object syntactically, but in reality, the method only exists on the class. Consider the following code:

SubStatic mySubStatic;

SuperStatic& ref = mySubStatic;

mySubStatic.beStatic();

ref.beStatic();

The first call to beStatic() will obviously call the SubStatic version because it is explicitly called on an object declared as a SubStatic. The second call is less clear. The object is a SuperStatic reference, but it refers to a SubStatic object. In this case, SuperStatic’s version of beStatic() will be called.

The reason is that C++ doesn’t care what the object actually is when calling a static method. It only cares about the compile-time type. In this case, the type is a reference to a SuperStatic.

The output of the previous example is:

SubStatic keepin’ it static.

SuperStatic being static, yo.

static methods are bound to the class in which they are defined, not to any object. A method in a class that calls a static method calls the version defined in that class, independent of the run-time type of the object on which the original method is called.

257

Chapter 10

If the Superclass Method Is Overloaded

When you override a method, you are implicitly hiding any other versions of the method. It makes sense if you think about it — why would you want to change some versions of a method and not others? Consider the following subclass, which overrides a method without overriding its associated overloaded siblings:

class Foo

{

public:

virtual void overload() { cout << “Foo’s overload()” << endl; }

virtual void overload(int i) { cout << “Foo’s overload(int i)” << endl; }

};

class Bar : public Foo

{

public:

virtual void overload() { cout << “Bar’s overload()” << endl; }

};

If you attempt to call the version of overload() that takes an int parameter on a Bar object, your code will not compile because it was not explicitly overridden.

myBar.overload(2); // BUG! No matching method for overload(int).

It is possible, however, to access this version of the method from a Bar object. All you need is a pointer or a reference to a Foo object.

Bar myBar;

Foo* ptr = &myBar;

ptr->overload(7);

The hiding of unimplemented overloaded methods is only skin deep in C++. Objects that are explicitly declared as instances of the subtype will not make the method available, but a simple cast to the superclass will bring it right back.

The using keyword can be employed to save you the trouble of overloading all the versions when you really only want to change one. In the following code, the Bar class definition uses one version of overload() from Foo and explicitly overloads the other.

class Foo

{

public:

virtual void overload() { cout << “Foo’s overload()” << endl; }

virtual void overload(int i) { cout << “Foo’s overload(int i)” << endl; }

};

class Bar : public Foo

{

public:

using Foo::overload;

virtual void overload() { cout << “Bar’s overload()” << endl; }

};

258

Discovering Inheritance Techniques

To avoid obscure bugs, you should override all versions of an overloaded method, either explicitly or with the using keyword.

If the Superclass Method Is Private or Protected

There’s absolutely nothing wrong with overriding a private or protected method. Remember that the access specifier for a method determines who is able to call the method. Just because a subclass can’t call its parent’s private methods doesn’t mean it can’t override them. In fact, overriding a private or protected method is a common pattern in object-oriented languages. It allows subclasses to define their own “uniqueness” that is referenced in the superclass.

For example, the following class is part of a car simulator that estimates the number of miles the car can travel based on its gas mileage and amount of fuel left.

class MilesEstimator

{

public:

virtual int getMilesLeft() {

return (getMilesPerGallon() * getGallonsLeft());

}

virtual void setGallonsLeft(int inValue) { mGallonsLeft = inValue; } virtual int getGallonsLeft() { return mGallonsLeft; }

private:

int mGallonsLeft;

virtual int getMilesPerGallon() { return 20; }

}

The getMilesLeft() method performs a calculation based on the results of two of its own methods. The following code uses the MilesEstimator to calculate how many miles can be traveled with 2 gallons of gas.

MilesEstimator myMilesEstimator;

myMilesEstimator.setGallonsLeft(2);

cout << “I can go “ << myMilesEstimator.getMilesLeft() << “ more miles.” << endl;

The output of this code is:

I can go 40 more miles.

To make the simulator more interesting, you may want to introduce different types of vehicles, perhaps a more efficient car. The existing MilesEstimator assumes that cars all get 20 miles per gallon, but this value is returned from a separate method specifically so that a subclass could override it. Such a subclass is shown here:

class EfficientCarMilesEstimator : public MilesEstimator

{

private:

virtual int getMilesPerGallon() { return 35; }

};

259

Chapter 10

By overriding this one private method, the new class completely changes the behavior of existing, unmodified, public methods. The getMilesLeft() method in the superclass will automatically call the overridden version of the private getMilesPerGallon() method. An example using the new class is shown here:

EfficientCarMilesEstimator myEstimator;

myEstimator.setGallonsLeft(2);

cout << “I can go “ << myEstimator.getMilesLeft() << “ more miles.” << endl;

This time, the output reflects the overridden functionality:

I can go 70 more miles.

Overriding private and protected methods is a good way to change certain features of a class without a major overhaul.

If the Superclass Method Has Default Arguments

Subclasses and superclasses can each have different default arguments, but the argument that is used depends on the declared type of the variable, not the underlying object. Following is a simple example of a subclass that provides a different default argument in an overridden method:

class Foo

{

public:

virtual void go(int i = 2) { cout << “Foo’s go with param “ << i << endl; }

};

class Bar : public Foo

{

public:

virtual void go(int i = 7) { cout << “Bar’s go with param “ << i << endl; }

};

If go() is called on a Bar object, Bar’s version of go() will executed with the default argument of 7. If go() is called on a Foo object, Foo’s version of go() will be executed with the default argument of 2. However (this is the weird part), if go() is called on a Foo pointer or Foo reference that really points to a Bar object, Bar’s version of go() will be called it will use the default Foo argument of 2. This behavior is shown here:

Foo myFoo;

Bar myBar;

Foo& myFooReferenceToBar;

myFoo.go();

myBar.go();

myFooReferenceToBar.go();

260

Discovering Inheritance Techniques

The output of this code is:

Foo’s go with param 2

Bar’s go with param 7

Bar’s go with param 2

Tricky, eh? The reason for this behavior is that C++ binds default arguments to the type of the variable denoting the object, not the object itself. For this same reason, default arguments are not “inherited” in C++. If the Bar class above failed to provide a default argument as its parent did, it would be overloading the go() method with a new zero-argument version.

When overriding a method that has a default argument, you should provide a default argument as well, and it should probably be the same value.

If the Superclass Method Has a Different Access Level

There are two ways you may wish to change the access level of a method — you could try to make it more restrictive or less restrictive. Neither case makes much sense in C++, but there are a few legitimate reasons for attempting to do so.

To enforce tighter restriction on a method (or on a data member for that matter), there are two approaches you can take. One way is to change the access specifier for the entire base class. This approach is described at the end of this chapter. The other approach is simply to redefine the access in the subclass, as illustrated in the Shy class that follows:

class Gregarious

{

public:

virtual void talk() { cout << “Gregarious says hi!” << endl; }

};

class Shy : public Gregarious

{

protected:

virtual void talk() { cout << “Shy reluctantly says hello.” << endl; }

};

The protected version of talk() in the Shy class properly overrides the method. Any client code that attempts to call talk() on a Shy object will get a compile error:

myShy.talk(); // BUG! Attempt to access protected method.

However, the method is not fully protected. One only has to obtain a Gregarious reference or pointer to access the method that you thought was protected:

Shy myShy;

Gregarious& ref = myShy;

ref.talk();

261

Chapter 10

The output of the preceding code is:

Shy reluctantly says hello.

This proves that making the method protected in the subclass did actually override the method (because the subclass version was correctly called), but it also proves that the protected access can’t be fully enforced if the superclass makes it public.

There is no reasonable way (or good reason why) to restrict access to a public parent method.

It’s much easier (and makes a lot more sense) to lessen access restrictions in subclasses. The simplest way is simply to provide a public method that calls a protected method from the superclass, as shown here:

class Secret

{

protected:

virtual void dontTell() { cout << “I’ll never tell.” << endl; }

};

class Blabber : public Secret

{

public:

virtual void tell() { dontTell(); }

};

A client calling the public tell() method of a Blabber object would effectively access the protected method of the Secret class. Of course, this doesn’t really change the access level of dontTell(), it just provides a public way of accessing it.

You could also override dontTell() explicitly in the Blabber subclass and give it new behavior with public access. This makes a lot more sense than reducing the level of access because it is entirely clear what happens with a reference or pointer to the base class. For example, suppose that Blabber actually made the dontTell() method public:

class Secret

{

protected:

virtual void dontTell() { cout << “I’ll never tell.” << endl; }

};

class Blabber : public Secret

{

public:

virtual void dontTell() { cout << “I’ll tell all!” << endl; }

};

262