Inapoi
Inainte
Cuprins
Operator overloading &
inheritance
Except for the assignment operator,
operators are automatically inherited into a derived class. This can be
demonstrated by inheriting from C12:Byte.h:
//: C14:OperatorInheritance.cpp
// Inheriting overloaded operators
#include "../C12/Byte.h"
#include <fstream>
using namespace std;
ofstream out("ByteTest.out");
class Byte2 : public Byte {
public:
// Constructors don't inherit:
Byte2(unsigned char bb = 0) : Byte(bb) {}
// operator= does not inherit, but
// is synthesized for memberwise assignment.
// However, only the SameType = SameType
// operator= is synthesized, so you have to
// make the others explicitly:
Byte2& operator=(const Byte& right) {
Byte::operator=(right);
return *this;
}
Byte2& operator=(int i) {
Byte::operator=(i);
return *this;
}
};
// Similar test function as in C12:ByteTest.cpp:
void k(Byte2& b1, Byte2& b2) {
b1 = b1 * b2 + b2 % b1;
#define TRY2(OP) \
out << "b1 = "; b1.print(out); \
out << ", b2 = "; b2.print(out); \
out << "; b1 " #OP " b2 produces "; \
(b1 OP b2).print(out); \
out << endl;
b1 = 9; b2 = 47;
TRY2(+) TRY2(-) TRY2(*) TRY2(/)
TRY2(%) TRY2(^) TRY2(&) TRY2(|)
TRY2(<<) TRY2(>>) TRY2(+=) TRY2(-=)
TRY2(*=) TRY2(/=) TRY2(%=) TRY2(^=)
TRY2(&=) TRY2(|=) TRY2(>>=) TRY2(<<=)
TRY2(=) // Assignment operator
// Conditionals:
#define TRYC2(OP) \
out << "b1 = "; b1.print(out); \
out << ", b2 = "; b2.print(out); \
out << "; b1 " #OP " b2 produces "; \
out << (b1 OP b2); \
out << endl;
b1 = 9; b2 = 47;
TRYC2(<) TRYC2(>) TRYC2(==) TRYC2(!=) TRYC2(<=)
TRYC2(>=) TRYC2(&&) TRYC2(||)
// Chained assignment:
Byte2 b3 = 92;
b1 = b2 = b3;
}
int main() {
out << "member functions:" << endl;
Byte2 b1(47), b2(9);
k(b1, b2);
} ///:~
The test code is identical to that in
C12:ByteTest.cpp except that Byte2 is used instead of Byte.
This way all the operators are verified to work with Byte2 via
inheritance.
When you examine the class Byte2,
you’ll see that the constructor must be explicitly defined, and that only
the operator= that assigns a Byte2 to a Byte2 is
synthesized; any other assignment operators that you need you’ll have to
synthesize on your own.
Multiple inheritance
You can inherit from one class, so it
would seem to make sense to inherit from more than one class at a time. Indeed
you can, but whether it makes sense as part of a design is a subject of
continuing debate. One thing is generally agreed upon: You shouldn’t try
this until you’ve been programming quite a while and understand the
language thoroughly. By that time, you’ll probably realize that no matter
how much you think you absolutely must use multiple inheritance, you can almost
always get away with single inheritance.
Initially, multiple inheritance seems
simple enough: You add more classes in the base-class list during inheritance,
separated by commas. However, multiple inheritance introduces a number of
possibilities for ambiguity, which is why a chapter in Volume 2 is devoted to
the
subject.
Incremental development
One of the advantages of inheritance and
composition is that these support incremental
development by allowing you
to introduce new code without causing bugs in existing code. If bugs do appear,
they are isolated within the new code. By inheriting from (or composing with) an
existing, functional class and adding data members and member functions (and
redefining existing member functions during inheritance) you leave the existing
code – that someone else may still be using – untouched and
unbugged. If a bug happens, you know it’s in your new code, which is much
shorter and easier to read than if you had modified the body of existing
code.
It’s rather amazing how cleanly the
classes are separated. You don’t even need the source code for the member
functions in order to reuse the code, just the header file describing the class
and the object file or library file with the compiled member functions. (This is
true for both inheritance and composition.)
It’s important to realize that
program development is an
incremental process, just like
human learning. You can do as much analysis as you want, but you still
won’t know all the answers when you set out on a project. You’ll
have much more success – and more immediate feedback – if you start
out to “grow” your project as an organic, evolutionary creature,
rather than constructing it all at once like a glass-box
skyscraper[52].
Although
inheritance for experimentation is a useful technique,
at some point after things stabilize you need to take a new look at your class
hierarchy with an eye to collapsing it into a sensible
structure[53].
Remember that underneath it all, inheritance is meant to express a relationship
that says, “This new class is a type of that old class.” Your
program should not be concerned with pushing bits around, but instead with
creating and manipulating objects of various types to express a model in the
terms given you from the problem
space.
Upcasting
Earlier in the chapter, you saw how an
object of a class derived from ifstream has all the characteristics and
behaviors of an ifstream object. In FName2.cpp, any ifstream
member function could be called for an FName2
object.
The
most important aspect of inheritance is not that it provides member functions
for the new class, however. It’s the relationship expressed between the
new class and the base class. This relationship can be summarized by saying,
“The new class is a type of the existing class.”
This description is not just a fanciful
way of explaining inheritance – it’s supported directly by the
compiler. As an example, consider a base class called Instrument that
represents musical instruments and a derived class called Wind. Because
inheritance means that all the functions in the base class are also available in
the derived class, any message you can send to the base class can also be sent
to the derived class. So if the Instrument class has a
play( ) member function, so will Wind instruments. This means
we can accurately say that a Wind object is also a type of
Instrument. The following example shows how the compiler supports this
notion:
//: C14:Instrument.cpp
// Inheritance & upcasting
enum note { middleC, Csharp, Cflat }; // Etc.
class Instrument {
public:
void play(note) const {}
};
// Wind objects are Instruments
// because they have the same interface:
class Wind : public Instrument {};
void tune(Instrument& i) {
// ...
i.play(middleC);
}
int main() {
Wind flute;
tune(flute); // Upcasting
} ///:~
What’s interesting in this example
is the tune( ) function, which accepts an Instrument
reference. However, in main( ) the tune( ) function is
called by handing it a reference to a Wind object. Given that C++ is very
particular about type checking, it seems strange that a function that accepts
one type will readily accept another type, until you realize that a Wind
object is also an Instrument object, and there’s no function that
tune( ) could call for an Instrument that isn’t also in
Wind (this is what inheritance guarantees). Inside tune( ),
the code works for Instrument and anything derived from
Instrument, and the act of converting a Wind reference or pointer
into an Instrument reference or pointer is called
upcasting.
Why “upcasting?”
The reason for the term is historical and
is based on the way class inheritance diagrams
have
traditionally been drawn: with the root at the top of the page, growing
downward. (Of course, you can draw your diagrams any way you find helpful.) The
inheritance diagram for Instrument.cpp is then:
Casting from derived to base moves
up on the inheritance diagram, so it’s commonly referred to as
upcasting. Upcasting is always safe because you’re going from a more
specific type to a more general type – the only thing that can occur to
the class interface is that it can lose member functions, not gain them. This is
why the compiler allows upcasting without any explicit casts or other special
notation.
Upcasting and the
copy-constructor
If you allow the compiler to synthesize a
copy-constructor for a derived class, it will automatically call the base-class
copy-constructor, and then the copy-constructors for all the member objects (or
perform a bitcopy on built-in types) so you’ll get the right
behavior:
//: C14:CopyConstructor.cpp
// Correctly creating the copy-constructor
#include <iostream>
using namespace std;
class Parent {
int i;
public:
Parent(int ii) : i(ii) {
cout << "Parent(int ii)\n";
}
Parent(const Parent& b) : i(b.i) {
cout << "Parent(const Parent&)\n";
}
Parent() : i(0) { cout << "Parent()\n"; }
friend ostream&
operator<<(ostream& os, const Parent& b) {
return os << "Parent: " << b.i << endl;
}
};
class Member {
int i;
public:
Member(int ii) : i(ii) {
cout << "Member(int ii)\n";
}
Member(const Member& m) : i(m.i) {
cout << "Member(const Member&)\n";
}
friend ostream&
operator<<(ostream& os, const Member& m) {
return os << "Member: " << m.i << endl;
}
};
class Child : public Parent {
int i;
Member m;
public:
Child(int ii) : Parent(ii), i(ii), m(ii) {
cout << "Child(int ii)\n";
}
friend ostream&
operator<<(ostream& os, const Child& c){
return os << (Parent&)c << c.m
<< "Child: " << c.i << endl;
}
};
int main() {
Child c(2);
cout << "calling copy-constructor: " << endl;
Child c2 = c; // Calls copy-constructor
cout << "values in c2:\n" << c2;
} ///:~
The operator<< for
Child is interesting because of the way that it calls the
operator<< for the Parent part within it: by casting the
Child object to a Parent& (if you cast to a base-class
object instead of a reference you will usually get undesirable
results):
return os << (Parent&)c << c.m
Since the compiler then sees it as a
Parent, it calls the Parent version of
operator<<.
You can see that Child has no
explicitly-defined copy-constructor. The compiler then synthesizes the
copy-constructor (since that is one of the four functions it will
synthesize, along with the
default constructor – if you don’t create any constructors –
the operator= and the destructor) by calling the Parent
copy-constructor and the Member copy-constructor. This is shown in the
output
Parent(int ii)
Member(int ii)
Child(int ii)
calling copy-constructor:
Parent(const Parent&)
Member(const Member&)
values in c2:
Parent: 2
Member: 2
Child: 2
However, if you try to write your own
copy-constructor for Child and you make an innocent mistake and do it
badly:
Child(const Child& c) : i(c.i), m(c.m) {}
then the default constructor will
automatically be called for the base-class part of Child, since
that’s what the compiler falls back on when it has no other choice of
constructor to call (remember that some constructor must always be called
for every object, regardless of whether it’s a subobject of another
class). The output will then be:
Parent(int ii)
Member(int ii)
Child(int ii)
calling copy-constructor:
Parent()
Member(const Member&)
values in c2:
Parent: 0
Member: 2
Child: 2
This is probably not what you expect,
since generally you’ll want the base-class portion to be copied from the
existing object to the new object as part of copy-construction.
To repair the problem you must remember
to properly call the base-class copy-constructor (as the compiler does) whenever
you write your own copy-constructor. This can seem a little strange-looking at
first but it’s another example of upcasting:
Child(const Child& c)
: Parent(c), i(c.i), m(c.m) {
cout << "Child(Child&)\n";
}
The strange part is where the
Parent copy-constructor is called: Parent(c). What does it mean to
pass a Child object to a Parent constructor? But Child is
inherited from Parent, so a Child reference is a
Parent reference. The base-class copy-constructor call upcasts a
reference to Child to a reference to Parent and uses it to perform
the copy-construction. When you write your own copy constructors you’ll
almost always want to do the same
thing.
 |
|