Programming Language Concepts Using C and C++/Object Orientation and Inheritance in C++
Logic of inheritance does not change much from one programming language to another. For instance, if base and derived classes share the same public interface, the derived class is said to be a subtype of its base class and instances of it can be treated as instances of the base class. Or, thanks to dynamic dispatch, inheritance can be used to provide polymorphism.
However, newcomers to C++ from Java, or any other object-oriented programming language for that matter, are in for a few surprises. What we will do in this chapter is to take a look at inheritance in C++ and underline the differences with other programming languages.
Inheritance Peculiarities in C++
[edit | edit source]Inheritance Kinds
[edit | edit source]First peculiarity of C++ is the assortment of inheritance kinds it offers to programmers: public, protected, and private. This meets the newcomer with the first inheritance example she tries. Take a look at the following code snippet.
class D : B { ... }
This innocent-looking code claims to derive D
from B
. However, it will not let you treat an object of D
as an object of B
. That’s right: D
is not seen as a subtype of B
. It looks like either C++ or the programmer got it wrong. Not exactly! Similar to the default section being private
, inheritance, unless otherwise stated, is taken to be of the so-called private kind. Deferring the answer of what we mean by private inheritance to another section, we modify the above code to meet our expectations as given below.
class D : public B { ... }
Done with this peculiarity, let’s move on to see some code examples. We will be using the following class definitions throughout the examples.
class B { public: B(void) { } void f1s(void) { cout << "In B::f1s(void)" << endl; } virtual void f1d(void) { cout << "In B::f1d(void)" << endl; } virtual void f2(int i) { cout << "In B::f2(int)" << endl; } ... }; // end of class B class D : public B { public: D(void) { } void f1s(void) { cout << "In D::f1s(void)" << endl; } virtual void f1d(void) { cout << "In D::f1d(void)" << endl; } virtual void f2(short s) { cout << "In D::f2(short)" << endl; } ... int _m_i; short _m_s; }; // end of class D
No Member Initialization
[edit | edit source]... D* d = new D(); cout << d->_m_i << " " << d->_m_s << endl; ...
As a Java programmer you might think the above code fragment should produce two 0's in succession. However, it will output two random values. Unlike Java and C# where, unless overridden, data members are provided with default initial values, C++ compiler does not initialize the data members. If they need to be initialized to 0 or to any other value, this must be done explicitly by the programmer. It should also be noted that one cannot declare a data member with an initial value. That is, changing int _m_i;
to int _m_i = 0;
in D
will give rise to a syntactic error.
D() : _m_i(0), _m_s(0) { }
orD() { _m_i = 0; _m_s = 0; }
The C++ compiler has full confidence in the programmer. After all, a C++ programmer does not make a mistake. An error prone statement, which may be seen as the begetter of a hideous mistake in Java, is assumed to be the conscious decision of an all-knowing programmer. It’s not a bug it’s a feature!
Default Dispatch Type is Static Dispatch
[edit | edit source]... B* b; // the static type of b is B* if (bool_expr) b = new D(); // if this branch is taken b’s dynamic type will be D* else b = new B(); // if control falls through to this limp, dynamic type of b will be B* b->f1s(); ...
As a competent Java programmer you would expect this to produce—depending on the value bool_expr
evaluates to—"In B::f1s(void)"
or "In D::f1s(void)"
. However, in C++ it always outputs "In B::f1s(void)"
!!! Unlike Java, C++, unless otherwise told, uses static dispatch in binding function calls. This means the address of the function invoked by the call to f1s
will be resolved statically. That is, the compiler will use the static type of the identifier. In other words, functions invoked as a result of the calls made can be figured out by checking the program text.
... B* b; if (bool_expr) b = new D(); else b = new B(); b->f1d(); ...
A virtual function is dispatched dynamically. So, unlike the previous one, this example will compile and yield an output depending on the value of bool_expr
. If it evaluates to true
it will output "In D::f1d(void)", otherwise it will output "In B::f1d(void)".
Hide-by-name Overloading
[edit | edit source]... D* d = new D(); int i = 3; d->f2(i); // will be dispatched to D::f2(short) ...
With Java semantics, above code outputs "In B::f2(int)". After all, d
is also an object of type B
and can use the public interface of B
just like a genuine B
object. So, both D::f2(short)
and B::f2(int)
are exposed to clients of d
. Not in C++! Unlike Java, where base and derived class member functions make up a set of overloaded functions, C++ restricts this set to a single scope. Since derived and base classes are different scopes, any derived class function with a name coinciding with a base class function will shadow all the functions in the base class. Technically, we say C++ hides by name while Java is said to hide by signature.
But doesn’t it go against the logic of inheritance? You claim d
to be an object of B
(through the public inheritance relationship) and don’t let its clients use some function appearing in the public interface of B
? That’s right and C++ provides means to meet your expectations.
Example: Delegation |
---|
|
Regardless of whether the call is to some virtual function or not, explicit use of the class name in the function call causes it to be dispatched statically. In the following function, for instance, [although this
is of type D*
and f2d(int)
is virtual
] function call in the second statement will be dispatched to B::f2d(int)
.
Note this type of static dispatch can be used to invoke any function in the receiver object’s class or any function in any one of the ancestor classes.
void f2d(int i) { cout << "Delegating..."; B::f2d(i); } ... }; // end of class D ... D* d = new D(); int i = 3; d->f2d(i); // will be delegated to B::f2d(int) through D::f2d(int) short s = 5; d->f2d(s); // will be dispatched to D::f2d(short) ...
Example: using declaration |
---|
|
Multiple Inheritance, No Root Class
[edit | edit source]Unlike Java, where a class can derive from one and only one class, C++ supports derivation from multiple classes. Considered with the fact that interface notion is not supported, this feature is heavily used to implement interfaces.
class D : public B1, public B2 { ... }
One other point to take note of in C++ is its lack of a root class. That is, there is no class—such as the Object
class in Java—that serves as a common denominator among different classes. Consequently, one talks about a directed acyclic graph of classes instead of a tree of classes.
Test Program
[edit | edit source]#include <iostream>
#include <string>
using namespace std;
namespace CSE224 {
namespace DS {
class B {
public:
B(void) { }
void f1s(void) { cout << "In B::f1s(void)" << endl; }
virtual void f1d(void) { cout << "In B::f1d(void)" << endl; }
virtual void f2(int i) { cout << "In B::f2(int)" << endl; }
virtual void f2d(int i) { cout << "In B::f2d(int)" << endl; }
virtual void f2u(string s) { cout << "In B::f2u(string)" << endl; }
virtual void f2u(void) { cout << "In B::f2u(void)" << endl; }
}; // end of class B
class D : public B {
public:
D(void) { }
void f1s(void) { cout << “In D::f1s(void)” << endl; }
virtual void f1d(void) { cout << “In D::f1d(void)” << endl; }
virtual void f2(short s) { cout << “In D::f2(short)” << endl; }
virtual void f2d(short s) { cout << “In D::f2d(short)” << endl; }
virtual void f2d(int i) { cout << “Delegating...”; this->B::f2d(i); }
virtual void f2u(float f) { cout << “In D::f2u(float)” << endl; }
using B::f2u;
int _m_i;
short _m_s;
}; // end of class D
} // end of namespace DS
} // end of namespace CSE224
using namespace CSE224::DS;
void default_is_static_dispatch(void) {
cout << "TESTING DEFAULT DISPATCH TYPE" << endl;
cout << "b: Static type: B*, Dynamic type: D*" << endl;
B* b = new D();
cout << "Sending (non-virtual) f1s(void) to b..."; b->f1s();
cout << "Sending (virtual) f1d(void) to b..."; b->f1d();
} // end of void default_is_static_dispatch(void)
void call_delegation(void) {
cout << "Testing delegation..." << endl;
D* d = new D();
int i = 3;
cout << "Sending (virtual) f2d(int) to d...";
d->f2d(i);
short s = 5;
cout << "Sending (virtual) f2d(short) to d...";
d->f2d(s);
} // end of void call_delegation(void)
void using_declaration(void) {
cout << "Testing the using declaration..." << endl;
D* d = new D();
float f = 3.0;
cout << "Sending (virtual) f2u(float) to d...";
d->f2u(f);
string s = string(“abc”);
cout << "Sending (virtual) f2u(string) to d...";
d->f2u(s);
cout << "Sending (virtual) f2u(void) to d...";
d->f2u();
} // end of void using_declaration(void)
void CPP_hides_by_name(void) {
cout << "TESTING HIDE-BY NAME" << endl;
D* d = new D();
int i = 3;
cout << "Sending (virtual) f2(int) to d...";
d->f2(i);
call_delegation();
using_declaration();
} // end of void CPP_hides_by_name(void)
void no_member_initialization(void) {
cout << "TESTING MEMBER INITIALIZATION" << endl;
D* d = new D();
cout << "_m_i: " << d->_m_i << " _m_s: " << d->_m_s << endl;
} // end of void no_member_initialization(void)
int main(void) {
no_member_initialization();
cout << endl;
default_is_static_dispatch();
cout << endl;
CPP_hides_by_name();
return 0;
} // end of int main(void)
gxx –o Test.exe Peculiarities.cxx↵ Test↵ TESTING MEMBER INITIALIZATION _m_i: -1 _m_s: 9544 TESTING DEFAULT DISPATCH TYPE b: Static type: B*, Dynamic type: D* Sending (non-virtual) f1s(void) to b...In B::f1s(void) Sending (virtual) f1d(void) to b...In D::f1d(void) TESTING HIDE-BY NAME Sending (virtual) f2(int) to d...In D::f2(short) Testing delegation... Sending (virtual) f2d(int) to d...Delegating...In B::f2d(int) Sending (virtual) f2d(short) to d...In D::f2d(short) Testing the using declaration... Sending (virtual) f2u(float) to d...In D::f2u(float) Sending (virtual) f2u(string) to d...In B::f2u(string) Sending (virtual) f2u(void) to d...In B::f2u(void)
Inheritance a la Java
[edit | edit source]In this part of the handout, we provide an insight into how C++ and Java can be related as far as inheritance is concerned. This is accomplished by simulating the concepts found in Java using those found in C++. Such an approach should not be taken as an advertisement campaign of Java; needless to say Java is not without competition. It should rather be taken as an incomplete attempt at providing clues to the inner workings of the mentioned concepts.
Root Class and the Interface Concept
[edit | edit source]In Java, expressing common attributes among unrelated objects is made possible by means of the root class and the interface concept. The former defines a common denominator among all classes, while the latter is used to classify a group of classes.[1] For instance, because it is listed in Object
all objects can be tested for equality with an object of a compatible type; or objects of classes claiming to be Comparable
can be compared with a compatible object.
These two notions are not supported in C++ as a linguistic abstraction. Instead, programmers are expected to resort to using conventions or simulate it through other constructs. For instance, testing for equality is accomplished by overriding the default implementation of the ==
operator; interface concept, which is not directly supported, can be simulated by means of abstract classes with pure virtual functions.
#ifndef OBJECT_HXX
#define OBJECT_HXX
namespace System {
class Object {
Our intention in having the header file Object
is to define a root class that can be used as a polymorphic type in generic functions, such as compareTo
defined in IComparable
; we do not mean to provide any shared functionality as is done in Object
class of Java. This, however, cannot be accomplished simply by defining an empty class. In order for a type to be polymorphic in C++ it must have at least one virtual function. We therefore include a dummy virtual function in our class definition.
But then, why did we make its access modifier protected
? First of all, it cannot be public
because we don’t want any functionality to be exposed through this class. What about declaring no_op to be private
? After all, declaring it as protected
means deriving classes can now send the no_op
message. Answer lies in the nature of polymorphism: In order for polymorphism to be possible, one should be able to override the definition of a dynamically-dispatched function found in the base class. This implies such functions should be open at least to the derived classes. As a matter of fact C++ compilers will not even let you declare virtual
functions in a <syntaxhighlightlang="cpp" enclose="none">private</syntaxhighlight> section.
protected:
virtual void no_op(void) { return; }
}; // end of class Object
} // end of namespace System
#endif
Definition: A pure virtual function is a virtual function that is not given a function body in the declaring class. A derived class claiming to be concrete must therefore provide an implementation for such a function.
In terms of C++-supported concepts, an interface is a "fieldless" abstract class whose all functions are pure virtual.
#ifndef ICOMPARABLE_HXX
#define ICOMPARABLE_HXX
#include "Object"
namespace System {
class IComparable {
public:
virtual int compareTo(const Object&) const = 0;
}; // end of class IComparable
} // end of namespace System
#endif
Module
[edit | edit source]Interface (Rational)
[edit | edit source]#ifndef RATIONAL_HXX
#define RATIONAL_HXX
#include <iostream>
using namespace std;
#include "IComparable"
#include "Object"
using namespace System;
#include "math/exceptions/NoInverse"
#include "math/exceptions/ZeroDenominator"
#include "math/exceptions/ZeroDivisor"
using namespace CSE224::Math::Exceptions;
namespace CSE224 {
namespace Math {
Having defined the interface concept as a variation on the class concept, we naturally should be cautious about speaking of the implementation relation. This is indeed the case in C++: one can speak of the extension relation only. As a consequence support for multiple inheritance is a must.
class Rational : public Object, public IComparable {
public:
Rational(long num = 0, long den = 1) throw(ZeroDenominator) {
if (den == 0) {
cerr << "Error: ";
cerr << "About to throw ZeroDenominator exception" << endl;
throw ZeroDenominator();
} // end of if (den == 0)
_n = num;
_d = den;
this->simplify();
} // end of constructor(long=, long=)
Rational(Rational& existingRat) {
_n = existingRat._n;
_d = existingRat._d;
} // end of copy constructor
Notice the following functions, unlike the rest of the member functions, will be dispatched statically. In Java, such an effect can be achieved by declaring methods to be final
.
long getNumerator(void) const { return _n; }
long getDenominator(void) const { return _d; }
In addition to marking functions virtual
, we also declare them to return a reference. This is because a reference is the best candidate for serving the purpose of handles in Java: it is an inheritance-aware, compiler-managed pointer.[2] That is, we can pass as argument to the next function [or any function expecting a reference to Rational
, for that matter] an object belonging in the class hierarchy rooted by the Rational
class; dereferencing of a reference is automatically done by the compiler-synthesized code.
As an alternative—although the resulting code would be less writable and readable—we could have used a plain pointer. However, using a plain object type is out of question. This is due to the fact that polymorphism together with inheritance requires sending the same message—that is what polymorphism is all about—to objects of probably varying sizes—enter inheritance—which in turn implies passing and returning variable-sized objects. This is something compilers cannot deal with! We should provide some assistance, which we do by injecting a fixed-size programming entity in between: pointer or reference.
virtual Rational& add(const Rational&) const;
virtual Rational& divide(const Rational&) const throw(ZeroDivisor)
virtual Rational& inverse(void) const throw(NoInverse);
virtual Rational& multiply(const Rational&) const;
virtual Rational& subtract(const Rational&) const;
virtual int compareTo(const Object&) const;
Note the following function serves a purpose akin to that of toString
in Java. Replacing sstream instead of ostream
and changing the implementation accordingly would make the analogy a more perfect one.
friend ostream& operator<<(ostream&, const Rational&);
private:
long _n, _d;
long min(long n1, long n2);
Rational& simplify(void);
}; // end of class Rational
} // end of namespace Math
} // end of namespace CSE224
#endif
Implementation (Rational)
[edit | edit source]#include <iostream>
#include <memory>
using namespace std;
#include "Object"
using namespace System;
#include "math/exceptions/NoInverse"
#include "math/exceptions/ZeroDenominator"
#include "math/exceptions/ZeroDivisor"
using namespace CSE224::Math::Exceptions;
#include "math/Rational"
namespace CSE224 {
namespace Math {
Rational& Rational::
add(const Rational& rhs) const {
Note the missing try-catch
block! Unlike Java, C++ does not mandate the programmer to put all potentially problematic code in a guarded region. One can say all C++ exceptions are treated like the Java exceptions deriving from the RuntimeException
class. This gives the programmer a degree of freedom that enables her to come up with cleaner code. For instance, reaching the next line means we are adding two well-formed Rational
objects. Result of such an action can never create a problem!
Rational* sum = new Rational(_n * rhs._d + _d * rhs._n, _d * rhs._d);
return sum->simplify();
} // end of Rational& Rational::add(const Rational&) const
Rational& Rational::
divide(const Rational& rhs) const throw(ZeroDivisor) {
try {
Rational& tmp_inv = rhs.inverse();
Rational& ret_rat = this->multiply(tmp_inv);
Now that we are done with the temporary object that holds the inverse of rhs
, we must return it to the memory allocator or put up with the consequences of creating garbage at each use of this function. That’s pretty annoying! But then again, a C/C++ programmer does not make such easy mistakes.
Notice the address-of operator before tmp_inv
. Application of this operator to a reference returns the starting address of the region aliased by the reference. [Remember references are silently dereferenced at their point of use] In our case, this will be the address of the object created as a result of sending the message inverse
to rhs
.
delete &tmp_inv;
return ret_rat;
} catch (NoInverse e) {
cerr << "Error: About to throw ZeroDivisor exception" << endl;
throw ZeroDivisor();
}
} // end of Rational& Rational::divide(const Rational&) const
Rational& Rational::
inverse(void) const throw(NoInverse) {
try {
Rational *res = new Rational(_d, _n);
return *res;
} catch(ZeroDenominator e) {
cerr << "Error: About to throw NoInverse exception" << endl;
throw NoInverse(_n, _d);
}
} // end of Rational& Rational::inverse(void) const
Rational& Rational::
multiply(const Rational& rhs) const {
Rational *res = new Rational(_n * rhs._n, _d * rhs._d);
return res->simplify();
} // end of Rational& Rational::multiply(const Rational&) const
Rational& Rational::
subtract(const Rational& rhs) const {
We formulate subtraction in terms of other operations: instead of subtracting a value, we add the negated value. For doing this we create two temporary objects meaningful only throughout the current call. Before returning to the caller we should return them to the memory allocator.
A so-called smart pointer object is exactly what we want. Such an object is initialized to point to a dynamically allocated object created by a new expression and frees it—the dynamically allocated object—at the end of its (smart pointer’s) lifetime. The following figure showing the memory layout after execution of line 47 should make this clear.
Heap object local to the function is created together with the smart pointer object, which is itself a local object created on the run-time stack.9 This means allocation-constructor call and destructor call-deallocation of this smart pointer object will be handled by the compiler-synthesized code. In other words, programmer need not worry about the life-cycle management of the smart pointer object. So, if we can guarantee the heap object is destroyed-deallocated together with this smart pointer, its life-cycle management will not be a problem anymore. This is accomplished by delet(e)ing the heap object within the destructor of the related smart pointer object, which means the heap object will have been destroyed-deallocated by the time the destruction of the smart pointer object is over. The following then describes the life-cycle of the smart pointer and the related heap object.
- Create the smart pointer in the run-time stack.
- Pass the related heap object to the constructor of the smart pointer.
- Use the heap object.
- Call the destructor of the smart pointer by means of the compiler-synthesized code.
- Delete the heap object from within the destructor of the smart pointer.
According to this, anonymous Rational
objects—new Rational(-1)
and &(rhs.multiply(*neg_1))
—created in the following definitions will have been automatically—that is, without the intervention of the programmer—returned before leaving the function.
auto_ptr< Rational > neg_1(new Rational(-1));
auto_ptr< Rational > tmp_mul(&(rhs.multiply(*neg_1)));
Observe the following application of the dereferencing operator receives as its operand a non-pointer variable, which may at first seem as an error. After all, *
works by returning the contents of memory indicated by its sole operand. However, this rather limited description ignores the possibility of overloading the dereferencing operator. It is indeed the overloaded version of this operator that enables the use of a non-pointer type. The following application of *
makes use of the overloaded version defined within the auto_ptr
class, which returns the contents of the heap object managed by the smart pointer.
To make things clearer, we can suggest the following implementation for the auto_ptr
class.
template <class ManagedObjectType> class auto_ptr { public: auto_ptr(ManagedObjectType* managedObj) { _managed_heap_object = managedObj; ... } // end of constructor(ManagedObjectType*) ... ManagedObjectType operator*(void) { ... return *_managed_heap_object; } // end of ManagedObjectType operator*(void) ... private: ManagedObjectType* _managed_heap_object; } // end of class auto_ptr<ManagedObjectType>
Rational &ret_rat = add(*tmp_mul);
return(ret_rat);
} // end of Rational& Rational::subtract(const Rational&) const
int Rational::
compareTo(const Object& rhs) const {
double this_equi = ((double) _n) / _d;
In addition to the traditional C-style casting C++ offers a variety of cast operators: const_cast
, dynamic_cast
, static_cast
, and reinterpret_cast
. Each of these performs a subset of the functionality offered by the traditional cast operator and therefore one can say the new operators do not add any new functionality. Nevertheless, thanks to the extra support from the compiler, they enable writing more type-safe programs. Using the new operators we state our intentions explicitly and therefore get more maintainable code.
Example: Removing const -ness property of an object.
|
---|
|
It should be noted that const_cast can also be used to change volatile -ness property of an object.
|
Since our intention of removing the const
-ness has been made explicit by the relevant operator, maintainer of the code will more quickly spot the occurrence of cast and realize what is being done. Alternative scheme of using the C-style cast lacks these qualities: it is both difficult to find the location of the cast and figure out that const
-ness is being removed.
Using dynamic_cast
will also provide us with the benefit of safer code. This particular operator is used to bi-directionally cast between polymorphic classes—that is; classes that has at least one virtual function—that are related with each other by a public derivation.
Question | ||
---|---|---|
dynamic_cast can be used to convert only between pointer/reference types. Why?
| ||
|
Definition: Converting from one class to another in the same class hierarchy is referred to as downcasting if the target type is more specialized. In case the target type is less specialized the act of casting is called upcasting.
Upcasting to a public base class is always successful since the messages listed in the interface of the target type is a subset of the source type interface. On the other hand, casting from a derived class to one of its non-public base classes leads to a compile-time error. Similarly, downcasting may give rise to run-time errors since we can potentially send messages that are not found in the interface of the source type.
Example: Safer code with dynamic_cast .
|
---|
|
The above code downcasts a PB*
variable to PD*
, through which one can send the extra message named g
. For this example, this doesn't seem to be a problem. But what if pb
was used to point to an object of PB
instead of PD
? What if it is used to point to objects of different types as is shown in the following fragment?
if(some_condition) { ... ; pb = new D; ... } else { ...; pb = new B; ... } ... PD* pd = dynamic_cast<PD*>(pb);
There is no guarantee that we can send g
to the underlying object, which can be of type PB
or PD
. This guarantee we are seeking can be provided only if we can check the object type at run-time. And this is exactly what dynamic_cast
does: by checking compatibility of the pointer/reference—static type—with the object—dynamic type—dynamic_cast
decides whether the cast taking place is valid or not. If so a legitimate value is returned. Otherwise, in the case of casting a pointer, a NULL
value is returned, which basically removes any possibility of sending an illegal message; in the case of failing to cast a reference std::bad_cast
exception is thrown. Same actions are taken also when source and target types are not related with inheritance.
Note this cost due to the run-time check performed by the compiler-synthesized code is never experienced as a result of using the traditional cast operator. This is because the C-style cast operator makes no use of any run-time information.
Observe casting up the class hierarchy—since messages received by an object of the derived class is a subset of that of its base classes—does not need any run-time checks. This means cost due to dynamic_cast
is not rationalized: why should we pay for making a control, whose result is known to us? Solution offered by C++ is another cast operator that does its job using compile-time information: static_cast
. This operator can be used for performing conversions that are implicitly carried out by the compiler, performing these implicit conversions in the reverse direction. It can also be used in place of dynamic_cast
if skipping the run-time checks is guaranteed to be safe.
Example: |
---|
|
This doesn't fully cover the functionality offered by the traditional cast operator. Conversions between unrelated/incompatible pointer types are missing, for instance. This missing functionality is covered by the reinterpret_cast operator
, which can also perform conversions between pointer and integral types.
Example: |
---|
|
It should be kept in mind that this operator makes no checks on the source and target types; it simply bitwise-copies the contents of the target into the source.
double rhs_equi = ((double) dynamic_cast<const Rational&>(rhs)._n) / dynamic_cast<const Rational&>(rhs)._d;
if (this_equi > rhs_equi) return 1;
else if (this_equi == rhs_equi) return 0;
else return -1;
} // end of int Rational::compareTo(const Object&) const
long Rational::
min(long n1, long n2) { return (n1 > n2 ? n1 : n2); }
Rational& Rational::
simplify(void) {
long upperLimit = min(_n, _d);
for (long i = 2; i <= upperLimit;)
if ((_n % i == 0) && (_d % i == 0)) { _n /= i; _d /= i; }
else i++;
if (_d < 0) { _n *= -1; _d *= -1; }
return(*this);
} // end of Rational& Rational::simplify(void)
ostream& operator<<(ostream& os, const Rational& rat) {
os << rat._n << " ";
if (rat._d != 1) os << "/ " << rat._d;
return os;
} // end of ostream& operator<<(ostream&, const Rational&)
} // end of namespace Math
} // end of CSE224
Interface (Whole)
[edit | edit source]#ifndef WHOLE_HXX
#define WHOLE_HXX
#include <iostream>
using namespace std;
#include "math/Rational"
namespace CSE224 {
namespace Math {
class Whole : public Rational {
public:
Remember Whole
derives from Rational
. Put differently—since inheritance can be seen as a compiler-managed composition—all Whole
objects have a Rational
sub-object as part of their memory layouts. Following notation used in the member initialization list with no reference to the member being initialized will initialize the Rational
sub-object found in the Whole
object being constructed.
Whole(long num) : Rational(num) { }
Whole(void) : Rational(0) { }
Whole(Whole& existingWhole) :
Rational(existingWhole.getNumerator()) { }
Whole& add(const Whole& rhs) const;
virtual Rational& add(const Rational&) const;
}; // end of class Whole
} // end of namespace Math
} // end of namespace CSE224
#endif
Implementation (Whole)
[edit | edit source]#include <iostream>
using namespace std;
#include "math/Rational"
#include "math/Whole"
namespace CSE224 {
namespace Math {
Rational& Whole::
add(const Rational& rhs) const {
cout << "[In Whole::add(Rational)] ";
return (Rational::add(rhs));
} // end of Rational& Whole::add(const Rational&) const
Whole& Whole::
add(const Whole& rhs) const {
cout << "[In Whole::add(Whole)] ";
Whole *res = new Whole(getNumerator() + rhs.getNumerator());
return *res;
} // end of Whole& Whole::add(const Whole&) const
} // end of namespace Math
} // end of namespace CSE224
Exception Classes
[edit | edit source]#ifndef NOINVERSE_HXX
#define NOINVERSE_HXX
#include <iostream>
using namespace std;
namespace CSE224 {
namespace Math{
namespace Exceptions {
class NoInverse {
public:
NoInverse(long num, long den) {
cerr << "Error: Throwing a NoInverse exception" << endl;
_n = num; _d = den;
} // end of constructor(long, long)
void writeNumber(void) {
cerr << "The problematic number is " << _n << "/" << _d << endl;
} // end of void writeNumber()
private:
long _n, _d;
}; // end of class NoInverse
} // end of namespace Exceptions
} // end of namespace Math
} // end of namespace CSE224
#endif
#ifndef ZERODENOMINATOR_HXX
#define ZERODENOMINATOR_HXX
namespace CSE224 {
namespace Math {
namespace Exceptions {
class ZeroDenominator {
public:
ZeroDenominator(void) {
cerr << "Error: Throwing a ZeroDenominator exception" << endl;
} // end of default constructor
}; // end of class ZeroDenominator
} // end of namespace Exceptions
} // end of namespace Math
} // end of namespace CSE224
#endif
#ifndef ZERODIVISOR_HXX
#define ZERODIVISOR_HXX
#include <iostream>
using namespace std;
namespace CSE224 {
namespace Math {
namespace Exceptions {
class ZeroDivisor {
public:
ZeroDivisor(void) {
cerr << "Error: Throwing a ZeroDivisor exception" << endl;
} // end of default constructor
}; // end of class ZeroDivisor
} // end of namespace Exceptions
} // end of namespace Math
} // end of namespace CSE224
#endif
Test Program
[edit | edit source]#include <iostream>
#include <memory>
using namespace std;
#include "math/Whole"
using namespace CSE224::Math;
#include "math/exceptions/ZeroDenominator"
using namespace CSE224::Math::Exceptions;
int main(void) {
cout << "Creating a Whole object(five) and initializing it with 5..." << endl;
auto_ptr < Whole > fivep(new Whole(5));
Whole& five = *fivep;
cout << "Creating a Rational object(three) and initializing it with 3..." << endl;
auto_ptr < Rational > threep(new Rational(3));
Rational& three = *threep;
cout << "Result of five.multiply(three) = ";
cout << five.multiply(three) << endl;
cout << "***************************************************" << endl;
cout << "Result of three.add(three) = ";
cout << three.add(three) << endl;
cout << "Result of three.add(five) = ";
cout << three.add(five) << endl;
cout << "Result of five.add(three) = ";
cout << five.add(three) << endl;
cout << "Result of five.add(five) = ";
cout << five.add(five) << endl;
cout << "***************************************************" << endl;
cout << "Setting a Rational object(ratFive) as an alias for a Whole object(five)..." << endl;
Rational& ratFive = five;
cout << "Result of ratFive.add(three) = ";
cout << ratFive.add(three) << endl;
cout << "Result of ratFive.add(five) = ";
cout << ratFive.add(five) << endl;
cout << "Result of ratFive.add(ratFive) = ";
cout << ratFive.add(ratFive) << endl;
cout << "Result of five.add(ratFive) = ";
cout << five.add(ratFive) << endl;
cout << "Result of three.add(ratFive) = ";
cout << three.add(ratFive) << endl;
cout << "***************************************************" << endl;
cout << "Creating a Rational object(r1) and initializing it with 3/2..." << endl;
auto_ptr < Rational > r1p(new Rational(3, 2));
Rational& r1 = *r1p;
cout << "Result of five.multiply(r1) = ";
cout << five.multiply(r1) << endl;
cout << "Result of five.divide(r1) = ";
cout << five.divide(r1) << endl;
return 0;
} // end of int main(void)
Private Inheritance
[edit | edit source]Programming being a pragmatic endeavor, one must strive to do it as efficiently as possible. Arguably the most effective way to achieve this is to re-use artifacts that have already been used in different stages of previous projects.[3] By doing so we save on development and test time, which means our next product makes it to the market in a shorter time.
One way to achieve reuse is inheritance. And for many it seems to be the only affordable one. Nevertheless, there is a contender: composition. This technique is realized by making an object a member of another.
class C {... B b; ...};
For the above example we say an object of C
is composed of, apart from other things, an object of B
. Put differently, we say an object of C
has-an (contains) object of B
. This is certainly different than inheritance where the relationship is defined to be an is-a relationship.
In C++, similar effect can be achieved through the so-called private inheritance.
class C : /* private */ B { ... }
Interface (List)
[edit | edit source]#ifndef LIST_HXX
#define LIST_HXX
#include "ds/exceptions/List_Exceptions"
using namespace CSE224::DS::Exceptions;
namespace CSE224 {
namespace DS {
class List {
friend ostream& operator<<(ostream&, const List&);
public:
List(void) : _size(0), _head(NULL), _tail(NULL) { }
List(const List&);
~List(void);
List& operator=(const List&);
bool operator==(const List&);
double get_first(void) throw(List_Empty);
double get_last(void) throw(List_Empty);
void insert_at_end(double new_item);
void insert_in_front(double new_item);
double remove_from_end(void) throw(List_Empty);
double remove_from_front(void) throw(List_Empty);
bool is_empty(void);
unsigned int size(void);
private:
What follows is the definition of a nested class, a class defined inside another. Such a class, when defined in a private
section, is not visible outside its enclosing class. This scheme is useful when the two classes are tightly coupled, as is the case in our example: A List_Node
object is used only in the context of a List
object. What makes up our List
objects is an implementation detail and should not be a concern to their users.
Although it might be tempting to draw a parallel between nested classes and inner classes of Java, that would be a mistake. As opposed to the special relation between the inner class and its enclosing class, in C++ the enclosing class has no special access privileges with regard to the classes nested within it. For this reason, changing public
to private
or protected
is not a good idea.
Another remark to be made is that nested classes of C++ do not keep any record of the enclosing object in the objects of the inner class, which makes them like more the static inner classes of Java.
class List_Node {
public:
List_Node(double val) : _info(val), _next(NULL), _prev(NULL) { }
double _info;
List_Node *_next, *_prev;
}; // end of class List_Node
private:
List_Node *_head, *_tail;
unsigned int _size;
A function declared in the private
section? Yes! Functions, which are used by other functions and are not part of the interface, are declared in the private
section. Note that you cannot get away without declaring such functions in the class interface.
void insert_first_node(double);
}; // end of List class
} // end of namespace DS
} // end of namespace CSE224
#endif
Implementation (List)
[edit | edit source]#include <iostream>
using namespace std;
#include "ds/List"
#include "ds/exceptions/List_Exceptions"
using namespace CSE224::DS::Exceptions;
namespace CSE224 {
namespace DS {
List::
List(const List& rhs) : _head(NULL), _tail(NULL), _size(0) {
List_Node *ptr = rhs._head;
for(unsigned int i = 0; i < rhs._size; i++) {
this->insert_at_end(ptr->_info);
ptr = ptr->_next;
} // end of for(unsigned int i = 0; i < rhs._size; i++)
} // end of copy constructor
Now that nodes of the List
objects are allocated on the heap, we must make sure they are turned back over to the memory allocator as the List
object itself is, implicitly or explicitly, deleted. For this reason, we need to write a destructor to free these nodes. Note a List
object is made up of two pointers, which show the head and the tail of the list, and a field holding its size. The nodes pointed, directly or indirectly, by these pointers are not part of the List
object. So, they will not be automatically freed together with the list object. For this reason, we do need a destructor.
Notice our decision is not affected by whether the List
object itself is created on the heap or not. Where the List
object is created has an effect on who should call the destructor: Whoever the responsible party might be, compiler or programmer, in all possible scenarios the destructor is implicitly called before deallocation of the object. If it is created on the heap the programmer is responsible for making the call. Otherwise, the compiler will take care of the drudgery as the scope of the object is closed.
List::
~List(void) {
for (int i = 0; i < _size; i++)
remove_from_front();
} // end of destructor
List& List::
operator=(const List& rhs) {
if (this == &rhs) return (*this);
for(unsigned int i = _size; i > 0; i--)
this->remove_from_front();
List_Node *ptr = rhs._head;
for(unsigned int i = 0; i < rhs._size; i++) {
this->insert_at_end(ptr->_info);
ptr = ptr->_next;
} // end of for(unsigned int i = 0; i < rhs._size; i++)
if (rhs._size == 0) {
_head = _tail = NULL;
_size = 0;
} // end of if(rhs._size == 0)
return (*this);
} // end of assignment operator
bool List::
operator==(const List& rhs) {
if (_size != rhs._size) return false;
if (_size == 0 || this == &rhs) return true;
List_Node *ptr = _head;
List_Node *ptr_rhs = rhs._head;
for (unsigned int i = 0; i < _size; i++) {
if (!(ptr->_info == ptr_rhs->_info))
return false;
ptr = ptr->_next;
ptr_rhs = ptr_rhs->_next;
} // end of for(unsigned int i = 0; i < _size; i++)
return true;
} // end of equality-test operator
double List::
get_first(void) throw(List_Empty) {
if (is_empty()) throw List_Empty();
return (_head->_info);
} // end of double List::get_first(void)
double List::
get_last(void) throw(List_Empty) {
if (is_empty()) throw List_Empty();
return (_tail->_info);
} // end of double List::get_last(void)
void List::
insert_at_end(double new_item) {
if (is_empty()) insert_first_node(new_item);
else {
List_Node *new_node = new List_Node(new_item);
_tail->_next = new_node;
new_node->_prev = _tail;
_tail = new_node;
}
_size++;
} // end of void List::insert_at_end(double)
void List::
insert_in_front(double new_item) {
if (is_empty()) insert_first_node(new_item);
else {
List_Node *new_node = new List_Node(new_item);
new_node->_next = _head;
_head->_prev = new_node;
_head = new_node;
}
_size++;
} // end of void List::insert_in_front(double)
double List::
remove_from_end(void) throw(List_Empty) {
if (is_empty()) throw List_Empty();
double ret_val = _tail->_info;
List_Node *temp_node = _tail;
if (_size == 1) { head = _tail = NULL; }
else {
_tail = _tail->_prev;
_tail->_next = NULL;
}
delete temp_node;
_size--;
return ret_val;
} // end of double List::remove_from_front(void)
double List::
remove_from_front(void) throw(List_Empty) {
if (is_empty()) throw List_Empty();
double ret_val = _head->_info;
List_Node *temp_node = _head;
if (_size == 1) { _head = _tail = NULL; }
else {
_head = _head->_next;
_head->_prev = NULL;
}
delete temp_node;
_size--;
return ret_val;
} // end of double List::remove_from_front(void)
bool List::
is_empty(void) { return(_size == 0); }
unsigned int List::
size(void) { return _size; }
void List::
insert_first_node(double new_item) {
List_Node *new_node = new List_Node(new_item);
_head = _tail = new_node;
} // end of void List::insert_first_node(double)
ostream& operator<<(ostream& os, const List& rhs) {
List tmp_list = rhs;
os << "<" << rhs._size << "> <head: ";
for (int i = 0; i < rhs._size - 1; i++) {
os << tmp_list._head->_info << ", ";
tmp_list._head = tmp_list._head->_next;
} // end of for(int i = 0; i < rhs._size; i++)
if (rhs._size > 0) os << tmp_list._head->_info;
os << ": tail>";
return(os);
} // end of ostream& operator<<(ostream&, const List&)
} // end of namespace DS
} // end of namespace CSE224
Interface (Stack)
[edit | edit source]#ifndef STACK_HXX
#define STACK_HXX
#include <iostream>
using namespace std;
#include "ds/exceptions/Stack_Exceptions"
using namespace CSE224::DS::Exceptions;
#include "ds/List"
namespace CSE224 {
namespace DS {
The List
class offers a superset of the functionality expected from a stack. This may at first lead us to think that we may define a new class, Stack
, and have it (publicly) derive from the List
class. The problem with this approach is that the public interface of the base class will be exposed as part of the public interface of the derived class. Not really what we would want in this case: the List
class offers a lot more than what we would expect from the Stack
class. For this reason we should resort to some other method, such as composition.
C++ offers an alternative: private inheritance. Using private inheritance, the derived class can still make use of the functionality offered by the base class but the base class interface is not exposed through the derived class. For this reason, Stack
class privately inherits from the List
class.
class Stack : private List {
public:
Now that the derived class can reuse the base class functionality but do not expose it to its users, this type of inheritance is also called implementation inheritance. For a similar reason, public inheritance is also called interface inheritance.
We do not need to write the functions of the orthodox canonical form because compiler-synthesized versions provide the equivalent of what we are required to do. This is basically because the only data field of Stack
is the List
sub-object it inherits from List
.
// Stack(void);
// Stack(const Stack&);
// ~Stack(void);
// Stack& operator=(const Stack&);
// bool operator==(const Stack&);
double peek(void) throw(Stack_Empty);
double pop(void) throw(Stack_Empty);
void push(double new_item);
Thanks to the following statement, we selectively expose a function from the privately inherited base class. It’s as if the is_empty
function(s) from the List
class were publicly inherited.
using List::is_empty;
}; // end of Stack class
} // end of namespace DS
} // end of namespace CSE224
#endif
Implementation (Stack)
[edit | edit source]#include <iostream>
using namespace std;
#include "ds/Stack"
#include "ds/exceptions/Stack_Exceptions"
using namespace CSE224::DS::Exceptions;
namespace CSE224 {
namespace DS {
double Stack::
peek(void) throw(Stack_Empty) {
double ret_val;
Users of our class should not be aware of how we implement the Stack
class. That’s why we need to re-throw the exceptions thrown by the List
class so that it makes more sense to the user.
try { ret_val = get_first();}
catch(List_Empty) { throw Stack_Empty(); }
return ret_val;
} // end of double Stack::peek(void)
void Stack::
push(double new_item) { List::insert_in_front(new_item); }
double Stack::
pop(void) throw(Stack_Empty) {
double ret_val;
try { ret_val = remove_from_front(); }
catch(List_Empty) { throw Stack_Empty(); }
return ret_val;
} // end of double Stack::pop(void)
} // end of namespace DS
} // end of namespace CSE224
Virtual Inheritance
[edit | edit source]With the possibility of multiple inheritance rises the issue of the so-called virtual inheritance. Consider the class hierarchy shown in Figure 2. Question that awaits your answer is: How many Animal
sub-objects will there be in a Politician
object? Looking at the figure the correct answer seems to be two. However, our logic tells us a different story: there can be only one Animal
sub-object in a Politician
.
Whichever one is the right answer, there might be cases where either one turns out to be a better choice. We must find a way to tell the difference between the options. This is where the notion of virtual inheritance comes into the picture. We define the Humanoid
and Ape
classes to be virtually derived from Animal
.
Example: Virtual inheritance |
---|
|
Thanks to these definitions, there is now only one Animal
sub-object in a Politician
. This is achieved by assuring that pointers—not objects themselves—are inserted into the derived classes. That is; instead of containing two Animal
sub-objects, a Politician
object now has two pointers both pointing to the same Animal
sub-object.
Note use of virtual inheritance causes the order of constructor call to be changed: Virtual base classes are always constructed prior to non-virtual base classes regardless of where they appear in the inheritance hierarchy.
A typical use of virtual inheritance involves implementation of mix-in classes. Mix-in classes are used for tuning the behavior of a base class and can be combined to obtain even more specialized classes. For example, using the following code one can create windows with different styles: plain window, window with menu, window with border, and window with menu and border. As a matter of fact, we can come up with our own mix-in, say scroll-bar mix-in, and get scroll-bar-supporting versions of these window styles.
Example: Implementing mix-ins by virtual inheritance. |
---|
|
Note the number of window styles grows exponentially with the number of mix-ins. But, thanks to virtual inheritance, we do not have to consider each and every combination. We start out with the base class and a few mix-ins. As we need more refined window styles we come up with a new class inheriting from the relevant mix-in classes. If we see certain attributes are missing from the mix-in classes we can write our own mix-in and use it like the others.
Implementing Polymorphism
[edit | edit source]In this section, we take a look at two widely used methods of implementing polymorphism. It is worth noting that both rely on dynamically dispatching the function call to the relevant [function] entry point. In other words, polymorphism is implemented by means of dynamic dispatch. Another point to make is the tool we use for expressing polymorphism: inheritance.
Mentioning polymorphism, inheritance, and dynamic dispatch in different contexts may make some think of them as unrelated concepts. This, however, is utterly wrong. Truth of the matter is, in an object-oriented context these are complementary concepts and cooperate for enabling the implementation of the is-a relation in a natural way. In order to express an is-a relation between two classes we need help from both inheritance and polymorphism: inheritance is used to define a common message protocol between the base and derived classes, whereas polymorphism is needed for providing the behavioral variability.[4] This is further made possible by dynamically dispatching the messages.
As was mentioned in previous section, inheritance without polymorphism leads to inflexible, object-based solutions. Likewise, polymorphism alone is generally not what you want. So, it is well-advised that you consider using these two together.
Previous remarks should not make you think we are bound to use the duo of inheritance and polymorphism only. Our success in software industry depends on producing reliable, easily-extensible, efficient software. The keyword in reaching this goal is reuse and aforementioned concepts are not without alternatives. Apart from the age-old composition technique, we can use generics for instance. Parameterizing a class or a subprogram will also give us the benefits of reuse. An example to the former is given in the Parameterized Types chapter, while use of the latter is provided below. Thanks to this method, users can sort any array provided that a Comparator<V>
object for the component type is also supplied.
Example: Generic methods in Java. |
---|
|
Such a method is said to provide parametric polymorphism. It is polymorphic in the sense that same method does the same thing for parameters of different types; to the user of the method, it looks as though there were separate methods for each different type.[5]
A similar effect can be experienced in the case of overloaded subprograms, where calls with different argument lists are dispatched to subprograms with different signatures and users get different behavior as a result of calling the seemingly same subprogram. For this reason, overloading is sometimes referred to as ad-hoc polymorphism.[6]
Having done away with the confusion about inheritance and polymorphism, let's move on to the techniques commonly used in implementing polymorphism. But before we do that, we will give a listing of the sample classes used in our presentation.
class B1 { ... public: virtual void f(int); void fs(int); ... ... } ; // end of base class B1 class B2 { ... public: virtual void f(int); virtual void g(void); ... ... } ; // end of base class B2 class D : public B1, public B2 { ... public: void f(int); ... ... }; // end of derived class D
vtables
[edit | edit source]The vtable technique, generally used in compilers of the PC world, utilizes a table containing rows of function entry address and offset pair. Objects of all classes with at least one dynamically dispatched function has a pointer, called the vptr, pointing to the start of this table. The offset column is used for adjusting the value of the this
pointer. This adjustment is required when an object of a derived class is used through a pointer of a base class. Take the definition of D
, whose object memory layout is given below. Given an object of D
, we can use it through pointers of D
and any type that is an ancestor type of D
, which in our case are D*
, B1*
, and B2*
. Put another way, through a variable of type B1*
we can manipulate any object that is an instance of a class deriving from B1
, which in our case are D
and B1
. Accordingly, the following is possible.
... D* d_obj = new D(...); B1* b1_obj = new B1(...); B2* b2_obj = new B2(...); ... fd(D* d_par) { ... d_par->f(...); d_par->g(...); ... } // end of ... fd(D*) ... fb1(B1* b1_par) { ... b1_par->f(...); ... ... } // end of ... fb1(B1*) ... fb2(B2* b2_par) { ... b2_par->f(...); b2_par->g(...); ... } // end of ... fb2(B2*) ... fd(d_obj) ...; ... fb1(b1_obj) ...; ... fb1(d_obj) ...; ... fb2(b2_obj) ...; ... fb2(d_obj) ...;
Having overridden f
in the derived class (D
) means invoking this version of f
will potentially make use of all properties found in B1
, B2
, and D
, which implies the receiver object of this function must at least be a D
object. As was mentioned before such an object can also be manipulated through B2*
. This is exemplified by executing line 7 of the above fragment following the call on line 15. Note also, upon executing the function call on line 14, the function call on line 7 is dispatched with a B2
object. Since b2_par
is of type B2*
, both cases are handled via the vptr field of a B2
object. However, in one case this object is B2
part of a D
object whereas in the other it is a plain B2
object. This is shown in figures above.
B2
as part of a D
object needs our attention. Starting address of the object (D
) and the pointer used for manipulating this object (B2*
) indicate different locations in memory. This requires that—in order to enable use of all properties of a D
object—we adjust the pointer value as many bytes as there are before the B2
sub-object, which is referred to as delta(B2
) in our figure.
Adjustor Thunks
[edit | edit source]The second technique we will look at can be seen as a less portable[7] optimization on the first one. As in the vptr technique, one column in the vtable contains a pointer-to-function. But we now utilize thunks instead of the adjustment column. In case an adjustment to this is needed, pointer-to-function in the one and only column of the vtable points to the thunk code generated by the compiler, which modifies this and jumps to the function entry. If no adjustment to this is required, the pointer-to-function contains the address of the entry of the function to be invoked.
Complications due to Multiple Inheritance
[edit | edit source]Observe implementation of polymorphism is complicated by requirements of multiple [implementation] inheritance. Did we content ourselves with single inheritance an object would always have a single vptr and we would never need to make any adjustments to this.
Question: Do we face the same complications while implementing multiple interface inheritance? As a starting point consider memory layouts of objects of a class that implements multiple interfaces or realizes an interface that multiply inherits from other interfaces.
Constructor and Destructor Call Order
[edit | edit source]Object construction involves allocating memory for the object and initializing its contents [and acquiring outside resources, if needed] by means of a call to the relevant constructor. This atomic sequence is triggered on three conditions:
- A variable with static extent is defined: In the case of a global variable, memory for the object is allocated in the static data region at compile time and constructor call [that is, initialization and resource acquisition] is executed as the first statement of the program. If there is more than one such variable, constructors are executed in the order in which the corresponding definitions occur in the text. Static local variables differ in two aspects: call to the constructor is executed once upon first entry to the function and this call does not modify the order of statement execution in the function.
- A block variable is defined: Both allocation and initialization take place at run-time each time control flow reaches the point of the variable definition.
- An object is created using the new operator: Object is allocated and initialized as the control flow reaches the statement where the new operator is applied.
Example: Construction order of variables with static extent. |
---|
|
→ |
In the constructor of C... // Global variable c_g First statement in f... In the constructor of C... // Static local variable c_sl Last statement in f... In main... First statement in f... Last statement in f... |
In case there may not be any programmer-defined constructors, the C++ compiler synthesizes a default constructor that makes calls to default constructors of the sub-objects. In an object with primitive type fields—since primitive types do not support the notion of constructor—this means no constructor call is ever made, which leaves the newly created object in a random state.
Complementing construction, destruction of the object is accomplished by a call to the destructor function, which is followed by release of the object's memory. The destructor, if there is any, is meant to release resources- both heap memory and outside resources- being used by the soon-to-be recycled object.
Destructor invocations of static variables take place following the last statement of the program while block variables are destroyed upon exit from the block they are defined in.[8] In the presence of multiple variable declarations in the same scope, the order of destructor calls for both kinds of variables is the reverse of the constructor call order.
An Object with Primitive Type Fields
[edit | edit source]Unlike Java and C#, where primitive type fields are guaranteed to have specific initial values, C++ does not perform any implicit initialization of such fields. In other words, unless provided by the programmer, such fields are left uninitialized.
Example: |
---|
|
→ |
... In the C1 constructor taking an int... i = 3 // for o3 In the default constructor of C1 // for o In the C1 constructor taking an int... i = 1 // for o1 In the C1 constructor taking an int... i = 2 // for o2 In the destructor of C1... i = 3 // for o3 In the destructor of C1... i = 2 // for o2 In the destructor of C1... i = 1 // for o1 In the destructor of C1... i = 138645 // for o ... |
An Object Composed of Sub-object(s)
[edit | edit source]In the process of creating a composite object, a call to the constructor [of the composite object] is preceded by the relevant constructor call(s) for the sub-object(s). Unless these calls are explicitly made in the member initialization list, sub-objects are initialized using the default constructor(s).
Example: |
---|
|
→ |
... In the default constructor of C1 // for o._o In the default constructor of C2 // for o In the destructor of C2 // for o In the destructor of C1 // for o._o ... |
In the case of multiple sub-objects constructor calls are made in the order in which they occur in the class definition.
Example: An object with multiple sub-objects. |
---|
|
→ |
... In the default constructor of C1 // for o._o1 In the default constructor of C1 // for o._o2 In the default constructor of C3 // for o In the destructor of C3 // for o In the destructor of C1 // for o._o2 In the destructor of C1 // for o._o1 ... |
Creating an Array of Objects
[edit | edit source]A facility to define group of variables belonging to the same type, an array variable is initialized by initializing each and every one of its components. Similarly, destroying this array requires destruction of each and every one of its components.
Example: |
---|
|
→ |
... In the default constructor of C1 // for o[0]._o In the default constructor of C2 // for o[0] In the default constructor of C1 // for o[1]._o In the default constructor of C2 // for o[1] In the destructor of C2 // for o[1] In the destructor of C1 // for o[1]._o In the destructor of C2 // for o[0] In the destructor of C1 // for o[0]._o ... |
Example: An object with an array of sub-objects. |
---|
|
→ |
... In the default constructor of C1 // for o._o[0] In the default constructor of C1 // for o._o[1] In the default constructor of C4 // for o In the destructor of C4 // for o In the destructor of C1 // for o._o[1] In the destructor of C1 // for o._o[0] ... |
Example: |
---|
|
→ |
... In the default constructor of C1 // for o._o[0]._o In the default constructor of C2 // for o._o[0] In the default constructor of C1 // for o._o[1]._o In the default constructor of C2 // for o._o[1] In the default constructor of C5 // for o In the destructor of C5 // for o In the destructor of C2 // for o._o[1] In the destructor of C1 // for o._o[1]._o In the destructor of C2 // for o._o[0] In the destructor of C1 // for o._o[0]._o ... |
Inheritance
[edit | edit source]Seeing inheritance as "compiler-managed composition" is key to figuring out constructor and destructor call order. Rest is all the same.
Example: |
---|
|
First line of the above fragment can be thought of having been converted by the compiler to the following. Note the identifier name is arbitrary and cannot be in any way referenced in the class implementation.
class IC1 { public: C1 _c1_part; private:
→ |
... In the default constructor of C1 // for C1 part of o1 In the default constructor of IC1 // for o1 In the destructor of IC1 // for o1 In the destructor of C1 // for C1 part of o1 ... |
Example: Inheritance and composition. |
---|
|
→ |
... In the default constructor of C1 // for C1 part of o1 In the default constructor of C1 // for o1._o In the default constructor of IC2 // for o1 In the destructor of IC2 // for o1 In the destructor of C1 // for o1._o In the destructor of C1 // for C1 part of o1 ... |
Member Initialization List
[edit | edit source]Unless otherwise told all implicit constructor calls are made to the default constructor. This behavior can be changed by appending member initialization list(s) to the function header of the constructor(s).
Example: |
---|
|
For pedagogical purposes, line 4 of the above fragment can be seen as the following. However, since it replaces two sequences of "an initialization followed by an assignment" with two initializations, using a member initialization list is a more efficient choice. This is because—even when you don't have a member initialization list—constructor(s) of the sub-objects are called before that of the composite object, which means the two lines in the following fragment are actually assignments, not initializations.[9] Before> they get executed each sub-object will have already been initialized using the default constructor of C1
.
IC3(int i) { _c1_part = C1(1); _o = C1(1);
→ |
... In the C1 constructor taking an int // for C1 part of o1 In the C1 constructor taking an int // for o1._o In the IC3 constructor taking an int // for o1 In the destructor of IC3 // for o1 In the destructor of C1 // for o1._o In the destructor of C1 // for C1 part of o1 ... |
Multiple Inheritance
[edit | edit source]Building on our informal definition of inheritance as compiler-managed composition we can treat objects of multiply inheriting classes as being composed of more than one sub-object. Therefore, for pedagogical purposes, we can accordingly consider line 1 of the following fragment as below.
class IC4 { public: C1 _c1_part; C2 _c2_part;
Example: |
---|
class IC4 : public C1, public C2 {
public:
IC4(void) { cout << "In the default constructor of IC4" << endl; }
~IC4(void) { cout << "In the destructor of IC4" << endl; }
}; // end of class IC4
...
{
IC4 o1;
...;
}
...
|
→ |
... In the default constructor of C1 // for C1 part of o1 In the default constructor of C1 // for _o of the C2 part of o1 In the default constructor of C2 // for C2 part of o1 In the default constructor of IC4 // for o1 In the destructor of IC4 // for o1 In the destructor of C2 // for C2 part of o1 In the destructor of C1 // for _o of the C2 part of o1 In the destructor of C1 // for C1 part of o1 ... |
Restating the Formula: Inheritance is Compiler-Managed Composition
[edit | edit source]What follows are three pairs of equivalent class definitions meant to provide an insight into what the compiler accomplishes behind the scenes. While perusing through the code keep in mind that code given in the right column reflects only what the compiler does, not how it does it.
class SC1 { ...; public: ...; void SC1_f1(...); void SC1_f2(...); ...; }; // end of class SC1
Public inheritance enables use of base class interfaces through that of the derived class. No effort is required on the programmer side, all is taken care of by the compiler. If for some reason you want to do it without inheritance, you must explicitly expose the functions of the base class and delegate calls to these functions to the corresponding functions in the base class. With private inheritance and selective exposition of the base class interface, this extra burden is lessened for C++ programmers.
|
|
Private derivation means the functionality in the base class is not visible through an object of the derived class. However, it is still possible to utilize this functionality in implementing the functions found in the interface of the derived class.
|
|
Sometimes a mixture of the two cases may be needed. That is; part of the functionality in the base class is visible and rest has to to be hidden. This selective exposition can be accomplished by a combination of private inheritance and the using
declaration.
|
|
Virtual Inheritance
[edit | edit source]Object of a virtual base class is always constructed prior to the objects of non-virtual classes. It should be kept in mind that "virtualness" is actually the property of the derivation, not that of the base class itself.
Example: |
---|
|
→ |
... In the default constructor of C1 In the default constructor of VC2 In the destructor of VC2 In the destructor of C1 ... |
Example: |
---|
|
→ |
... In the default constructor of C1 In the default constructor of VC2 In the default constructor of VC3 In the default constructor of VC4 In the destructor of VC4 In the destructor of VC3 In the destructor of VC2 In the destructor of C1 ... |
Notes
[edit | edit source]- ↑ One other option would be to make use of the code generation aspect of Java annotations.
- ↑ The only thing that lacks in this picture is garbage-collection. A partial solution to this will be provided by means of smart pointers, which will be introduced in the implementation part.
- ↑ Reuse is usually taken to be code reuse. However, analysis and design documents, program code, test cases and test code are all candidates for reuse. As a matter of fact, reuse of artifacts from the early stages of the software production process has a larger impact on productivity.
- ↑ We can achieve the same result by defining the common message protocol in an interface and get classes to implement this interface.
- ↑ Notice it is once again dynamic dispatch that makes the magic work. It is not known which method the
compare
message will be dispatched to until the program is run and line 6 is executed. Use of pointer-to-function in the equivalent C code of Callback section in the Sample C Programs chapter is a testimony to this. - ↑ Unlike other contexts where the word "polymorphism" passes, the function call—by matching the arguments with the signature of the function—is resolved at compile- or link-time and therefore statically dispatched.
- ↑ For instance, some architectures do not allow the goto instruction into the body of another function.
- ↑ Destructor invocations of dynamically allocated objects take place at the point of the relevant application of
delete
. - ↑ This belies our definition of a constructor, which states that a constructor is used to initialize an object. As far as C++ is concerned, what takes place in a constructor is assignment; initialization is made using the member initialization list.