A class
The class is the C++ construct for encapsulation. Encapsulation means publishing an interface through which you make things happen, and hiding the implementation and data necessary to do the job. A class is used to hide data, and publish operations on the data, at the same time. Let's look at the "Range" example from last month, but this time make it a class. The only operation that we allowed on the range last month was that of construction, and we left the data visible for anyone to use or abuse. What operations do we want to allow for a Range class? I decide that 4 operations are desirable:
Construction (same as last month.)
find lower bound.
find upper bound.
ask if a value is within the range. The second thing to ask when wishing for a function is (the first thing being what it's supposed to do) is in what ways things can go wrong when calling them, and what to do when that happens. For the questions, I don't see how anything can go wrong, so it's easy. We promise that the functions will not throw C++ exceptions by writing an empty exception specifier.
I'll explain this class by simply writing the public interface of it:
struct BoundsError {};
class Range
{
public:
Range(int upper_bound = 0, int lower_bound = 0)
throw (BoundsError);
// Precondition: upper_bound >= lower_bound
// Postconditions:
// lower == upper_bound
// upper == upper_bound
int lowerBound() throw ();
int upperBound() throw ();
int includes(int aValue) throw ();
private:
// implementation details.
};
This means that a class named "Range" is declared to have a constructor, behaving exactly like the constructor for the "Range" struct from last month, and three member functions (also often called methods,) called "lowerBound", "upperBound" and "includes". The keyword "public," on the fourth line from the top, tells that the constructor and the three member functions are reachable by anyone using instances of the Range class. The keyword "private" on the 3rd line from the bottom, says that whatever comes after is a secret to anyone but the "Range" class itself. We'll soon see more of that, but first an example (ignoring error handling) of how to use the "Range" class:
int main(void)
{
Range r(5);
cout << "r is a range from " <<>> i;
if (i == 0)
break;
cout << upper_bound =" 0," lower_bound =" 0)">= lower_bound
// Postconditions:
// lower == upper_bound
// upper == upper_bound
int lowerBound() throw ();
int upperBound() throw ();
int includes(int aValue) throw ();
private:
int lower;
int upper;
};
Range::Range(int upper_bound, int lower_bound)
throw (BoundsError)
: lower(lower_bound), /***/
upper(upper_bound) /***/
{
// Preconditions.
if (upper_bound <>= lower && aValue <= upper; /***/ } First, you see that the constructor is identical to that of the struct from last month. This is no coincidence. It does the same thing and constructors are constructors. You also see that "lowerBound", "upperBound" and "includes", look just like normal functions, except for the "Range::" thing. It's the "Range::" that ties the function to the class called Range, just like it is for the constructor. The lines marked /***/ are a bit special. They make use of the member variables "lower_bound" and "upper_bound." How does this work? To begin with, the member functions are tied to instances of the class, you cannot call any of these member functions without having an instance to call them on, and the member functions uses the member variables of that instance. Say for example we use two Range instances, like this: Range r1(5,2); Range r2(20,10); Then r1.lowerBound() is 2, r1.upperBound() is 5, r2.lowerBound() is 10 and r2.upperBound() is 20. So how come the member functions are allowed to use the member data, when it's declared private? Private, in C++, means secret for anyone except whatever belongs to the class itself. In this case, it means it's secret to anyone using the class, but the member functions belong to the class, so they can use it. So, where is the advantage of doing this, compared to the struct from last month? Hiding data is always a good thing. For example, if we, for whatever reason, find out that it's cleverer to represent ranges as the lower bound, plus the number of valid values between the lower bound and upper bound, we can do this, without anyone knowing or suffering from it. All we do is to change the private section of the class to: private: int lower_bound; int nrOfValues; And the implementation of the constructor to: Range::Range(int upper_bound, int lower_bound) throw (BoundsError) : lower(lower_bound), /***/ nrOfValues(upper_bound-lower_bound) /***/ ... And finally the implementations of "upperBound" and "includes" to: int Range::upperBound() throw () { return lower+nrOfValues; } int Range::includes(int aValue) throw () { return aValue >= lower && aValue <= lower+nrOfValues; } We also have another, and usually more important, benefit; a promise of integrity. Already with the struct, there was a promise that the member variable "upper" would have a value greater than or equal to that of the member variable "lower". How much was that promise worth with the struct? This much: Range r(5, 2); r.lower = 25; // Oops! Now r.lower > r.upper!!!
Try this with the class. It won't work. The only one allowed to make changes to the member variables are functions belonging to the class, and those we can control.
Destructor
Just as you can control construction of an object by writing constructors, you can control destruction by writing a destructor. A destructor is executed when an instance of an object dies, either by going out of scope, or when removed from the heap with the delete operator. A destructor has the same name as the class, but prepended with the ~ character, and it never accepts any parameters. We can use this to write a simple trace class, that helps us find out the life time of objects.
#include
class Tracer
{
public:
Tracer(const char* tracestring = "too lazy, eh?");
~Tracer(); // destructor
private:
const char* string;
};
Tracer::Tracer(const char* tracestring)
: string(tracestring)
{
cout << "+ " << string << endl;
}
Tracer::~Tracer()
{
cout << "- " << string << endl;
}
What this simple class does is to write its own parameter string, prepended with a "+" character, when constructed, and the same string, prepended by a "-" character, when destroyed. Let's toy with it!
int main(void)
{
Tracer t1("t1");
Tracer t2("t2");
Tracer t3;
for (unsigned u = 0; u < 3; ++u)
{
Tracer inLoop("inLoop");
}
Tracer* tp = 0;
{
Tracer t1("Local t1");
Tracer* t2 = new Tracer("leaky");
tp = new Tracer("on heap");
}
delete tp;
return 0;
}
When run, I get this behaviour (and so should you, unless you have a buggy compiler):
[d:\cppintro\lesson2]tracer.exe
+ t1
+ t2
+ too lazy, eh?
+ inLoop
- inLoop
+ inLoop
- inLoop
+ inLoop
- inLoop
+ Local t1
+ leaky
+ on heap
- Local t1
- on heap
- too lazy, eh?
- t2
- t1
What conclusions can be drawn from this? With one exception, the object on heap, objects are destroyed in the reversed order of creation (have a careful look, it's true, and it's always true.) We also see that the object, instantiated with the string "leaky" is never destroyed.
What happens with classes containing classes then? Must be tried, right?
class SuperTracer
{
public:
SuperTracer(const char* tracestring);
~SuperTracer();
private:
Tracer t;
};
SuperTracer::SuperTracer(const char* tracestring)
: t(tracestring)
{
cout << "SuperTracer(" << tracestring << ")" << endl;
}
SuperTracer::~SuperTracer()
{
cout << "~SuperTracer" << endl;
}
int main(void)
{
SuperTracer t1("t1");
SuperTracer t2("t2");
return 0;
}
What's your guess?
[d:\cppintro\lesson2]stracer.exe
+ t1
SuperTracer(t1)
+ t2
SuperTracer(t2)
~SuperTracer
- t2
~SuperTracer
- t1
This means that the contained object ("Tracer") within "SuperTracer" is constructed before the "SuperTracer" object itself is. This is perhaps not very surprising, looking at how the constructor is written, with a call to the "Tracer" class constructor in the initialiser list. Perhaps a bit surprising is the fact that the "SuperTracer" objects destructor is called before that of the contained "Tracer", but there is a good reason for this. Superficially, the reason might appear to be that of symmetry, destruction always in the reversed order of construction, but it's a bit deeper than that. It's not unlikely that the member data is useful in some way to the destructor, and what if the member data is destroyed when the destructor starts running? At best a destructor would then be totally worthless, but more likely, we'd have serious problems properly destroying our no longer needed objects.
So, the curious wonders, what about C++ exceptions? Now here we get into an interesting subject indeed! Let's look at two alternatives, one where the constructor of "SuperTracer" throws, and one where the destructor throws. We'll control this by a second parameter, zero for throwing in the constructor, and non-zero for throwing in the destructor. Here's the new "SuperTracer" along with an interesting "main" function.
class SuperTracer
{
public:
SuperTracer(int i, const char* tracestring)
throw (const char*);
~SuperTracer() throw (const char*);
private:
Tracer t;
int destructorThrow;
};
SuperTracer::SuperTracer(int i, const char* tracestring)
throw (const char*)
: t(tracestring),
destructorThrow(i)
{
cout << "SuperTracer(" << tracestring << ")" << endl;
if (!destructorThrow)
throw (const char*)"SuperTracer::SuperTracer";
}
SuperTracer::~SuperTracer() throw (const char*)
{
cout << "~SuperTracer" << endl;
if (destructorThrow)
throw (const char*)"SuperTracer::~SuperTracer";
}
int main(void)
{
try {
SuperTracer t1(0, "throw in constructor");
}
catch (const char* p)
{
cout << "Caught " << p << endl;
}
try {
SuperTracer t1(1, "throw in destructor");
}
catch (const char* p)
{
cout << "Caught " << p << endl;
}
try {
cout << "Let the fun begin" << endl;
SuperTracer t1(1, "throw in destructor");
SuperTracer t2(0, "throw in constructor");
}
catch (const char* p)
{
cout << "Caught " << p << endl;
}
return 0;
}
Here we can study different bugs in different compilers. Both GCC and VisualAge C++ have theirs. What bugs does your compiler have? Here's the result when running with GCC. Comments about the bug found are below the result:
[d:\cppintro\lesson2]s2tracer.exe
+ throw in constructor
SuperTracer(throw in constructor)
- throw in constructor
Caught SuperTracer::SuperTracer
+ throw in destructor
SuperTracer(throw in destructor)
~SuperTracer
Caught SuperTracer::~SuperTracer
Let the fun begin
+ throw in destructor
SuperTracer(throw in destructor)
+ throw in constructor
SuperTracer(throw in constructor)
- throw in constructor
~SuperTracer
Abnormal program termination
core dumped
The first 4 lines tell that when an exception is thrown in a constructor, the destructor for all so far constructed member variables are destructed, through a call to their destructor, but the destructor for the object itself is never run. Why? Well, how do you destroy something that was never constructed? The next four lines reveal the GCC bug. As can be seen, the exception is thrown in the destructor, however, the member Tracer variable is not destroyed as it should be (VisualAge C++ handles this one correctly.) Next we see the interesting case. What happens here is that an object is created that throws on destruction, and then an object is created that throws at once. This means that the first object will be destroyed because an exception is in the air, and when destroyed it will throw another one. The correct result can be seen in the execution above. Program execution must stop, at once, and this is done by a call to the function "terminate". The bug in VisualAge C++ is that it destroys the contained Tracer object before calling terminate.
What's the lesson learned from this? To begin with that it's difficult to find a compiler that correctly handles exceptions thrown in destructors. More important, though, think *very* carefully, before allowing a destructor to throw exceptions. After all, if you throw an exception because an exception is in the air, your program will terminate very quickly. If you have a bleeding edge compiler, you can control this by calling the function "uncaught_exception()" (which tells if an exception is in the air,) and from there decide what to do, but think carefully about the consequences
No comments:
Post a Comment