Escolar Documentos
Profissional Documentos
Cultura Documentos
Int
Part Part Part Part Part Part Part Part Part Part1 Part1 Part1 Part1
1 2 3 4 5 6 7 8 9 0 1 2 3
Classes
Last month we saw, among others, how we can give a struct well defined values by using constructors, and how C+
+ exceptions aid in error handling. This month we'll look at classes, a more careful study of object lifetime,
especially in the light of exceptions. The stack example from last month will be improved a fair bit too.
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:
int main(void)
{
Range r(5);
cout << "r is a range from " << r.lowerBound() << " to "
<< r.upperBound() << endl;
int i;
for (;;)
{
cout << "Enter a value (0 to stop) :";
cin >> i;
if (i == 0)
break;
cout << endl << i << " is " << "with"
<< (r.includes(i) ? "in" : "out") << " the range"
<< endl;
}
return 0;
}
A test drive might look like this:
[d:\cppintro\lesson2]rexample.exe
r is a range from 0 to 5
Enter a value (0 to stop) :5
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;
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 <iostream.h>
class Tracer
{
public:
Tracer(const char* tracestring = "too lazy, eh?");
~Tracer(); // destructor
private:
const char* string;
};
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;
class SuperTracer
{
public:
SuperTracer(const char* tracestring);
~SuperTracer();
private:
Tracer t;
};
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;
};
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
An improved stack
The stack from last month was in many ways better than a corresponding C implementation, but it was far from
adequate. An easy, C-ish way of improving it, is to implement it as an abstract data type, where functions push, pop,
and whatever's needed is available to the users. The C++ way is, not surprisingly, to write a stack class. Before
going into that, though, some thinking is needed regarding what the stack should do.
Minimum for a stack is functionality to push new elements onto it, and to pop the top element from it. The pop
function is a classical headache, because it both changes the state of the stack (removes the top element from it) and
returns whatever was the top element. This behaviour is dangerous in terms of errors, because you can easily lose
data. What if something fails while removing the top element? Should you return the top element value? If you do,
does that indicate that the it has been removed? It's better to make two functions of it, one that returns the top
element, and one that removes it. The one that removes it either returns or throws an exception (remember, either a
function fails, or does what it's supposed to do, there's no middle way. If it fails, it exits through an exception,
otherwise it returns.)
OK, so, we can see a class that, on the surface, looks something like this:
class intstack
{
public:
intstack(); // initialise empty stack
~intstack(); // free memory by popping all elements
void push(int aValue);
void pop(); // remove top element
int top(); // retrieve value of top element
private:
// implementation details
};
This looks fair. Normally copying and assignment (a = b) would be implemented too, but we'll wait with that until
next month, or this article will grow far too long. Now let's look at what can go wrong in the different operations.
• top. What if the stack is empty? It mustn't be.
• pop. Again, what if the stack is empty?
• push. Out of memory.
• construction. Nothing really.
• destruction. Tough one. If the stack is in a bad state, it might be indestructible.
Since top and pop requires that the stack isn't empty, we must allow the user to check if the stack is empty,
otherwise we don't leave them a chance, so another function is needed.
• isEmpty. I don't see how anything can go wrong in here.
So, with the problems identified, let's think about what to do when they occur.
• top and pop on empty stack, throw exception, stack remains empty.
• Out of memory on push. Throw exception and leave stack unchanged.
• invalid stack state in destruction? Can we find out of we have them? I don't think we can, without adding
significant control data, that probably increases the likelihood of exactly the kind of errors we want to
avoid. Thus, I *think* the best solution for this problem is to just be careful with the coding, and hope it
doesn't happen.
This leaves us with two different errors: Stack underflow (pop or top on empty stack), and out of memory.
We also found, rather easily, the preconditions for operations pop and top (!isEmpty().)
Now to think of post conditions. What's the post conditions for the different operations?
push(anInt): The stack can't be empty after that (post conditions always reflect successful completion, not failure.)
Also top() == anInt.
pop(): Currently no way to say, but let's change things a bit. Instead of having the method isEmpty() we add the
method nrOfElements(), then nrOfElements will be one less after pop.
top(): nrOfElements() same after as before.
Construction (from nothing): nrOfElements() == 0.
Destruction? Nothing. There's no object left to check the post condition on! We can state a post condition that all
memory allocated by the stack object is deallocated, but we can't check it (try to think of a method to do that, and
tell me if you find one.)
So, now we can write the public interface of the stack:
class intstack
{
public:
intstack() throw ();
// Preconditions: -
// implementation details
};
The promise to always leave the stack unchanged in when exceptions occur means that we must guarantee that
whatever internal data structures we're dealing with must always be destructible. This is tricky, but it can be done.
This requirement is also implied by our destructor guaranteeing not to throw anything.
*1*: the structs stack_underflow and bad_alloc are empty, we just throw them, and use the struct itself as the
information, nothing more is needed. For really new compilers, the new operator throws a pre-defined class called
bad_alloc. If you have such a compiler, remove the declaration of it above.
*2*: This looks odd, perhaps, but what this means is that if there are elements on the stack, the top elements must be
the same. Or literally as it says in the code comment, either the stack is empty, or the top elements are equal. You'll
get used to this reversed looking logic.
*3*: This is how the assignment operator looks like, if included, and below it, the copy constructor (constructing a
new stack by copying the contents of an old one.) I said we wouldn't implement these this month, and ironically that
is why they are declared private. The reason is that if you don't declare a copy constructor and assignment operator,
the C++ compiler will do it for you, and unfortunately, the compiler generated ones are usually not the ones you'd
want. I'll talk more about this next month. By declaring them private, however, coping and assignment is explicitly
illegal, so it's not a problem.
So, how do we implement this then? Why not like the one from last month, but with an additional element counter? I
think that's a perfectly reasonable approach. Here comes the complete class declaration, with the old
"stack_element" as a nested struct within the class.
class intstack
{
public:
intstack() throw ();
// Preconditions: -
struct stack_element
{
stack_element(int aValue, stack_element* p) throw ()
: value(aValue), pNext(p) {};
int value;
stack_element* pNext;
};
stack_element* pTop;
unsigned elements;
};
The only peculiarity here is that the constructor for the nested struct "stack_element" is defined in line (i.e. at the
point of declaration.) As a rule of thumb, this should be avoided, but it's OK for trivial member functions, like this
constructor, which only copies values.
So let's look at the implementation, bit by bit.
intstack::intstack() throw ()
: pTop(0),
elements(0)
{
// Preconditions: -
}
intstack::~intstack() throw ()
{
// Preconditions: -
// Postconditions:
// The memory allocated by the object is deallocated
while (pTop != 0)
{
stack_element* p = pTop->pNext;
delete pTop; // guaranteed not to throw.
pTop = p;
}
}
These are rather straight forward. The guarantee that "delete pTop" doesn't throw comes from the fact that the
destructor for "stack_element" can't throw (which is because we haven't written anything that can throw, and the
contents of "stack_element" itself can't throw since it's fundamental data types only.)
// Postconditions:
// nrOfElements() == 0 || top() == old top()
try {
pTop = pTmp;
++elements;
// Postconditions:
// nrOfElements() == 1 + old nrOfElements()
// top() == anInt
if (nrOfElements() != 1 + old_nrOfElements
|| top() != anInt)
{
throw pc_error();
}
}
catch (...) {
// Behaviour on exception:
// Stack unchanged.
pTop = pOld;
elements = old_nrOfElements;
throw;
}
}
This is not trivial, and also contains some news. Let's start from the beginning. "old_nrOfElements" is used both for
the post condition check that the number of elements after the push is increased by one, but also when restoring the
stack should an exception be thrown. The call to "nrOfElements" could throw "pc_error". If it does, the exception
passes "push" and to the caller since we're not catching it. This is harmless since we haven't done anything to the
stack yet. On the next line we store the top of stack as it was before the push. This is used solely for restoring the
stack in the case of exceptions. This assignment cannot throw since "pOld" and "pTop" are fundamental types
(pointers). On the next line a new stack element is created on the heap. Here there are three possibilities. Either the
creation succeeds as expected, in which case everything is fine, or we're out of memory (the only possible error
cause here since the constructor for "stack_element" cannot throw.) For most of you, an out of memory situation
will mean that the return value stored in "pTmp" is 0. That case is taken care of on the next two lines.
If you have a brand new compiler, on the other hand, operator new itself throws "bad_alloc" when we're out of
memory. If you have such a compiler, it'll most probably complain about the next two lines. If so, just remove them,
since they're unnecessary in that case. OK, either case, if we're out of memory here, "bad_alloc" will be thrown and
the stack will be unchanged. Next we start doing things that changes the stack, and since we promise the stack won't
be changed in the case of exceptions, things that do change the stack goes into a "try" block. Setting the new stack
top and incrementing the element counter is not hard to understand.
The post condition check is interesting, though. Here we have three situations in which an exception results. The call
to "nrOfElements" may throw, the call to "top" may throw, and the post condition check itself might fail, in which
case we throw ourselves. All these three situations are handled in the catch block. "catch (...)" will catch anything
thrown from within the try block above. What we do when catching something, is to free the just allocated memory
(which won't throw for the same reason as for the destructor.) We also restore the old stack top and the element
counter. Thus the stack is restored to the state it had before entering "push", without having leaked memory. Then,
what we must do, is to pass the error on to the caller of "push", and that is what the empty "throw;" does. An empty
"throw;" means to re throw whatever it was that was caught. A throw like this is only legal within a catch block (use
it elsewhere and see your program terminate rather quickly.)
if (nrOfElements() == 0 || pTop == 0)
{
throw stack_underflow();
}
return pTop->value;
// Postconditions:
// nrOfElements() == old nrOfElements()
if (nrOfElements() == 0 || pTop == 0)
{
throw stack_underflow();
}
// Postconditions:
// nrOfElements() + 1 == old nrOfElements()
if (nrOfElements() + 1 != old_elements)
{
// Behaviour on exception:
// Stack unchanged.
throw pc_error();
}
}
catch (...) {
elements = old_elements;
pTop = pOld;
throw;
}
delete pOld; // guaranteed not to throw.
}
The exception protection of "pop" works almost exactly the same way as with "push". The thing worth mentioning
here, though, is why "delete pOld" is located after the "catch" block and not within the "try" block. Suppose the
deletion did, despite its promise, throw something. If it did, it would be caught, and the top of stack would be left to
point to something undetermined. As it is now, if it breaks its promise, we too break our promise not to alter the
stack when leaving on exception, but we at least make sure the stack is in a usable (and most notably, destructible)
state.
After having spent this much time on writing this class, it's time to have a little fun and play with it, don't you think?
#include <iostream.h>
int main(void)
{
try {
cout << "Constructing empty stack is1" << endl;
intstack is1;
cout << "is1.nrOfElements() = "
<< is1.nrOfElements() << endl;
Exercise
1. Something that is badly missing in the stack implementation above, is a good check for the integrity of the
stack object itself. For example, what if we somehow manage to get "elements" to non-zero, while "pTop"
is 0? That's a terrible error that must not occur, and if it does, it must not go undetected. What I'd like you
to do, is to see what kind of "internal state" tests that can be done, and to implement them. Please discuss
your ideas with me over e-mail (this month, if I take a long time in responding, please don't feel ignored.
I'll be net-less most of August.)
2. It's generally considered to be a bad idea to have public data in a class. Can you think of why? Mail me
your reasons.
Recap
Again a month filled with news.
• You have seen how classes can be used to encapsulate internals and publish interfaces with the aid of the
access specifiers "public:" and "private:"
• Member functions are always called together with instances (objects) of the class, and thus always have
access to the member data of the class.
• A member function can access private parts of a class.
• Destruction of objects is done in the reversed order of construction, except when the objects are allocated
on the heap, in which case they're destroyed when the delete operator is called for a pointer to them.
• We have seen that throwing exceptions in destructors can be lethal. (This is not to say that it should never
ever be done, but that a lot of thought is required before doing so, to ensure that the program won't die all
of a sudden.)
• You can now iterate your way to a good design by thinking of 1. What the function should do. 2. What can
go wrong. 3. What should happen when something goes wrong. 4. How can a user of the class prevent
things from going wrong. When you have satisfactory answers to all four questions for all functionality of
your class, you have a safe design.
• You have seen how it is possible to, by carefully crafting your code, make your member functions
"exception safe" without being bloated with too many special cases ( "catch(...)" and "throw;" helps
considerably here.)
Next
Next month there will most probably be a break, since I'll be on a well needed vacation. After that, however, we'll
have a look at copy construction and assignment (together with a C++ idiom often referred to as the "orthodox
canonical form.") I promise to explain the references in more detail too.
Please, by the way, I need your constant feedback. Write me! I want opinions and questions. If you think I'm wrong
about things, going to fast, too slow, teaching the wrong things, whatever; tell me, ask me.
Part Part Part Part Part Part Part Part Part Part1 Part1 Part1 Part1
1 2 3 4 5 6 7 8 9 0 1 2 3
[NOTE: Here is a link to a zip of the source code for this article. Ed.]
So far we've covered error handling through exceptions, and encapsulation with classes. The feedback from part 2
tells me I forgot a rather fundamental thing; what exactly is encapsulation, what should we make classes of. What's
the meaning of a class? I will get to this, but first let's finally have a look at the promised references.
References
C++ introduces an indirect kind of type that C does not have, the reference. A reference is in itself not a type, it
always is a reference of some type, just like arrays are arrays of something and pointers are pointers to something.
A reference is a means of indirection. It's a way of reaching another variable. This may sound a lot like a pointer, but
don't confuse the two, they're very different. See a reference more as an alias for another variable. Some details
about references:
• References must be initialized to refer to something. There is no such thing as a 0 reference.
• Once bound to a variable there is no way to make the reference refer to something else.
• There is no such thing as "reference arithmetic."
• You cannot get the address of a reference. You can try to, but what you get is the address of the variable
referred to.
References are denoted with an unary "&", just the same ways as pointers are denoted with an unary "*". Let's have
a look at an example:
int main(void)
{
int i = 0;
int& x; // error, would be an unbound reference.
int& r = i; // r now refers to i.
++r; // now i == 1, r still refers to i.
if (&r != &i)
throw "Broken compiler";
return 0;
}
From this, one may wonder, what on earth are references for? Why would anyone want them? Well, for one, they
offer a certain security over pointers; it's so easy to get a pointer referring to something that doesn't exist, or
something else than the intended. They're also handy as a short-cut into long nested structs" and arrays. Here's an
example:
struct A {
int b[5];
int x;
char d;
};
struct C {
A* p[10];
int q;
};
C* pc;
A& ra = pc->p[2];
ra.b[3]=5; // pc->p[2]->b[3] = 5;
ra.b[4]=2; // pc->p[2]->b[4] = 2;
The reference in this case just makes life easier.
In parts 1 and 2, I used references when catching exceptions. References are also often used for parameters to
functions. If we look at the exceptions, it means that instead of getting a local copy of the thing thrown, we get a
reference to the thing thrown, and we can manipulate it if we want to, instead of manipulating our own copy. The
same goes for parameters to functions. Passing an object by reference instead of by value, can some times be
necessary, and sometimes beneficial in terms of performance. The reason for the performance benefit is that the
when passing a parameter by value, the object is copied; the function uses a local object of its own, that is identical
to the one you passed. If copying the object is an expensive operation, which in some cases it is, then passing by
reference is cheaper. However, passing parameters by reference can be dangerous. When you do, the function has
access to the very object you pass, and if the function modifies it, the caller better be prepared for that. A commonly
used way around this is to declare the parameter as a "const" reference. This means that you get a reference, as
before, but the reference is treated as was it a constant, and because of this all attempts to change its value will cause
a compile time error.
Here's an example of passing a parameter by "const" reference. It uses the "intstack" from the previous lesson:
int main(void)
{
intstack i;
i.push(5);
workWithStack(i);
return 0;
}
Since the "intstack" class does not have a copy constructor (it was declared private, remember?) it is impossible to
pass instances of it to functions in other ways than by reference (or by pointer.)
There are situations when a reference is dangerous, though. One such trap, that I think all C++ programmers fall into
at least once, is returning a reference to a local variable. Have a look at this:
#include <iostream.h>
int main(void)
{
cout << ir() << endl;
return 0;
}
What will this program print? It's hard to tell. It could be anything, if it prints at all. It might just crash. Why? The
function "ir" returns a reference to an "int", that the "main" function prints. So far so good, but what does the
reference returned refer to? It refers to the local variable "i" in "ir". If you remember the "tracer" examples from the
previous lesson, you remember that the variable ceases to exist when exiting the function. In other words, the
reference returned refers to a variable that no longer exists! Don't do this! Or rather, do it now, at once, just to have
your one time mistake over with :-)
What's a class?
Now for the theoretical biggie. What, exactly, is the meaning of a class. When should you write a class, what should
the class allow you to do, and what's a good name for a class? What's the relation between classes and objects?
When you write programs in Object Oriented Programming Languages, be it C++, Objective-C, SmallTalk, Eiffel,
Modula-3, Java or whatever, you write classes. A class is, as I mentioned in part 2, a method of encapsulation, but
more importantly, a class is a type. When you define a class, you add a new type to the language. C++ comes with a
set of built in types like "int", "unsigned" and "double". In the previous lesson, when we wrote the class "intstack",
we introduced a new type to the language, which programs could use, the stack of integers. The member functions
of the class, describe the semantics of the type. With the built in integral types, we have operations like adding two
instances of the type, yielding a third instance, which value happens to be the sum of the values of the other two. We
can increment the value of instances of the type with operations like ++, and so on. With the "intstack", we had the
operations "pop", "top", "push" and "nrOfElements", in addition to well defined construction and destruction of
instances.
So, how can you know what classes to make? Classes are, as a rule of thumb, descriptions of ideas. "Bicycle" for
example, is a classic example of a class. The idea "Bicycle" that is, not my particular bicycle. My bicycle is a
physical entity that is currently getting wet in the rain. The idea of bicycle is a very good candidate for a class. What
my bicycle is, on the other hand, is a good candidate for an instance of the class "Bicycle." So, when thinking of the
problem you want to solve, you might have a good candidate for a class X, if you can say "The X ...", "An X...", or
"A kind of X...". The objects are the instances of types (yes, an instance of type "float" is also an object, they need
not be instances of classes.) A class represent the idea, and the functions that represent the semantics. Usually
instances of the class has a state (for example, the state of a stack is the elements in it, and their order.) Having state
means that the same member function can give different results depending on what has been done to the object
before calling the member function (again, with a stack, the value returned by "top()" or "nrOrElements()" depends
on the history of "push()" and "pop()" calls.) The class has member data to represent state. There are, however,
exceptions to this rule of thumb. For example, is a mathematical function a class that you'd like to have instances of
to toy with in your program? According to the rule, it is not, since a mathematical function is state less. In most
situations, the answer would, as expected, be no, but if you design a program for use by electronics engineers when
designing their gadgets, you better have amplifiers (multiplication,) adders, subtractors and so on, or they won't use
your program.
Note that objects don't exist when you write your program. Objects are run-time entities. When you write your
program, what exists are types, descriptions of how instances of types can be used, and descriptions of semantics
and state representation. When your program executes, the identifiers, (like "pc" in the reference example above) are
replaced by bit-patterns representing objects.
So, then, what member functions should a class have? This is even harder to say, because there are so many ways to
solve every problem. However, the things that you need to do, when solving your problem, to instances of types,
like "Bicycle" or "intstack", must in one way or the other be expressible through the classes. If I need to ride my
bicycle, it can be that the class "Bicycle", should have the member function "beRiddenBy" accepting an instance of
class "Human", but it might also be that class "Human" should have the member function "ride" accepting an
instance of class "Bicycle" as its parameter. If the starting point or destination are important, they probably should
be parameters to the member functions. If the road itself is important, you probably need a class "Road", which you
want to pass an instance of to the member function of either "Bicycle::beRiddenBy" or "Human::ride".
Given this, you might start to feel like someone's been fooling you. This Object Oriented Programming thing is a
hoax! What it's all about, is class oriented programming. The objects are, after all, just the run time instances of the
classes.
class C
{
public:
C(const C& c); // copy constructor
// other necessary member functions.
};
The job of the copy constructor is to create an object that is identical to another object. It is your job to make sure it
does this. This does not mean that every member variable of the newly constructed object must have values identical
to the ones in the original. On the contrary, they often differ. What's important, though, is that they're semantically
identical (i.e. given the same input to member functions, they give the same response.) The "intstack" for example
must make its own copy of the stack representation in the copy constructor. This means that the base pointer will
differ, but as far as you can see through the "push", "pop" and "top" member functions, there is no difference
between the copy and the original.
Next in line is copy assignment. Again, given a class C, the copy assignment operator looks like this:
class C
{
public:
C& operator=(const C&);
// other necessary member functions.
};
Writing the copy assignment operator is more difficult than writing the copy constructor. Not only does the copy
assignment operator need to make the object equal to its parameter, it also needs to cleanly get rid of whatever
resources it might have held when being called (The copy constructor does not have this problem since it creates a
new object that cannot have held any data since before.) The return value of an assignment operator is (by tradition,
not by necessity) a reference to the object just assigned to. When inside a member function (the assignment operator
as defined above is a member function) the object can be reached through a pointer named "this," which is a pointer
to the class type. For the class C, above, the type of "this" is "C* const" This means that is's a pointer to type C, and
the pointer itself is a constant (i.e. you cannot make "this" point to anything else than the current instance.) The
reference to the object is obtained by dereferencing the "this" pointer, so the last statement of an assignment operator
is almost always "return *this;"
The difficulty of writing a good copy constructor and copy assignment operator is best shown through a classical
error:
class bad
{
public:
bad(void); // default constructor
bad(const bad&); // copy constructor
~bad(void); // destructor
bad& operator=(const bad&); // copy assignment
private:
int* pi;
};
bad::bad(void)
: pi(new int(5)) // allocate new int on heap and
// give it the value 5.
{
}
bad::bad(const bad& b)
: pi(b.pi) // initialize pi with the value of b's pi
{ // This is very bad, as you will soon see
}
bad::~bad(void)
{
delete pi;
}
int main(void)
{
bad b1;
{
bad b2(b1); // b2.pi is now the same as b1.pi.
} // Here b2 is destroyed, and b2's destructor is
// called. This means that the memory area
// pointed to by b2.pi (and hence also b1.pi) is
// no longer valid
bad::bad(const bad& b)
: pi(new int(*b.pi)) // initialize pi as a new int
// with the value of b's pi
{ // This guarantees that both the new object and the
} // original are destructible.
int main(void)
{
bad b1;
{
bad b2(b1); // b2.pi now points to its own area.
} // Here b2 is destroyed, and b2's destructor is
// called. This means that the memory area
// pointed to by b2.pi is no longer valid
// b1.pi is still valid, though.
Const Correctness
When talking about passing parameters to functions by reference, I mentioned the const reference as a way to ensure
that the parameter won't get modified, since the const reference treats whatever it refers to as a constant and thus
won't allow you to do things that would modify it. The question is, how does the compiler know if something you do
to an object will modify it? Does "pop" modify the "intstack?" Yes, it does. It removes the top element. Does "top"
modify the stack? No. So, it should be allowed to call "top" for a constant stack, right? The problem is that the
compiler doesn't know which member functions modify the objects, and which don't (and assumes they do, just to
be on the safe side) unless you tell it differently. Since, by default, a member function is assumed to alter the object,
you are, by default, not allowed to do anything at all to a constant object. This is of course hard. Fortunately we can
tell the compiler differently. We can change "top" to be declared as follows:
class intstack {
public:
// misc member functions
int top(void) const throw(stack_underflow,pc_error);
// misc other member functions
};
It's the word "const" after the parameter list that tells the compiler that this member function will not modify the
object and can safely be called for constant objects. As a matter of fact, now when we know about references, we
can do even better by writing two member functions "top", one "const" and one not, with the non-const version
returning a non-const reference to the element instead. This has a tremendous advantage: For constant stack objects,
we can get the value of the top element, for non-constant stack objects, we can alter the value of the top element by
writing like this:
intstack is;
is.push(5); // top is now 5;
is.top() = 3; // change value of top element!
There is no magic involved in this. Just as I mentioned in part one, functions can be overloaded if their parameter list
differs. Member functions can be overloaded on "constness." The "const" member function is called for constant
objects (or, const references or pointers, since they both treat the object referred to as if it was a constant.) The non-
const member function is called for non-constant objects. Note that it is only member functions you can do this
"const" overloading on. You cannot declare non-member functions "const." Our overloaded "top" member functions
can be declared like this:
class intstack {
public:
// misc member functions
int top(void) const throw(stack_underflow,pc_error);
int& top(void) throw (stach_underflow,pc_error);
// misc other member functions
};
This is getting too much without concrete examples. Here's a version of "intstack" with copy constructor, copy
assignment operator, const version of "top" and "nrOfElements", and a non-const version of "top" (just as above.)
Only the new and changed functions are included here. You'll find a zip file with the complete sources at the top.
class intstack
{
public:
// the previous memberfunctions
private:
// helper functions for copy constructor, copy
// assignment and destructor.
unsigned
intstack::nrOfElements() const throw (pc_error)
{
// Preconditions: -
return elements;
// Postconditions:
// nrOfElements() == 0 || top() == old top()
// no need to check anything with this
// implementation as it's trivially
// obvious that nothing will change.
}
There isn't anything at all that differs from the previous version of "nrOfElements", other than that it's declared to be
"const." Had we, in this member function (or any other member function declared as "const" attempted to modify
any member variable, the compiler would give an error, saying that we're attempting to break our promise not to
modify the object. "const" methods are thus good also as a way of preventing you from making mistakes. Note that
declaring a member function "const" does not mean it's only for constant objects, it just means that it's callable on
constant objects too. Whenever you have a member function that does not modify any member variable, declare it
"const" so that errors can be caught by the compiler. It saves you debug time, in addition to making those member
functions callable for constant objects (or constant references or pointers.)
Next in turn is "top", or rather the two versions of "top":
int& intstack::top(void)
throw (stack_underflow, pc_error)
{
// Preconditions:
// nrOfElements() > 0
if (nrOfElements() == 0 || pTop == 0)
{
throw stack_underflow();
}
return pTop->value;
// Postconditions:
// nrOfElements() == old nrOfElements()
// No need to check with this implementation!
}
As can be seen, not much differs between the two variants of "top." The implementation is in fact identical for both,
but the first one returns a value and is declared const, the other one is not declared const and returns a reference. So
why do we have two identical implementations here, when I earlier mentioned that this is always undesirable? The
reason is simply that although the implementation is identical, neither can be expressed in terms of the other. The
non-const version cannot be implemented with the aid of the const version, since we'd then return a reference to a
local value. This is always bad, does not have the desired effect, and quite likely to cause unpredictable run-time
behaviour. The "const" version could be implemented in terms of the non-const version, if it wasn't for the fact that
it is not declared "const." The implementation of a const member function is not allowed to alter the object, and is,
as a consequence of this, not allowed to call non-const member functions for the same object.
Remember that a reference really isn't an object on its own? You cannot distinguish it in any way from the object it
refers to. In this case it means that what's returned from the non-const version of "top" is the top element itself, not a
local copy of it. Since it is the element itself, it can be modified. Note that there is a danger in this too: What about
this example?
intstack is;
is.push(45);
int& i=is.top(); // i now refers to the top element
i=32; // modify top element.
int val=is.top(); // val is 32.
is.pop(); // what does i refer to now?
i=45; // what happens here?
The answer to the last two questions is that "i" refers to a variable that no longer exists and that when assigning to it,
or getting a value from it, anything can happen. If you're lucky, your program will crash right away, if you're out of
luck, it'll start behaving randomly erratically!
Now for the copy constructor. With the help of the "copy" member function, it's really simple!
intstack::stack_element*
intstack::copy(void) const throw (bad_alloc)
{
stack_element* pFirst = 0; // used in catch block.
try {
stack_element* p = pTop;
if (p != 0)
{ // take care of first element here.
stack_element* pPrevious = 0;
if (pFirst == 0) //**1
throw bad_alloc();
pPrevious = pFirst;
pPrevious = pPrevious->pNext;
if (pPrevious == 0) //**1
throw bad_alloc();
}
}
return pFirst;
}
catch (...) // If anything went wrong, deallocate all
{ // and rethrow!
while (pFirst != 0)
{
stack_element* pTmp = pFirst->pNext;
delete pFirst; // guaranteed not to throw.
pFirst = pTmp;
}
throw;
}
}
To begin with, the return type is "intstack::stack_element*". The type "stack_element" is only known within
"intstack," so whenever used outside of "intstack" it must be explicitly stated that it is the "stack_element" type that
is defined in "intstack." As long as we're "in the header" of a member function, nested types must be explicitly
stated. Well within the function, it is no longer needed, since it is then known what class the type belongs to.
The whole copying is in a "try" block, so we can deallocate things if something goes wrong. The local variable
"pFirst", used to point to the first element of the copy, is defined outside of the "try" block, so it can be used inside
the "catch" block. If we didn't leave this for the "catch" block, there would be no way it could find the memory to
deallocate.
If "pTop" is non-zero, the whole structure that "pTop" refers to is copied.
There are two details worth mentioning here.
• The "if" statements marked //**1 are only needed for older compilers. New compilers automatically throw
"bad_alloc" when they're out of memory. Old compilers, however, return 0.
• The "while" statement marked //**2 might look odd. What happens is that the variable "p" is given the
value of "p->pNext", and that value is compared against zero. Remember that assignment is an expression,
and that expression can be used, for example, for comparisons. The assignment "p=p->pNext" must be in a
parenthesis for this to work. The precedence rules are such that assignment has lower precedence than
comparison, so if we left out the parenthesis, the effect would be to assign "p" the value of "p->pNext"
compared to 0, which would not be what we intended.
At the places where a "stack_element" is allocated, it is important that the "pNext" member variable is given the
value 0, since it is always put at the end of the stack. If it was not set to 0, it would not be possible to know that it
was the last element, and our program would behave erratically. It's not until we have successfully created another
element to append to the stack, that the "pNext" member variable is given a value other than 0.
Now, it's up to you to toy with the "intstack". Whenever you have a need for a stack of integers, here you have one.
Exercises
• When is guarding against self assignment necessary? When is it desirable?
• How can you disallow assignment for instances of a class?
• The non-const version of "top" returns a reference to data internal to the class. Mail me your reasons for
why this can be a bad idea (it can, and usually even is!) Can it be bad in this case?
• When can returning references be dangerous? When is it not?
• Mail me an exhaustive list of reasons when assignment or construction can be allowed to fail under the
Orthodox Canonical Form.
• When is it OK to use the auto-generated copy constructor and copy assignment operator?
Recap
This month, yet more news has been introduced to you, as coming C++ programmers.
• You have seen how C++ references work.
• You have learned about "const", and how it works for objects.
• You have seen how you can make member functions callable for "const" objects by declaring them as
"const", and seen that member functions declared "const" are callable for non-const objects as well.
• You have found out how you can overload member functions on "constness" to get different behaviour for
const objects and non-const objects.
• You have learned about the "Orthodox Canonical Form", which always gives you construction from
nothing, construction by copying, assignment and destruction.
• You have learned that your objects should always be in a destructible and copyable state, no matter what
happens.
• You have seen how you can implement common behaviour in private member functions. These member
functions are then only callable from within member functions of that class.
Coming up
Next month I hope to introduce you to components of the C++ standard library. Most compilers available today do
not have this library, but fortunately it is available and downloadable for free from a number of sources. Knowing
this library, and how to use it, will be very beneficial for you, partly because it is standard, and partly because it's
remarkably powerful.
As usual, you are the ones who can make this course the ultimate C++ course for you. Send me e-mail at once,
stating your opinions, desires, questions and (of course) answers to this month's exercises!
Capitulo 4
NOTE: Here is a link to a zip of the source code for this article. Ed.]
So I've done it again, promised something I cannot keep. I wanted to introduce you to the C++ standard library, but
there are problems with the available implementations and Watcom compilers, and I don't own a Watcom compiler
to work around it with... Later. This month, however, we'll look at another very important aspect of C++, that of
templates. A template is a way of loosening your dependency on a type, without sacrificing type safety (this will get
clear later). Templates are the foundation of the standard C++ library too, so getting to know them before hand
might not be so bad anyway.
Why templates
The last two articles made some effort in perfecting a stack of integers. Today, I want a stack of doubles. What do I
do? Rewrite it all and call it doublestack? It's one alternative. Then I want a stack of char* (another rewrite) and a
stack of bicycles (yet a rewrite, and a bizarre view). And then, of course, we end up with 4 versions of stack, all with
identical code, just one data member in the internal struct with different type for all of them. Sigh...
There's of course the C kind of solution, always make it a stack of void*, and cast to whatever you want (and just
hope you won't cast to the wrong type). No, the latter alternative isn't an alternative in my book. Type safety is
essential for writing solid programs (Smalltalk programmers disagree). The former alternative isn't an alternative
either. Just think of this little nightmare, 4, at least, more or less identical pieces of code. When you find a bug
(when, not if), you have to correct it in as many places. Yuck... OK, so I guess I've explained what templates are for.
They're the solution to the above problem. But how?
Function templates
In the first C++ article, I wrote a set of overloaded functions called "print", which printed parameters of different
types. The code in each of those functions was exactly the same (exactly the kind of redundancy that's always to be
avoided). This is an ideal place for a template. Here's what a template function for printing something can look like:
int main(void)
{
print(5); // print<int>
print(3.141592); // print<double>
print("cool"); // print<const char*>
print(2); // print<int> again.
return 0;
}
Weird? OK, time for some demystifying. The code for the template, is not a function. It's a function template,
something which the compiler uses to create functions. This is very much like a cookie cutter. Once you have a
cookie cutter, you can make cookies with the shape of the cutter. More or less any kind of cookie can be made with
that cutter. When the compiler reads the function template, it does pretty much nothing at all, other than to
remember that there's a function template with one template parameter and the name "print". When the compiler
reaches "main", and sees the call to "print(5)", it looks for a function "print" taking an "int" as a parameter. There is
none, so it expands the template function "print" with the type "int", and actually makes a new function. Note that
this is done by the compiler at compile time. The same happens for the other types. The compiler always first checks
if there is a function available, and if there isn't, it tries to create it by expanding the function template. This order of
things is necessary to avoid unnecessary duplication. After all, "print(2)" uses the same function as "print(5)" does,
rather than creating yet another copy of it.
Let's compile and run:
[c:\desktop]icc /Q temp_test.cpp
[c:\desktop]temp_test.exe
t=5
t=3.14159
t=cool
t=2
[c:\desktop]
Although it does not seem like it, type safety is by no means compromised here. It's just seen in a somewhat
different way. For the function template "print", there's only one requirement on the type T; it must be possible to
print it with the "<<" operator to "cout". If the type cannot be printed, a compilation error occurs. To test it, here's
what GCC says when trying to print the "intstack" from last month:
Class templates
Just as you can write functions that are independent of type (and yet type safe!) we can write classes that are
independent of type. In a sense, class templates exist as builtins in C and C++. You have arrays and pointers (and
references) that all act on a type, some type. The type they act on does not change their behaviour, they're still
arrays, pointers and references, but of different types. Let's explore writing a simple class template, by improving
the old "Range" class from lesson 2. In case you don't remember, the original "Range" looks like this:
#include <iostream.h>
int main(void)
{
Range<int> ri(100,10);
Range<double> rd(3.141592,-3.141592);
if (ri.includes(55))
{
cout << "[" << ri.lowerBound() << ", "
<< ri.upperBound() << "] includes 55"
<< endl;
}
if (!rd.includes(62))
{
cout << "[" << rd.lowerBound() << ", "
<< rd.upperBound()
<< "] does not include 62" << endl;
}
return 0;
}
Take a careful look at the syntax here. To use a class template, you must explicitly state what type it is for. There is
unfortunately no way to say "Range(5,10)" and have the compiler automatically understand that you mean
"Range<int>(5,10)". As with function templates, a class template is expanded when it's referred to, so when the
compiler first sees "Range<int>", it creates the class, by expanding whatever is needed. The compiler will also treat
every member function just as any template function, i.e. the code will not be expanded until it is called from
somewhere. The above code calls all members of "Range", but had "includes" not been called, it would not have
been expanded. One unfortunate side of this is that "includes" could actually contain errors, and this would be
unnoticed by the compiler, until "includes" was called.
Advanced Templates
Now that the basics are covered, we should have a look at some power usage (this section is abstract, so it may
require a number of readings).
One unusually clever place for templates, is as something called "traits classes". A traits class is never instantiated,
and doesn't contain any data. It just tells things about other classes, that is its sole purpose. The name "traits class" is
odd. Originally they were called "baggage classes", since they're useless on their own, and belong to something else,
but for some reason some people didn't like the name, so it was changed. My intention is to write a traits class,
which tells the name of the type it is specialized for (explanation of that comes below), and to write a special
template print, which prints ranges looking like the constructor call for the range, and finally, a function template,
which is used to create ranges, without needing to specify the type. When done, I will be able to write:
print(make_range(10,4));
and when executed, see:
Range<int>(10,4)
Magic? No, just templates!
Here we go...
A traits class, is a simple class template. The traits class needed here, is one that tells the name of a type. The class
template just looks like this:
class A
{
private:
int data;
public:
void f(void);
static void g(void);
static void h(void);
};
void A::f(void)
{
cout << data << endl;
}
void A::g(void)
{
cout << data << endl; // error! Cannot access data.
}
void A::h(void)
{
cout << "A::h" << endl;
}
int main(void)
{
A a;
a.f(); // prints something.
a.h(); // prints "A::h"
A::h(); // also prints "A::h"
A::f(); // Error, f is bound to an object, and must be
// called on an object.
return 0;
}
"A::g()" is in error, because it's declared static, and thus not bound to any object, and as such cannot access any
member data, since member data belongs to objects.
The calls "a.h()" and "A::h()" are synonymous. Since "h" is not tied to an object, it can be called through the class
scope operator "A::", which means it's the "h" belonging to the class named "A".
Calling "A::f()" is an error, since it is not static. This means it belongs to an object, and must be called on an object
(through the "." operator).
Now back to traits classes. The whole idea for traits classes is one of "specialization". The class template is the
general way of doing things, but if you want the class to take some special care for a certain type, you can do what's
called a specialization. A member function specialization is usually not declared, just defined, like this:
const char* type_name<char>::as_string()
{
return "char";
}
Of course, if you have a top modern compiler, you'll get a compilation error. The syntax has changed, so compilers
very much up to date with the standardization requires you to write like this:
template <>
const char* type_name<char>::as_string()
{
return "char";
}
A minor, but in a sense, understandable difference. The "template <>" part clarifies that it's a template we're dealing
with, but the template parameter list is empty, since we're specializing for known types.
This is how traits classes usually look. They have a template interface, the class, which declares a number of static
member functions. Those member functions are intended to tell something about some other class. Normally, the
template member functions are not defined, instead specializations are. Their purpose is only to tell something about
other classes, nothing else.
Now, we can use the "type_name" traits class for "char" as follows:
print(Range<int>(10,5));
And it will work (if we specialize "type_name<int>::as_string()", that is).
Now for the last detail; the function template that creates "Range" instances.
print(make_range(10,5));
just as we planned to. Neat, eh? If you want to learn more about traits classes, have a look at Nathan Meyers traits
article from the June '95 issue of C++ Report.
Exercises
• Biggie: Rewrite last months "intstack" as a class template, "stack<T>" What happens if the copy
constructor, operator== or destructor of T throws exceptions?
• When can you, and when can you not use exception specifiers for templates?
• What are the requirements on the type parameter of the templatized "Range"? Can you use a range of
"intstack"?
• What are the requirements on the type parameter of the templatized "stack<T>"?
Recap
Quite a lot of news this month. You've learned:
• how to write type independent functions with templates, without sacrificing type safety.
• how the compiler generates the template functions from your function template.
• about template classes, which can contain data of a type not known at the time of writing.
• that templates restricts the usefulness of exception specifiers.
• how to specialize class templates for known types.
• how to write and use traits classes.
Coming up
If the standard library's up and running on Watcom, that'll be next month's topic, otherwise it'll be C++ I/O streams.
As always, send me e-mail at once, stating your opinions, desires, questions and (of course) answers to this month's
exercises!
Part Part Part Part Part Part Part Part Part Part1 Part1 Part1 Part1
1 2 3 4 5 6 7 8 9 0 1 2 3
Introduction
We've seen how the fundamental types of C++ can be written to the screen with "cout << value" and read from
standard input with "cin >> variable". This month, you will learn how you can do the same for your own classes and
structs. It's surprisingly easy to do.
class X
{
public:
...
X& operator=(int i);
...
};
X x;
x=5; //**
At the last line of the example, what actually happens is that operator= is called for the object named "x". Another
way of expressing this is:
x.operator=(5);
In fact, this syntax is legal, and it generates identical code, because this is how the compiler will treat the more
human-readable form "x=5".
As we can see then, an operator overridden in a class, is just like any other member function, it's just called in a
peculiar form.
Let's go back to printing again. "cout" is an object of some class, which has operator<<(T) overloaded, where T is
any of the fundamental types of C++. The relevant section of the class definition looks as follows:
class ostream
{
...
public:
...
ostream& operator<<(char);
ostream& operator<<(signed char);
ostream& operator<<(unsigned char);
ostream& operator<<(short);
ostream& operator<<(unsigned short);
ostream& operator<<(int);
ostream& operator<<(unsigned int);
ostream& operator<<(long);
ostream& operator<<(unsigned long);
ostream& operator<<(float);
ostream& operator<<(double);
ostream& operator<<(long double);
ostream& operator<<(const char*);
...
};
The value returned by each of these is the stream object itself (i.e. if you call "operator<<(char)" on "cout", the
return value will be a reference to "cout" itself.)
With the above in mind, we can see that writing
int i;
double d;
int i;
double d;
(cout.operator<<(i)).operator<<(d);
The only difference for reading is that the class is called "istream" instead, and that the operator used is
operator>>().
Range r;
int i;
...
cout << r << i;
The compiler will treat it as
operator<<(cout, r).operator<<(i);
This even works for more complex expressions, like:
Range r;
int i;
int j;
...
cout << i << r << j;
Which the compiler interprets as:
operator<<(cout.operator<<(i),r).operator<<(j);
Study these examples carefully, to make sure you understand what's going on.
Now, after these examples, it's fairly easy to get down to work with implementing the operator<< function.
Formatting
There are a number of ways in which the output format of the fundamental types of C++ can be altered, and a few
ways in which the requirements on the input format can be altered. For example, a field width can be set, and
alignment within that field. For integral types, the base can be set (decimal, octal, hexadecimal). For floating point
types the format can be fixed point or scientific. All of these, and yet some, are controlled with a few formatting
flags, and a little data. All flags are set or cleared with the member functions "os.setf()" and "os.unsetf()". I think
they're difficult to use, but fortunately there are easier ways of achieving the same effect, and we'll visit those later.
The base for integral output is altered with a call to "os.setf(v, ios::basefield)", where v is one of "ios::hex",
"ios::dec" or "ios::oct". As a small example, consider:
#include <iostream.h>
int main(void)
{
int i=19;
cout << i << endl;
cout.setf(ios::hex, ios::basefield);
cout << i << endl;
cout.setf(ios::oct, ios::basefield);
cout << i << endl;
cout.setf(ios::dec, ios::basefield);
cout << i << endl;
return 0;
}
The result of running this program is:
19
13
23
19
The base is converted as expected, but there is no way to see what base it is. This can be improved with the
formatting flag ios::showbase, so let's set that one too.
int main(void)
{
int i=19;
cout.setf(ios::showbase);
cout << i << endl;
cout.setf(ios::hex, ios::basefield);
cout << i << endl;
cout.setf(ios::oct, ios::basefield);
cout << i << endl;
cout.setf(ios::dec, ios::basefield);
cout << i << endl;
return 0;
}
The output of this program is
19
0x13
023
19
That's more like it, right? The call to "setf()" for setting the "ios::showbase" flag is different, though. "setf()" is
overloaded in two forms. One accepts a set of flags and a mask, the other one a full set of flags only. All the
formatting flags of the iostreams are represented as bits in an integer, and the version with the mask clears the bits
represented by the mask, except those explicitly set by the first parameters. Formatting bits not represented by the
mask will remain unchanged. The second form, the one accepting only one parameter, sets the flags sent as
parameter, and leaves the others unchanged (in other words, it bitwise "or"es the current bit-pattern with the one
provided as the parameter.) Now you begin to see why this is messy. If the masked version is called, and the mask is
"ios::basefield", the only formatting flags of the stream that will be affected are "ios::hex" or "ios::dec" or "ios::oct".
The three of these are mutually exclusive, so a call to "os.setf(ios::hex)", is potentially dangerous (what if "ios::oct"
was already set? Then you'd end up with both being set.) The second parameter "ios::basefield" guarantees that if
you set "ios::hex", then "ios::oct" and "ios::dec" will be cleared. While it's possible to set two, or all three of these
flags at the same time, it's not a very good idea (yields undefined behaviour.)
That was setting the base for integral types, now for something that's common to all types, field width and
alignment. The field width is set with "os.width(int)", and the curious can get the current field width by calling
"os.width(void)." Simple enough, let's try it out:
#include <iostream.h>
int main()
{
cout << '[' << -55 << ']' << endl;
cout.width(10);
cout << '[' << -55 << ']' << endl;
cout << '[';
cout.width(10);
cout << -55 << ']' << endl;
cout << '[' << -55 << ']' << endl;
return 0;
}
Executing this programs shows something interesting; the width set does not affect the printing separate characters,
and the width is reset after printing the first thing that uses it. This is not very intuitive I think. The result of running
the program is shown below:
[-55]
[ -55]
[ -55]
[-55]
Had you expected this? I didn't, for sure.
Now, let's play with alignment within a field. If the field width is not set, or the field width set is smaller than that
necessary to represent the value to be printed, alignment doesn't matter, but if there's extra room, alignment does
make a difference. Alignment is set with the two parameter version of "os.setf()", where the first parameter is one os
"ios::left", "ios::right", or "ios::internal", and the second parameter is "ios::adjustfield". As with the base for integral
types, the three alignment forms are mutually exclusive, so don't set two of them at the same time. Let's alter the
width setting program to show the behaviour.
#include <iostream.h>
int main()
{
cout.setf(ios::right, ios::adjustfield);
cout << '[' << -55 << ']' << endl;
cout.setf(ios::left, ios::adjustfield);
cout << '[' << -55 << ']' << endl;
cout.setf(ios::internal, ios::adjustfield);
cout << '[' << -55 << ']' << endl;
cout.width(10);
cout.setf(ios::right, ios::adjustfield);
cout << '[' << -55 << ']' << endl;
cout.width(10);
cout.setf(ios::left, ios::adjustfield);
cout << '[' << -55 << ']' << endl;
cout.width(10);
cout.setf(ios::internal, ios::adjustfield);
cout << '[' << -55 << ']' << endl;
return 0;
}
The result of running this is, after the above explanations, not very surprising:
[-55]
[-55]
[-55]
[ -55]
[-55 ]
[- 55]
Well, OK, I found the formatting of "ios::internal" to be a bit odd, but it kind of makes sense.
If the field width is larger than that required for a value, the current alignment defines where in the field the value
will be, and where in the field space will be. Space, but the way, is just the default, we can change the "padding
character", by calling "os.fill(char)", and get the current value with a call to "os.fill(void)". Let's exercise that one
too:
#include <iostream.h>
int main()
{
cout.width(10);
cout.fill('.');
cout << -5 << endl;
cout.width(10);
cout << -5 << endl;
return 0;
}
Running it yields the surprising result
........-5
........-5
Why was this surprising? Earlier we saw that the field width is "forgotten" once used. The pad character, however,
remains the same until explicitly changed.
Now that you have the general idea, why not try the other formatting flags there are:
• ios::fixed and ios::scientific control the format of floating point numbers (the mask used is ios::floatfield.)
ios::showpos controls whether a "+" should be prepended to positive numbers or not (just like a "-" is
prepended to negative numbers.
• ios::uppercase controls whether hexadecimal digits should be displayed with upper case letters or lower
case letters.
• ios::showpoint controls whether the decimals should be shown for floating point numbers if they are all
zero.
The only thing remaining for formatting is "os.precision", which comes in two flavours. One without parameters
which reports the current precision, and one with an int parameter. The unpleasant thing about this parameter, is that
many compilers interpret it differently. Some think the precision is the number of digits after the decimal point,
while most think it's the number of digits to display. The November 1997 draft C++ standards document (which, by
the way, most probably is the final C++ standards document,) says the number of digits after the decimal point is
what's controlled, but I'm not sure if that's what the current standards document says.
At any rate, inconsistencies aside, this is a mess, isn't it?
An easier way
The authors of the I/O package realized that this is a mess, so they defined something called "manipulators." You've
already used one manipulator a lot, "endl." A manipulator does may, or may not, print something on the stream, but
it will alter the stream in some way. For example "endl" prints a new line character, and flushes the stream buffer.
There are two kinds of manipulators, those accepting a parameter, and those that does not. Let's first focus on those
that don't, just like "endl." The ones available are: "dec", "hex", "oct", "endl", "ends", and "flush". Their use is
simple:
#include <iostream.h>
int main(void)
{
cout << hex << 127 << " " << oct << 127 << " "
<< oct << 127 << endl;
return 0;
}
The advantage of this is both that the code becomes clearer, and that there's no way you can accidentally set illegal
base flag combinations. "ends" is rarely used, it's there to print a terminating '\0' (the terminating '\0' of strings is
never printed normally.) "flush" flushes the stream buffer (i.e. forces printing right away.)
How do these manipulators work? There's a rather odd looking operator<< for output streams. It looks like:
class spaces
{
public:
spaces(int s) : nr(s) {};
ostream& printOn(ostream& os) const {
for (int i=0; i < nr; ++i)
cout << ' ';
return os;
}
private:
int nr;
};
ostream& operator<<(ostream& os, const spaces& s)
{
return s.printOn(os);
}
Can you see what happens if we call "cout << spaces(40)"? First the object of class "spaces" is created, with a
parameter of 40. That parameter is in the constructor stored in the member variable "nr". Then the global operator<<
for an ostream& and a const space& is called, and that function in its turn calls the printOn member function for the
spaces object, which goes through the loop printing space characters.
I think writing manipulators requiring parameters this way is lots easier than trying to understand the non-portable
way provided by your compiler vendor.
Now something for you to think about until next month, what about our I/O of our own classes with respect to the
formatting state of the stream? How's the "Range" class printed if the field width and alignment is set to something?
How should it be printed (hint, your probably want it printed differently from what will be the case if you don't take
care of it.)
Exercises
• Find out which formatting parameters "stick" (like the choice of padding character) and which ones are
dropped immediately after first use (like the field width.)
• With the above in mind, and remembering that destructors can be put to good work, write a class which will
accept an ostream as its constructor parameter, and which on destruction will restore the ostreams
formatting state to what it was on construction.
• Experiment with the formatting flags on input, which have effect, and which don't? Of those that do have
an effect, do they have the effect you expect?
• Write an input manipulator accepting a character, which when called compares it with a character read from
the stream, and sets the ios::fail status bit if they differ.
Recap
This month you've learned a number of things regarding the fundamentals of C++ I/O. For example
• How to set, clear and recognise the error state of a stream.
• Why exceptions are not to be used when input is wrong.
• How to make sure your own classes can be written and read.
• The very messy, and the somewhat less messy way of altering the formatting state of a stream.
• How to write your own stream manipulators.
Standards update
• The prefix and postfix functions are history. Instead you create an object of type istream::sentry or
ostream::sentry, and check it, like this:
The destructor of the sentry object does the work corresponding to that of the postfix function.
• "istream" and "ostream" are in fact not classes in the standard, but typedef's for class templates. The class
templates are template <class charT, class traits> class basic_istream<charT, traits>, and template<class
charT, class traits> class basic_ostream<charT, traits>. "istream" is typedefed as "basic_istream<char,
char_traits<char> >", and "ostream" as "basic_ostream<char, char_traits<char> >". There's also the pair
"wistream" and "wostream", that are streams of wide characters.
• The mechanism for writing manipulators is standardised (and heavily based on templates.) I still think it's
easier to write a class the way I showed you.
• Any operation that sets an error status bit may throw an exception. Which error status bits cause exceptions
to be thrown is controlled with an exception mask (a bit mask.) By default, though, no exceptions are
thrown.
• Formatting of numeric types (and time) is localised. By default most implementations will probably use the
same formatting as they do today, but with the support for "imbuing" streams with other locales (formatting
rules.)
• The header name is <iostream> (no .h) and the names actually std::istream and std::ostream (everything in
the C++ standard library is named std::whatever, and every standard header is named without trailing .h)
Coming up
Next month we'll have a look at inheritance. Inheritance is what Object Orientation is all about. People who
enjoy and understand the philosophy of Platon will feel at home. Inheritance is a way of expressing
commonality.
As always, send me e-mail at once, stating your opinions, desires, questions and (of course) answers to this
month's exercises!
Part1 Part2 Part3 Part4 Part5 Part6 Part7 Part8 Part9 Part10 Part11 Part12 Part13
Int
Part Part Part Part Part Part Part Part Part Part1 Part1 Part1 Part1
1 2 3 4 5 6 7 8 9 0 1 2 3
Introduction
I admit it. I've had very little inspiration for writing this month. Maybe it's the winter darkness, or the to be switch of
jobs. However, instead of leaving you in the dark for a month, here's some food for thought, even though it's short
and not the planned details on inheritance.
Example:
Study this function template:
+----+-----+-----+-----+----+
primes_lt_10 = | 2 | 3 | 5 | 7 |XXXX|
+----+-----+-----+-----+----+
^ ^
| |
begin end (points to non-existing
element one past the last
one.)
Fortunate as it is, this is legal in C and C++. It illegal to dereference the "one-past-the-end" pointer, but the value is
legal, and decrementing it will make it point to the last element (as opposed to making it point n, where n>1,
elements past the end, which yields "undefined behaviour", i.e. all bets are off.)
This is useful, we can now copy arrays (or parts of arrays) of any type to arrays (or parts there of) of any type, if the
source type can be implicitly converted to the destination type, by using the copy function template. Very useful.
However, the fun has only begun. The real joy begins when we realize we can write our own types that behaves the
same way.
Other uses
Let's assume we want to print the contents of an array. How can we do this? Of course we can, as usual, use a loop
over all elements and print them, but I can assure you, we can use the copy function template to do it. How?
Printing an array (actually, the values in an array) means copying the values from the array to the screen, through
some conversion (the output formatting.) The problem thus becomes, how do we make a type that does the
necessary conversion, prints on the screen, and conforms to the requirements stated for the template parameter
"OUT?"
The secrets seems to lie in the lines "*dest = *begin" and "++dest". As I see it, there are two alternatives.
Either "*dest = *begin" prints the value of "*begin" on the screen, and "++dest" does nothing at all, or "*dest =
*begin" makes our variable "dest" remember the value to print and on "++dest" it does the printing. To show you
how this can be done, I will do the former. You can do the latter as an exercise.
Of course, it is now necessary to see how some more operators can be overloaded; operator* and operator++. Of
these, operator* is very straight- forward, while operator++ requires some thought since there are two operator++,
one postfix and one prefix (i.e. there's a difference between "dest++" and "++dest.")
Assuming a class C, operator ++ is (usually) modeled as follows (and always with these member function
signatures:)
class C
{
public:
... // misc
C& operator++(void); // prefix, i.e. ++c;
C operator++(int); // postfix, i.e. c++
... // other misc.
};
C& C::operator++(void)
{
... // whatever's needed to "increment" it.
return *this;
}
class int_writer
{
public:
// trust the compiler to generate necessary constructors and
destructor
int_writer& operator++();
int_writer& operator*();
int_writer& operator=(int i); // does the real writing.
};
Operator++ we implement to do nothing at all, since it's not used for anything, but operator* and operator= are
interesting. If you look at a pointer to T, dereferencing it yields a T. In this case, however, I say that dereferencing
an int_writer yields an int_writer. Weird? Well, yes. It's weird, but it makes perfect sense anyway. What do I want to
use the result of operator* for? Only for assigning to, and I want the assignment to write something on standard
output, right? If I make operator* return the very object for which operator* was called on, we can use operator= for
that class to do the writing. If we made operator* return some other type, we need to create two types, one
int_writer, and one class whose only job in life is to be assignable by int, and which very assignment actually means
writing. Perhaps the latter is purer, but the former is so much less work. Here's what the implementation looks like:
int_writer& int_writer::operator++()
{
return *this; // do nothing
}
int_writer& int_writer::operator*()
{
return *this; // do nothing
}
int_writer& int_writer::operator=(int i)
{
cout << i << endl;
return *this;
}
This means that if "dest" is of type int_writer, the line "*dest = *begin", can be expanded to:
dest.operator*().operator=(*begin);
Since the return value of "dest.operator*()" is a reference to "dest" itself, the following "operator=(*begin)" means
"dest.operator=(*begin)", and if the type of "*begin" can be implicitly converted to "int", we're writing something.
Cool eh? Here's all it takes to write the contents of the prime number array:
copy(iarr,iarr+isize,int_writer());
Of course, the name "int_writer" is a dead giveaway for a class template, isn't it? Why limit it to integers only?
copy(iarr,iarr+isize,writer<int>());
With this template, yet another requirement surfaced; for writer<T>, T must be writable through operator<<. Of
course, that's no surprise, how else would you write it?
As a last example, let's have a look at the source side, the types for "IN". Can we create a type matching the
requirements for "IN", such that a copy would read values from standard input (normally the keyboard?)
The requirements for "IN" are a little bit more complicated than those for "OUT." It must be not-equal comparable,
it must be possible to reach one value from another, through operator++, such that operator!= yields true, and
operator* must return a value.
This requires some thought, especially on the reachability issue. To make this example simple, I propose that we can
create a "reader<T>" with a number, and the number is the amount of T's to read from standard input. For every
operator++, a new T is read, and the number of reads remaining is decremented. It must also be possible to create an
"end" reader<T>, and we can use the parameter-less constructor for that. Here's how reader<T> might look like:
Conclusion
What you've seen here is, most probably your first ever encounter with, generic programming. The template
parameters "IN" and "OUT" (from "copy") are called "iterators," or "input iterators" and "output iterators" to be
more specific. Anything that behaves like an input iterator, can be used as one, and likewise for output iterators. We
can write input iterators for data base access, enumerating files in a directory, the series of prime numbers or
whatever you want to get values from. We can write output iterators to store values in a data base, to enter values at
the end of a linked list, to send audio data to our sound card, and whatever you need. The function template "copy"
will be useful for any combination of the above, as long as the types are convertable from *IN to *OUT. This is
*VERY* useful. If you write an algorithm in terms of generic iterators, you can use it with any kind of data
source/sink which have iterators that follows your convention.
To make matters even better, this is all part of the now final draft C++ standard. The draft contains a function
template "copy", which behaves identically to what I used in this article, and iterators called "input_iterator" and
"output_iterator" which behaves very similarly to the "reader" and "writer" class templates.
The standard documents 5 iterator categories, input iterator, output iterator, forward iterator (sort of the combination,
allows both read and write,) bidirectional iterator (like forward, but allows moving backwards too,) and lastly
random access iterators (iterators which can be incremented/decremented by more than one.) Pointers in arrays are
typical bidirectional iterators. If you write your iterators to comply with the requirements of one of these categories,
your iterators can be used with any of the algorithms that requires such iterators. Every algorithm you write can be
used with any iterators of the type your algorithm requires. That is a major time/code/debug saver.
Part Part Part Part Part Part Part Part Part Part1 Part1 Part1 Part1
1 2 3 4 5 6 7 8 9 0 1 2 3
class Shape
{
public:
virtual void draw(Canvas&) = 0;
virtual void rotate(double angle) = 0;
virtual void translate(Coordinate c) = 0;
virtual void scale(double) = 0;
};
The ``= 0'' ending of a member function declaration makes it pure virtual. Pure virtual means that it must be
overridden by descendants. Having one or more pure virtual member functions in a class makes the class an abstract
base class. Abstract because you cannot instantiate objects of the class. If you try you'll get compiler errors. A class
which has only pure virtual member functions and no data is often called a pure abstract base class, or some times an
interface class. The latter is more descriptive; the class defines an interface that descendants must conform to, and
any piece of code that can understand the interface can operate on objects implementing the interface (the concrete
classes like ``Triangle'', ``Rectangle'', and ``Circle'').
The graphically experienced reader has of course noticed that rotation of a circle can be implemented extremely
efficiently by doing nothing at all, so how can we take care of that scenario? It's unnecessary to write code that does
nothing, is it not? Let's have a look at the alternatives.
• Let's just ignore it. It won't work, though, since then our ``Circle'' class will be an abstract class (at least one
pure virtual is not ``terminated.'')
• We can change the interface of ''Shape`` such that ``rotate'' is not a pure virtual, and code its
implementation to do nothing. This doesn't seem like a good idea because then the programmer
implementing the square might forget to implement ``rotate'' without getting compiler errors.
The root of this lies in the illusion that doing nothing at all is the default behaviour, while it is an optimization for
circles. As such the ``do nothing at all'' code belongs in ``Circle`` only. In other words, the best solution is with the
original pure abstract ``Shape'' class, and an empty implementation for ``Circle::rotate.''
Sweden
Name
Street Number
{Country-Code}Postal-Code City
{Country-Name}
USA
Name
Number Street
City, State Zip
{Country-Name}
Name
Number Street
City
{Country}
Postal-Code
Then, of course, there are totally different types of addresses. E-mail, Ham Radio call-signs, phone number, fax
number, etc.
As a simplification for this example I'll treat State and Zip in U.S. addresses as a unit, and I will assume that Postal-
Code and State/Zip in U.S. addresses are synonymous (i.e. I'll only have one field that's used either as postal code or
as state/zip combination, depending on country). As an exercise you can improve this. Make sure ``State'' is only
dealt with in address kinds where it makes sense. The Country-Code as can be seen in the Swedish address example
will also be ignored (this too makes for an excellent exercise to include). The address class hierarchy will be done
such that other kinds of addresses like e-mail addresses and phone numbers can be added.
Here's the base class:
class Address
{
public:
virtual const char* type() const = 0;
virtual void print(int international=0) const = 0;
virtual void acquire(void) = 0;
virtual ~Address();
};
The idea here is that ``type'' can be used to ask an address object what kind of address it is, a mailing address, e-mail
address and so on. If the parameter for ``print'' is non-zero, the address will be printed in international form, (i.e.
country name will be added to mailing addresses and international prefixes added to phone numbers). The member
function ``acquire'' is used for asking an operator to enter address data. Note that the destructor is virtual, but not
pure virtual (what would happen if it was?)
Unselfish protection
All kinds of mailing addresses will share a base, inheriting from ``Address'', that contains the address fields, and
ways to access them. This class, however, will not implement any of the formatting pure virtuals from ``Address.''
That must be done by the concrete address classes with knowledge about the country's formatting and naming. The
member function ``type'' will be defined here, however, to always return the string ``Mailing address'', since all
kinds of mailing addresses are mailing addresses, even if they're Swedish addresses or U.S. Addresses. Access to the
address fields is for the concrete classes only, and this is a problem. We've seen how we can make things generally
available by declaring them public, or by hiding them from the general public by making them private. Here we
want something in between. We want descendants, the concrete address classes, to access the address fields, but
only the descendants and no one else. This can be achieved through the third protection level, ``protected.'' Protected
means that access is limited to the class itself (of course) and all descendants of it. It is thus looser than private, but
much stricter than public.
Here comes the ``MailingAddress'' base class:
class MailingAddress : public Address
{
public:
virtual ~MailingAddress();
const char* type() const;
protected:
MailingAddress();
Address::~Address()
{
}
A trap many beginners fall into is to think that since the destructor is empty, we can save a little typing by declaring
it pure virtual and there won't be a need to implement it. That's wrong, though, since the destructor will be called
when a descendant is destroyed. There's no way around that. If you declare it pure virtual and don't implement it,
you'll probably get a nasty run-time error when the first concrete descendant is destroyed.
The observant reader might have noticed a nasty pattern of the authors refusal to get to the point with pure virtuals
and implementation. Yes, you can declare a member function pure virtual, and yet implement it! Pure virtual does
not illegalize implementation. It only means that the pure virtual version will NEVER be called through virtual
dispatch (i.e. by just calling the function on an object, a reference or a pointer to an object.) Since it will never, ever,
be called through virtual dispatch, it must be implemented by the descendants, hence the rule that you cannot
instantiate objects where pure virtuals are not terminated. By termination, by the way, I mean declaring it in a non
pure virtual way. OK, so a pure virtual won't ever be called through virtual dispatch. Then how can one be called?
Through explicit qualification. Let's assume, just for the sake of argument, that we through some magic found a way
to implement the some reasonable generic behaviour of ``acquire'' in ``Address,'' but we want to be certain that
descendants do implement it. The only way to call the implementation of ``acquire'' in ``Address'' is to explicitly
write ``Address::acquire.'' This is what explicit qualification means. There's no escape for the compiler; writing it
like this can only mean one thing, even if ``Address::acquire'' is declared pure virtual.
Now let's look at the middle class, the ``MailingAddress'' base class.
MailingAddress::~MailingAddress()
{
delete[] name_data;
delete[] street_data;
delete[] number_data;
delete[] city_data;
delete[] postalCode_data;
delete[] country_data;
}
I said when explaining the interface for this class, that it is responsible for handling the resources for the member
data. Since we don't know the length of the fields, we oughtn't restrict them, but rather dynamically allocate
whatever is needed. The ``delete[]'' syntax is for deleting arrays as opposed to just ``delete'' which deletes single
objects. Note that it's legal to delete the 0 pointer. This is used here. If, for some reason, one of the fields are not set
to anything, it will be 0. Deleting the 0 pointer does nothing at all. From this to the constructor:
MailingAddress::MailingAddress()
: name_data(0),
street_data(0),
number_data(0),
city_data(0),
postalCode_data(0),
country_data(0)
{
}
The only thing the constructor does is to make sure all pointers are 0, in order to guarantee destructability.
The ``type'' and read-access methods are trivial:
name(name());
The meaning of this is, of course, ``set the name to what it currently is.'' We must make sure that doing this works
(or find a way to illegalize the construct, but I can't think of any way). If the source and destination are different,
however, the old destination must be deleted, a new one allocated on heap and the contents copied. Like this:
SwedishAddress::SwedishAddress()
: MailingAddress()
{
country("Sweden"); // what else?
}
void SwedishAddress::acquire(void)
{
char buffer[100]; // A mighty long field
USAddress::USAddress()
: MailingAddress()
{
country("U.S.A."); // what else?
}
void USAddress::acquire(void)
{
char buffer[100]; // Seems like a mighty long field
A toy program
Having done all this work with the classes, we must of course play a bit with them. Here's an short and simple
example program that (of course) also makes use of the generic programming paradigm introduced last month.
int main(void)
{
const unsigned size=10;
Address* addrs[size];
Address** first = addrs; // needed for VACPP (bug?)
Address** last = get_addrs(addrs,addrs+size);
switch (answer[0]) {
case 'U': case 'u':
*current = new USAddress;
break;
case 'S': case 's':
*current = new SwedishAddress;
break;
default:
return current;
}
(**current).acquire();
++current;
}
return current;
}
In part 6 I mentioned that virtual dispatch could replace switch statements, and yet here is one. Could this one be
replaced with virtual dispatch as well? It would be unfair of me to say ``no'', but it would be equally unfair of me to
propose using virtual dispatch here. The reason is that we'd need to work a lot without gaining anything. Why? We
obviously cannot do virtual dispatch on the ``Address'' objects we're about to create, since they're not created yet.
Instead we'd need a set of address creating objects, which we can access through some subscript or whatever, and
call a virtual creation member function for. Doesn't seem to save a lot of work does it? Probably the selection
mechanism for which address creating object to call would be a switch statement anyway!
So, that was reading, now for the rest. ``for_each'' does something for every iterator in a range. It could be
implemented like this:
print::print(int i)
: international(i)
{
}
Recap
This month, you've learned:
• what pure virtual means, and how you declare pure virtual functions.
• that despite what most C++ programmers believe, pure virtual functions can be implemented.
• that the above means that there's a distinction between terminating a pure virtual, and implementing one.
• why it's a bad idea to make destructors pure virtual.
• a new protection level, ``protected.''
• why protected data is bad, and how you can work around it in a clever way.
• that switch statements cannot always be replaced by virtual dispatch.
• that there is a ``function call'' operator and how to define and use it.
Exercises
• Find out what happens if you declare the ``MailingAddress'' destructor pure virtual, and yet define it.
• Think of two ways to handle the State/Zip problem, and implement both (what are the advantages,
disadvantages of the methods?)
• Rewrite ``get_addrs'' to accept templatized iterators instead of pointers.
Coming up
As I mentioned just a few lines above, most of the C++ language is covered. Quite a bit of the library remains, and
lots of useful and cool techniques are waiting to be exploited. Here's something for you to think about destructor,
pointer and delete. Anyway, next month we'll look at file I/O (finally). /Björn
Part Part Part Part Part Part Part Part Part Part1 Part1 Part1 Part1
1 2 3 4 5 6 7 8 9 0 1 2 3
In parts 5 and 6, the basics of I/O were introduced, with formatted reading and writing from standard input and
output. We'll now have a look at I/O for files. In a sense, it's better to stop using the term I/O here, and instead use
streams and streaming, since the ideas expressed here and in parts 5 and 6 can be used for other things than I/O, for
example in-memory formatting of data (we'll see that at the very end of this article.)
Files
In what way is writing ``Hello world'' on standard output different from writing it to a file? The question is worth
some thought, since in many programming languages there is a distinct difference. Is the message different? Is the
format (as seen from the program) different? I cannot see any difference in those aspects. The only thing that truly
differs is the media where the formatted message ends up. In the former case, it's on your screen, but for file I/O it's
in a file somewhere on your hard disk. In other words, there is very little difference, or at least, there's very much in
common.
As we've seen so far, commonality is expressed either through inheritance or templates, depending on what's
common and what's not. To refresh your memory, templates are used when we want the same kind of behaviour,
independent of data. For example a stack of some data type. Inheritance is used when you want similar, but in some
important aspects different, behaviour at runtime for the same kind of data. We saw this for the staff hierarchy and
mailing addresses in parts 7 and 8. In this case it's inheritance that's the correct solution, since the data will be the
same, but where it will end up (and most notably, how it does end up there) differs. (Incidentally, there's a good case
for using templates too, regarding the type of characters used. The C++ standard does indeed have templatized
streams, just for differing between character types. Few compilers today support this, however. See the ``Standards
Update'' towards the end of the article for more information.)
The inheritance tree for stream types look like this:
The way to read this is that there's a base class named ``ios'', from which the classes ``istream'' and ``ostream''
inherit. The classes ``ifstream'' and ``ofstream'' in their turn inherit from ``istream'' and ``ostream'' respectively. The
``f'' in the names imply that they're file streams. Then there's the odd ones, ``iostream'', which inherits from both
``istream'' and ``ostream'', and ``fstream'' which inherits from both ``ifstream'' and ``ofstream.'' Inheriting from two
bases is called multiple inheritance, and is by many seen as evil. Many programming languages have banned it:
Objective-C, Java, Smalltalk to mention a few, while other programming languages, like Eiffel, go to the other
extreme and allow you to inherit the same base several times Personally I think multiple inheritance is very useful if
used right, but it can cause severe problems. Here is a situation where it's used in the right way. Anyway, this means
that ``fstream'' is a file stream for both reading and writing, while ``iostream'' is an abstract stream for both reading
and writing. More often than you think, you probably don't want to use the ``iostream'' or ``fstream'' classes.
This inheritance, however, means that all the stream insertion and extraction functions (the ``operator>>'' and
``operator<<'') you've written, will work just as they do with file streams. Now, wasn't that neat? In other words, the
only things you need to learn for file based I/O are the details that are specific to files.
File Streams
The first thing you need to know before you can use file streams is how to create them. The parts of interest look
like this:
ios::ate open with the get and set pointer at the end
(see Seeking for info) of the file.
ios::app open for append, that is, any write you make
to the file will be appended to the file.
#include <fstream.h>
Binary streaming
So far we've dealt with formatted streaming only, that is, the process of translating raw data into a human readable
form, or translating human readable data into the computer's internal representation. Some times you want to stream
raw data as raw data, for example to save space in a file. If you look at a file produced by, for example a word
processor, it's most likely not in a human readable form. Note that binary streaming does not necessarily mean using
the ``ios::binary'' mode when opening a file (although, that is indeed often the case.) They're two different concepts.
Binary streaming is what you use your stream for, raw data that is, and opening a file with the ``ios::binary'' mode,
means turning the brain damaged LF<->CR/LF translation off.
Binary streaming is done through the stream member functions :
ostream& ostream::flush();
Force the data in the stream to be written (file streams are usually buffered.)
int istream::get();
Read one character from the stream, and return it. The value is an ``int'' instead of ``char'' since the return value
might be ``EOF'' (which is not uniquely representable as a ``char.'')
#include <fstream.h>
#include <fstream.h>
Seeking
Up until now we have seen streams as, what it sounds like, continuous streams of data. Sometimes however, there's
a need to move around, both backward and forward. Streams like standard input and standard output are truly
continuous streams, within which you cannot move around. Files, in contrast, are true random access data stores.
Random access streams have something called position pointers. They're not to be confused with pointers in the
normal C++ sense, but it's something referring to where in the file you currently are. There's the put pointer, which
refers to the next position to write data to, if you attempt to write anything, and the get pointer, which refers to the
next position to read data from. An ostream of course only has the put pointer, and an istream only the get pointer.
There's a total of 6 new member functions that deal with random access in a stream:
streampos istream::tellg();
istream& istream::seekg(streampos);
streampos ostream::tellp();
ostream& ostream::seekp(streampos);
class FileArray
{
public:
FileArray(const char* name, size_t elements);
// Create a new array and set the size.
FileArray<int> x;
...
x[5] = 4;
int y = x[3];
When ``operator[]'' is on the left hand side of an assignment, I want to write data to the file, and if its on the right
hand side of an assignment, I want to read data from the file. Ouch.
Warning: I've often seen it suggested that the solution is to have the const version read and return a value, and the
non-const version write a value. As slick as it would be, it's wrong and it won't work. The const version is called for
const array objects, the non-const version for non-const array objects.
Instead what we have to do is to pull a little trick. The trick is, as so often in computer science, to add another level
of indirection. This is done by not taking care of the problem in ``operator[],'' but rather let it return a type, which
does the job. We create a class template, looking like this:
FileArrayProxy<T>&
operator=(const FileArrayProxy<T>& p);
FileArrayProxy(const FileArrayProxy<T>&);
private:
... all other constructors.
FileArray<T>& array;
const size_t index;
};
We have to make sure, of course, that there are member functions in ``FileArray<T>'' that can read and write (and of
course, those functions are not the ``operator[],'' since then we'd have an infinite recursion.) All constructors, except
for the copy constructors, are made private to prevent users from creating objects of the class whenever they want
to. After all, this class is a helper for the array only, and is not intended to ever even be seen. This, however, poses a
problem; with the constructors being private, how can ``FileArray<T>::operator[]()'' create and return one?
Enter another C++ feature: friends. Friends are a way of breaking encapsulation. What?!?! Yes, what you read is
right. Friends break encapsulation, and (this is the real shock) that's a good thing! Friends break encapsulation in a
controlled way. We can, in ``FileArrayProxy<T>'' declare ``FileArray<T>'' to be a friend. This means that
``FileArray<T>'' can access everything in ``FileArrayProxy<T>,'' including things that are declared private.
Paradoxically, violating encapsulation with friendship strengthens encapsulation when done right. The only
alternative here to using friendship, is to make the constructors public, but then anyone can create objects of this
class, and that's what we wanted to prevent. Friends are useful for strong encapsulation, but it's important to use it
only in situations where two (or more classes) are so tightly bound to one another that they're meaningless on their
own. This is the case with ``FileArrayProxy<T>.'' It's meaningless without ``FileArray<T>,'' thus ``FileArray<T>'' is
declared a friend of ``FileArrayProxy<T>.'' The declaration then becomes:
class FileArrayProxy
{
public:
FileArrayProxy& operator=(const T&); // write a value
operator T() const; // read a value
// compiler generated destructor
FileArray<T>& array;
const size_t index;
// farray.hpp
#ifndef FARRAY_HPP
#define FARRAY_HPP
#include <fstream.h>
#include <stdlib.h> // size_t
fstream stream;
size_t max_size;
stream.read((char*)&t, sizeof(t));
// what if read fails?
return t;
}
All of a sudden, we face an unexpected problem. The above code won't compile. The member function is declared
``const'', and as such, all member variables are ``const'', and neither ``seekg'' nor ``read'' are allowed on constant
streams. The problem is one of differing between logical constness and bitwise constness. This member function is
logically ``const'', as it does not alter the array in any way. However, it is not bitwise const; the stream member
changes. C++ cannot understand logical constness, only bitwise constness. If you have a modern compiler, the
solution is very simple; you declare ``stream'' to be ``mutable fstream stream;'' in the class definition. I, however,
have a very old compiler, so I have to find a different solution. This solution is, yet again, one of adding another
level of indirection. I can have a pointer to an ``fstream.'' When in a ``const'' member function, the pointer is also
``const'', but not what it points to (there's a difference between a constant pointer, and a pointer to a constant.) The
only reasonable way to achieve this is to store the stream object on the heap, and in doing this I introduce a possible
danger; what if I forget to delete the pointer? Sure, I'll delete it in the destructor, but what if an exception is thrown
already in the constructor, then the destructor will never execute (since no object has been created that must be
destroyed.)
Do you remember the ``thing to think of until this month?'' The clues were, destructor, pointer and delete. Thought
of anything? What about this extremely simple class template?
T* p;
};
T t;
(*pstream).read((char*)&t, sizeof(t));
// what if read fails?
return t;
}
I bet the change wasn't too horrifying.
(*pstream).write((char*)&elem, sizeof(elem));
// what if write failed?
}
Now for the constructors:
T t;
storeElement(max_size-1,t);
// What if this fails?
}
size_t index;
FileArray<T>& fa;
#endif // FARRAY_HPP
That was it. Can you see what happens with the proxy? Let's analyze a small code snippet:
1 FileArray<int> arr("file",10);
2 arr[2]=0;
3 int x=arr[2];
4 arr[0]=arr[2];
On line two, ``arr.operator[](2)'' is called, which creates a ``FileArrayProxy<int>'' from ``arr'' with the index 2. The
object, which is a temporary and does not have a name, has as its member ``fa'' a reference to ``arr'', and as its
member ``index'' the value 2. On this temporary object, ``operator=(int)'' is executed. This operator in turn calls
``fa.storeElement(index, t),'' where ``index'' is still 2 and the value of ``t'' is 0. Thus, ``arr[2]=0'' ends up as
``arr.storeElement(2,0)''. On line 3, a similar proxy is created through the call to ``operator[](2)'' This time, however,
the ``operator int() const'' is called. This member function in turn calls ``fa.readElement(2)'' and returns its value,
thus ``int x=arr[2]'' translates to ``int x=arr.readElement(2).'' On line 4, finally, ``arr[0]=arr[2]'' creates two
temporary proxies, one referring to index 0, and one to index 2. The assignment operator is called, which in turn
calls ``fa.storeElement(0,p)'', where p is the temporary proxy referring to element 2. Since ``storeElement'' wants an
``int,'' ``p.operator int() const'' is called, which calls ``arr.readElement(2).'' In other words ``arr[0] = arr[2]'' generates
the code ``arr.storeElement(0, arr.readElement(2)).''
As you can see, the proxies don't add any new functionality, they're just syntactic sugar, albeit very useful. With
them we can treat our file arrays very much like any kind of array. There's one thing we cannot do:
int* p = &arr[2];
int& x = arr[3];
*p=2;
x=5;
With ordinary arrays, the above would be legal and have well defined semantics, assigning arr[2] the value 2, and
arr[3] the value 5. With our file array we cannot do this, but unfortunately the compiler does not prevent it (a decent
compiler will warn that we're binding a constant or pointer to a temporary.) We'll mend that hole next month (think
about how) and also add iterators, which will allow us to use the file arrays almost exactly like real ones.
char* s = "23542";
istrstream is(s);
int x;
is >> x;
After executing this snippet, ``x'' will have the value 23542. ``istrstream'' isn't much more exciting than that.
``ostrstream'' on the other hand is more exciting. There are two alternative uses for ``ostrstream.'' One where you
have an array you want to store data in, and one where you want the ``ostrstream'' to create it for you, as needed
(usually because you have no idea what size the buffer must have.) The former usage is like this:
char buffer[24];
ostrstream os(buffer, sizeof(buffer));
double x=23.34;
os << "x=" << x << ends;
The variable ``buffer'' will contain the string ``x=23.34'' after this snippet. The stream manipulator ``ends'' zero
terminates the buffer. Zero termination is not done by default, since the stream cannot know where to put it, and
besides you might not always want it.
The other variant, where you don't know how large a buffer you will need, is generally more useful (I think.)
ostrstream os;
double x=23.34, y=34.45;
os << x << '*' << y << '=' << x*y << ends;
const char* p = os.str();
const size_t length=os.pcount();
Standards update
With the C++ standard, a lot of things have changed regarding streams. As I mentioned already last month, the
headers are actually <iostream> and <fstream>, and the names std::istream, std::ostream, etc. The streams are
templatized too, which both makes life easier and not. The underlying type for std::ostream is:
std::basic_ostream<class charT,
class traits=std::char_traits<charT> >
``charT'' is the basic type for the stream. For ``ostream'' this is ``char'' (ostream is actually a typedef.) There's another
typedef, ``std::wostream'', where the underlying type is ``wchar_t'', which on most systems probably will be 16-bit
Unicode. The class template ``char_traits'' is a traits class which holds the type used for EOF, the value of EOF, and
some other house keeping things.
Why the standard has removed the file stream open modes ios::create and ios::nocreate is beyond me, as they're
extremely useful.
Casting is ugly, and it's hard to see in large code blocks. There are four new cast operators, that are highly visible, in
the standard. They're (in approximate order of increasing danger,) dynamic_cast<T>, static_cast<T>, const_cast<T>
and reinterpret_cast<T>. In the binary streaming seen in this article, reinterpret_cast<T> would be used, as a way of
saying, ``Yeah, I know I'm violating type safety, but hey, I know what I'm doing, OK?'' The good thing about it is
that it's so visible that anyone doubting it can easily spot the dangerous lines and have a careful look. The syntax is:
os.write(reinterpret_cast<const char*>(&variable), sizeof(variable));
Finally, the generally useful strstreams has been replaced by ``std::istringstream'', ``std::ostringstream'' and
``std::stringstream'' (plus wide variants, std::wistringstream, etc.) defined in the header <sstream>. They do not
operate on ``char*'', but on strings (there is a string class, or again, rather a string class template, where the most
important template parameter is the underlying character.) ``std::ostringstream'' does not suffer from the freeze
problem that ``ostrstream'' does.
Recap
The news this month were:
• streams dealing with files, or in-memory formatting, are used just the same way as the familiar ``cout'' and
``cin,'' which saves both learning and coding (the already written ``operator<<'' and ``operator>>'' can be
used for all kinds of streams already.)
• streams can be used for binary, unformatted I/O too. This normally doesn't make sense for ``cout'' and
``cin'' or in-memory formatting (as the name implies,) but it's often useful when dealing with files.
• It is possible to move around in streams, at least file streams and in-memory formatting streams. It's
generally not possible to move around in ``cin'' and ``cout.''
• proxy classes can be used to differentiate read and write operations for ``operator[]'' (the construction can of
course be used elsewhere too, but it's most useful in this case.)
• friends break encapsulation in a way that, when done right, strengthens encapsulation.
• there's a difference between logical const and bitwise const, but the C++ compiler doesn't know and always
assumes bitwise const.
• truly simple smart pointers can save some memory management house keeping, and also be used as a work
around for compilers lacking ``mutable'' (i.e. the way of declaring a variable as non-const for const
members, in other words, how to differentiate between logical and bitwise const.)
• streams can be used also for in-memory formatting of data.
Exercises
• Improve the file array such that it accepts a ``stream&'' instead of a file name, and allows for several arrays
in the same file.
• Improve the proxy such that ``int& x=arr[2]'' and ``int* p=&arr[1]'' becomes illegal.
• Add a constructor to the array that accepts only a ``size_t'' describing the size of the array, which creates a
temporary file and removes it in its destructor.
• What happens if we instantiate ``FileArray'' with a user defined type? Is it always desireable? If not, what is
desireable? If you cannot define what's desireable, how can instantiation with user defined types be
banned?
• How can you, using the stream interface, calculate the size of a file?
Coming up
Next month will be devoted to improving the ``FileArray.'' We'll have iterators, allow arbitrary types, add error
handling and more. I assume I won't need to tell you that it'll be possible to use the ``FileArray,'' just as ordinary
arrays with generic programming, i.e. we can have the exact same source code for dealing with both!
Part Part Part Part Part Part Part Part Part Part1 Part1 Part1 Part1
1 2 3 4 5 6 7 8 9 0 1 2 3
[Note: the source code for this month is here. Ed.]
Last month a file based array template for truly huge amounts of data was introduced. While good, it was nowhere
near our goals. Error handling was missing completely, making it dangerous to use in real life. There was no way to
say how a user defined data type should be represented on disk, yet they weren't disallowed, which is a dangerous
combination. It was also lacking iterators, something that is handy, and is an absolute requirement for generic
programming with algorithms that are independent of the source of the data. On top of that, we'd really like the
ability to store several different arrays in the same file, and also have an anonymous array which creates a temporary
file and removes it when the array is destroyed. All of these will be dealt with this month, yet very little will be new.
Instead it's time to make use of all the things learned so far in the course.
class ArrayFileElementAccess<X>
{
public:
enum { size= ... };
...
};
This is a bit ugly, but it is perfectly harmless.
The advantage gained by adding the traits class is flexibility and safety. If someone wants to use a file array for their
own class, they're free to do so. However, they must first write a ``FileArrayElementAccess'' specialisation. Failure
to do so will result in a compilation error. This early error detection is beneficial. The sloppy solution from last
month would not yield any error until run-time, which means a (usually long) debugging session.
FileArray(fstream& fs);
// use an existing file and get size from there
...
private:
void initFromFile(const char*);
fstream& stream;
size_t array_size; // in elements
streampos home;
};
char pattern[6];
stream.read(pattern,6);
if (stream.eof()) {
stream.clear(); // clear error state
// and initialise.
home = stream.tellp();
// Then we must go the the end and write
// the end pattern.
stream.seekp(home+elem_size*array_size);
stream.write("AEnd",4);
home = stream.tellg();
stream.seekg(home+elem_size*array_size);
char epattern[4];
stream.read(epattern,4);
if (strncmp(epattern,"AEnd",4)) {
// Whoops, corrupt file!
stream.clear(ios::failbit);
return;
}
// Seems like we have a valid array!
}
Other than the above, the only change needed for the array is that seeking will be done relative to ``home'' rather
than the beginning of the file (plus the size of the header entries.) The new versions of ``storeElement'' and
``readElement'' become:
return traits::readFrom(stream);
// what if read fails?
// What if too much data is read?
}
class temporaryfile
{
public:
temporaryfile();
~temporaryfile();
iostream& stream();
private:
char* name;
fstream fs;
};
temporaryfile::temporaryfile()
: name(::tempnam(".","array")),
fs(name, ios::in|ios::out|ios::binary)
{
// what if tmpnam fails and name is 0
// what if fs is bad?
}
temporaryfile::~temporaryfile()
{
fs.close();
::remove(name);
// what if remove fails?
::free(name);
}
In the above code, ``tempnam'', ``remove'' and ``free'' are prefixed with ``::``, to make sure that it's the names in
global scope that are meant, just in case someone enhances the class with a few more member functions whose name
might clash.
For the sake of syntactical convenience, I have added yet another operator to the ``ptr'' class template:
Code reuse
If you're an experienced C programmer, especially experienced with programming embedded systems where
memory constraints are tough and you also have a good memory, you might get a feeling that something's wrong
here.
What I'm talking about is something I mentioned the first time templates were introduced: ``Templates aren't source
code. The source code is generated by the compiler when needed.'' This means that if we in a program uses
FileArray<int>, FileArray<double>, FileArray<X> and FileArray<Y> (where ``X'' and ``Y'' are some classes,) there
will be code for all four types. Now, have a close look at the member functions and see in what way
``FileArray<int>::FileArray(iostream& fs, size_t elements)'' differs from ``FileArray<char>::FileArray(iostream&
fs, size_t elements)''. Please do compare them.
What did you find? The only difference at all is in the handling of the member ``elem_size'', yet the same code is
generated several times with that as the only difference. This is what is often referred to as the template code bloat
of C++. We don't want code bloat. We want fast, tight, and slick applications.
Since the only thing that differs is the size of the elements, we can move the rest to something that isn't templatised,
and use that common base everywhere. I've already shown how code reuse can be done by creating a separate class
and have a member variable of that type. In this article I want to show an alternative way of reusing code, and that is
through inheritance. Note very carefully that I did not say public inheritance. Public inheritance models ``is-A''
relationships only. We don't want an ``is-A'' relationship here. All we want is to reuse code to reduce code bloat.
This is done through private inheritance. Private inheritance is used far less than it should be. Here's all there is to it.
Create a class with the desired implementation to reuse and inherit privately from it. Nothing more, nothing less. To
a user of your class, it matters not at all if you chose not to reuse code at all, reuse through encapsulation of a
member variable, or reuse through private inheritance. It's not possible to refer to the descendant class through a
pointer to the private base class, private inheritance is an implementation detail only, and not an interface issue.
To the point. What can, and what can not be isolated and put in a private base class? Let's first look at the data. The
``stream'' reference member can definitely be moved to the base, and so can the ``pfile'' member for temporary files.
The ``array_size'' member can safely be there too and also the ``home'' member for marking the beginning of the
array on the stream. By doing that alone we have saved just about nothing at all, but if we add as a data member in
the base class the size (on disk) for the elements, and we can initialise that member through the
``FileArrayElementAccess::size'' traits member, all seeking in the file, including the initial seeking when creating
the file array, can be moved to the base class. Now a lot has been gained. Left will be very little. Let's look at the
new improved implementation:
Now for the declaration of the base class.
class FileArrayBase
{
public:
protected:
FileArrayBase(iostream& io,
size_t elements,
size_t elem_size);
FileArrayBase(iostream& io);
FileArrayBase(size_t elements, size_t elem_size);
iostream& seekp(size_t index) const;
iostream& seekg(size_t index) const;
size_t size() const; // number of elements
size_t element_size() const;
private:
class temporaryfile
{
public:
temporaryfile();
~temporaryfile();
iostream& stream();
private:
char* name;
fstream fs;
};
void initFromFile(const char* p);
ptr<temporaryfile> pfile;
iostream& stream;
size_t array_size;
size_t e_size;
streampos home;
};
The only surprise here should be the nesting of the class ``temporaryfile.'' Yes, it's possible to define a class within a
class. Since the ``temporaryfile'' class is defined in the private section of ``FileArrayBase'', it's inaccessible from
anywhere other than the ``FileArrayBase'' implementation. It's actually possible to nest classes in class templates as
well, but few compilers today support that. When implementing the member functions of the nested class, it looks a
bit ugly, since the surrounding scope must be used.
FileArrayBase::temporaryfile::temporaryfile()
: name(::tempnam(".","array")),
fs(name,ios::in|ios::out|ios::binary)
{
// what if tmpnam fails and name is 0
// what if fs is bad?
}
FileArrayBase::temporaryfile::~temporaryfile()
{
fs.close();
::remove(name);
// What if remove fails?
::free(name);
}
iostream& FileArrayBase::temporaryfile::stream()
{
return fs;
}
The implementation of ``FileArrayBase'' is very similar to the ``FileArray'' earlier. The only difference is that we use
a parameter for the element size, instead of the traits class.
FileArrayBase::FileArrayBase(iostream& io,
size_t elements,
size_t elem_size)
: stream(io),
array_size(elements),
e_size(elem_size)
{
char pattern[sizeof(ArrayBegin)];
stream.read(pattern,sizeof(pattern));
if (stream.eof()) {
stream.clear(); // clear error state
// and initialize.
// begin of array pattern.
stream.write(ArrayBegin,sizeof(ArrayBegin));
stream.seekp(home+elem_size*array_size);
stream.write(ArrayEnd,sizeof(ArrayEnd));
if (array_size != elements) {
// Uh oh. The data read from the stream,
// and the size given in the constructor
// mismatches! What now?
stream.clear(ios::failbit);
}
if (e_size != elem_size) {
stream.clear(ios::failbit);
}
// set put and get pointer to past the end pos.
stream.seekp(stream.tellg());
}
To make life a little bit easier, I've assumed two arrays of char named ``ArrayBegin'' and ``ArrayEnd'', which hold
the patterns to be used for marking the beginning and end of an array on disk.
FileArrayBase::FileArrayBase(iostream& io)
: stream(io)
{
char pattern[sizeof(ArrayBegin)];
stream.read(pattern,sizeof(pattern));
initFromFile(pattern);
FileArrayBase::FileArrayBase(size_t elements,
size_t elem_size)
: pfile(new temporaryfile),
stream(pfile->stream()),
array_size(elements),
e_size(elem_size),
home(stream.tellg())
{
stream.seekp(home+array_size*e_size);
char c;
stream.write(&c,1);
// set put and get pointer to past the end pos.
stream.seekg(stream.tellp());
}
class A {};
class B : public A {};
class C : public A {};
class B1 : public B{};
void x()
{
try {
f();
}
catch (B& b) {
// **1
}
catch (C& c) {
// **2
}
catch (A& a) {
// **3
}
}
At ``**1'' above, objects of class ``B'' and class ``B1'' are caught if thrown from ``f''. In ``**2'' objects of class ``C''
(and descendants of C, if any are declared elsewhere) are caught. At ``**3'' all others from the ``A'' hierarchy are
caught.
This may seem like a curious detail of purely academic worth, but it's extremely useful. We can use abstraction
levels for errors. For example, we can have a root class ``FileArrayException'', from which all other exceptions
regarding the file array inherits. We can see that there are clearly two kinds of errors that can occur in the file array;
abuse and environmental issues outside the control of the programmer. For abuse I mean things like indexing
outside the valid bounds, and with environmental issues I mean faulty or full disks (Since there are several programs
running, a check if there's enough disk space is still taking a chance. Even if there was enough free space when the
check was made, that space may be occupied when the next statement in the program is executed.)
A reasonable start for the exception hierarchy then becomes:
class FileArrayCreateError
: public FileArrayRuntimeError {};
For whenever the creation of the array fails, regardless of why (it's not very easy to find out if it's a faulty disk or
lack of disk space, for example.)
class FileArrayStreamError
: public FileArrayRuntimeError {};
If after creation, something goes wrong with a stream; for example if seeking or reading/writing fails.
class FileArrayDataCorruptionError
: public FileArrayRuntimeError {};
If an array is created from an old existing file, and we note that the header or trailer doesn't match the expected.
class FileArrayBoundsError
: public FileArrayLogicError {};
Addressing outside the legal bounds.
class FileArrayElementSizeError
: public FileArrayLogicError {};
If the read/write members of the element access traits class are faulty and either write too much (thus overwriting the
data for the next element) or reads too much (in which case the last few bytes read will be garbage picked from the
next element.)
It's of course possible to take this even further. I think this is quite enough, though.
Now we have a reasonably fine level of error reporting, yet an application that wishes a coarse level of error
handling can choose to catch the higher levels of the hierarchy only.
As an exercise, I invite you to add the throws to the code. Beware, however; it's not a good idea to add exception
specifications to the member functions making use of the T's (since you cannot know which operations on T's that
may throw, and what they do throw.) You can increase the code size and eligibility gain from the private inheritance
of the implementation in the base by putting quite a lot of the error handling there.
Iterators
An iterator into a file array is something whose behavior is analogous to that of pointers into arrays. We want to be
able to create an iterator from the array (in which case the iterator refers to the first element of the array.) We want
to access that element by dereferencing the iterator (unary operator *,) and we want iterator arithmetic with integers.
An easy way of getting there is to let an iterator contain a pointer to a file array, and an index. Whenever the iterator
is dereferenced, we return (*array)[index]. That way we even have error handling for iterator arithmetic that lead us
outside the valid range for the array given for free from the array itself. The iterator arithmetics becomes simple too,
since it's just ordinary arithmetics on the index type. The implementation thus seems easy; all that's needed is to
define the operations needed for the iterators, and the actions we want. Here's my idea:
• creation from array yields iterator referring to first element
• copy construction and assignment are of course well behaved.
• moving forwards and backwards with operator++ and operator--.
• addition of array and ``long int'' value ``n'' yields iterator referring to n:th element of array.
• iterator+=n (where n is of type long int) adds n to the value of the index in the iterator. This addition is
never an error; it's dereferencing the iterator that's an error if the index is out of range. Operator -= is
analogous.
• iterator+n yields a new iterator referring to the iterator.index+n:th element of the array, and analogous for
operator-.
• iterator1-iterator2 yields a long int which is the difference between the indices of the iterators. If iterator1
and iterator2 refer to different arrays, it's an error and we throw an exception.
• iterator1==iterator2 returns non-zero if the arrays and indices of iterator1 and iterator2 are equal.
• iterator1!=iterator2 returns !(iterator1==iterator2)
• *iterator returns whatever (*array)[index] returns, i.e a
• leArrayProxy. * iterator[n] returns (*array)[index+n].
• iterator1<iterator2 returns true if the iterators refer to the same array and iterator1.index < iterator2.index. If
the iterators refer to different arrays, it's an error and we throw an exception. Likewise for operator>.
• iterator1>=iterator2 returns !(iterator1<iterator2). Likewise for operator<=.
I think the above is an exhaustive list. Neither of the above is difficult. It's just a lot of code to write, and thus a good
chance of making errors. With a little thought, however, quite a lot of code can be reused over and over, thus
reducing the amount to write and also the risk for errors. As an example, a rule of thumb when writing a class for
which an object ``o'' and some other value ``v'' the operations ``o+=v'', ``o+v'' and ``v+o'' are well defined and
behaves like they do for the built in types (which they really ought to, unless you want to give the class users some
rather unhealthy surprises) is to define ``operator+='' as a member of the class, and two versions of operator+ that
are implemented with ``operator+=''. Here's how it's done in the iterator example:
Recap
This month the news in short was:
• You can increase flexibility for your templates without sacrificing ease of use or safety by using traits
classes.
• Enumerations in classes can be used to have class-scope constants of integral type.
• Modern compilers do not need the above hack. Defining a class-scope static constant of an integral type in
the class declaration is cleaner and more type safe.
• Standard C++ and even C, does not have any support for the notion of temporary files. Fortunately there are
commonly supported extensions to the languages that do.
• Private inheritance can be used for code reuse.
• Private inheritance is very different from public inheritance. Public inheritance models ``is-A''
relationships, while private inheritance models ``is-implemented-in-terms-of'' relationships.
• A user of a class that has privately inherited from something else cannot take advantage of this fact. To a
user the private inheritance doesn't make any difference.
• Private inheritance is in real-life used far less than it should be. In many situations where public inheritance
is used, private inheritance should've been used.
• Exception catching is polymorphic (i.e. dynamic binding works when catching.)
• The polymorphism of exception catching allows us to create an arbitrarily fine-grained error reporting
mechanism while still allowing users who want a coarse error reporting mechanism to use one (they'll just
catch classes near the root of the exception class inheritance tree.)
• Always implement binary operator+, operator-, operator* and operator/ as functions outside the classes, and
always implement them in terms of the operator+=, operator-=, operator*= and operator/= members of the
classes.
Exercises
• Alter the file array such that it's possible to instantiate two (or more) kinds of FileArray<X> in the same
program, where the alternatives store the data in different formats. (hint, the alternatives will all need
different traits class specialisations.)
• What's the difference between using private inheritance of a base class, and using a member variable of that
same class, for reusing code?
• In which situations is it crucial which alternative you choose?
Coming up
Next month we'll have a look at smart pointers. I'm beginning to dry up on topics now, however, so please write and
give me suggestions for future topics to cover.
Part Part Part Part Part Part Part Part Part Part1 Part1 Part1 Part1
1 2 3 4 5 6 7 8 9 0 1 2 3
[Note: the source code for this month is here. Ed.]
In the past two articles, we've seen how a simple smart pointer, called simply ``ptr<T>'' was used to make memory
handling a little bit easier. That is the core purpose of all smart pointers. You can find a few things in common with
them all. They're templates, they relieve you of the burden of remembering to deallocate the memory, their syntax
resembles that of pointers, they aren't pointers, and they're dangerous if you forget that you're dealing with smart
pointers.
While ``ptr<T>'' served its purpose, it's a bit too simplistic to be generally useful. For example there was no way to
rebind an object to another pointer, or to tell it not to delete the memory (that too can be useful at times, as we will
see later in this article.)
This article is devoted to the only smart pointer provided by the standard C++ library; the class template
``auto_ptr<T>''.
Exception safety
In this respect ``auto_ptr<T>'' and ``ptr<T>'' are equal. They both delete whatever they point to in their
destructor. The only thing that ``auto_ptr<T>'' has to offer over ``ptr<T>'', with respect to exception safety, is
that we can tell an ``auto_ptr<T>'' object that it no longer owns a memory area. This can be used for holding
onto something we want to return in normal cases, but deallocate in exceptional situations. Here is a code fragment
showing such a situation:
// simple transfer
auto_ptr<int> p1(new int(1));
// p1 owns the memory area.
auto_ptr<int> p2;
// p2 doesn't own anything.
p2 = p1;
// now p2 owns the memory area, p1 doesn't.
// p1 has become the 0 "pointer"
auto_ptr<int> p3(p2);
// Now it is p3 that owns the memory area, not
// p1 or p2.
I think the above example speaks for itself. An important issue here for those of you who have used early versions of
the ``auto_ptr<T>'' is that older versions did not become 0 ``pointers'' when not owning the memory area, but that is
the behaviour set in the final draft of the C++ standard. Of course, the above program snippet is too simplistic to be
useful. The properties of the ``auto_ptr<T>'' are more useful when working with functions, where it works as
both documentation and implementation of ownership transfer. What do you think about this?
auto_ptr<int> creation();
void termination(auto_ptr<int> rip);
void f()
{
auto_ptr<int> pi=creation();
// It's now clear that we are responsible for
// deletion of the memory area allocated. Even
// if we chose to release ownership from "pi",
// we must take care of deallocation somehow.
termination(pi);
auto_ptr<int> creator();
void termination(auto_ptr<int>);
void f()
{
int* p = creator();
// illegal. An auto_ptr<T> cannot be implicitly
// converted to a pointer.
auto_ptr<T> ap=p;
// illegal. A pointer cannot be implicitly
// converted to an auto_ptr<T>. When creating a
// new auto_ptr<T> object, use the constructor
// syntax auto_ptr<T> ap(p);
ap=p;
// also illegal. If you want to rebind an
// auto_ptr<T> object to point to something else,
// use the "reset" member function; ap.reset(p).
termination(p);
// also illegal. The auto_ptr<T> required by the
// termination function cannot be implicitly
// created from the pointer.
}
It is indeed fortunate that the first and last error above are illegal. Imagine the maintenance headaches you could get
otherwise. What would the first mean? Would the implicit conversion from ``auto_ptr<T>'' to a raw pointer
transfer ownership or not? All implementations I have seen where such implicit conversions are allowed do not
transfer the ownership, which in the situation above means that the memory would be deallocated when the
``auto_ptr<T>'' object returned is destroyed (which it would be immediately after the conversion.) The last is
as bad. What about this situation?
int i;
termination(&i);
Ouch! The function would attempt to delete the local variable. For both the result will be something that in
standardese is called ``undefined behaviour'', but which in normal English best translates to ``a crash now, a crash
later, or generally funny behaviour (possibly followed by a crash later.)'' Well, since it is illegal, we do not have to
worry about it.
The second error ``auto_ptr<int> ap=p;'' is perhaps a bit unfortunate since the intended behaviour is clear.
That it is illegal comes as a natural consequence of banning the third situation ``ap=p'' which is not clear. ``ap''
might be declared somewhere far far away, so that in the code near the assignment it is not obvious if it is an
``auto_ptr<T>'' or a normal pointer.
In this respect ``auto_ptr<T>'' is better than ``ptr<T>'', since ``ptr<T>'' does allow implicit construction,
allowing the last error.
class A {};
class B : public A{};
auto_ptr<A> pa(new B());
auto_ptr<B> pb(new B());
pa=pb;
auto_ptr<A> pa2(pb);
The reverse is (of course) not allowed. For ``ptr<T>'' this is not a problem, since the functionality is only required
if ownership transfer is allowed.
Implementation
The definition of ``auto_ptr<T>'' looks as follows:
auto_ptr<A>&
auto_ptr<A>::operator=(auto_ptr<B>&) throw()
will be generated. If class ``B'' is publicly derived from class ``A'', the generated code will compile just fine,
otherwise we will get an error message from the compiler. This feature can, to the best of my knowledge, not be
worked around. It is an essential addition to the C++ language. Unfortunately even fewer compilers support this than
support the ``explicit'' keyword.
The code
Let us do the member functions one by one, beginning with the constructor. The only thing it needs to do is to
initialize the ``auto_ptr<T>'' object such that it owns the memory area, and if it points to anything at all, it owns
it (by definition.)
Efficiency
The question of efficiency pops up now and then. How much does it cost, performance and memory-wise to use the
``auto_ptr<T>'' instead of ``ptr<T>'' from last month?
If you use ``auto_ptr<T>'' instead of ``ptr<T>'', and use only the functionality that ``ptr<T>'' offers, the price
is nothing at all. The constructor, destructor, ``operator*'' and ``operator->'' holds exactly the same code for
both templates. You pay for what you use only.
Compared to raw pointers and doing your own deletion? I do not know. It will depend a lot on how clever your
compiler is with inlining. Most probably close to none at all. If you have a measurable speed difference in a real-
world application, I would say the difference is that with ``auto_ptr<T>'' you do many more deletions (i.e. you
have mended memory leaks you were not aware of having.)
Recap
The news this month were:
• The standard class template ``auto_ptr<T>'' handles memory deallocation and ownership transfer.
• Automatic memory deallocation and ownership transfer reduces the risk for memory leaks, especially when
exceptions occur.
• Implicit conversions between raw pointers and smart pointers is bad (even if it may seem tempting at first.)
• The ``explicit'' keyword disallows implicit construction of objects.
• The ``explicit'' keyword can be faked.
• Member templates can be used to create member functions at compile time, just like function templates can
be used to create functions at compile time.
• ``inline'' hints to a compiler that you think a function is so small that it is better to directly insert the
function code where required instead of making a function call.
Exercises
• Why is it a bad idea to have arrays (or other collections) of ``auto_ptr<T>''?
• Can smart pointers be dangerous? When? ``auto_ptr<T>'' too?
• What is a better name for this function template?
Coming up
If I missed something, or you want something clarified further or disagree with me, please drop me a line and I'll
address your ideas in future articles.
Next month we'll have a look at a smarter pointer; a reference counted one. I'm beginning to dry up on topics now,
however, so please write and give me suggestions for future topics to cover.
Part Part Part Part Part Part Part Part Part Part1 Part1 Part1 Part1
1 2 3 4 5 6 7 8 9 0 1 2 3
Introduction
Last month's ``auto_ptr<T>'' class template, documents and implements ownership transfer of dynamically
allocated memory. Often, however, we do not want to be bothered with ownership. We want several places of the
code to be able to access the memory, but we also want to be sure that the memory is deallocated when no longer
needed. The general solution to this is called automatic garbage collection (something you can read several theses
on, and also buy a few commercially available libraries for.) The less general solution is reference counting; no
owner, but last one out locks the door. The idea is that a counter is attached to every object allocated. When
allocated it is set to 0. When the first smart pointer attaches to it, the count is incremented to 1. Every smart pointer
attaching to the resource, increments the reference count; and every smart pointer detaching from a resource (the
smart pointer destroyed, or assigned another value) the resource's counter is decremented. If the counter reaches
zero, no one is referring to it anymore, so the resource must be deallocated. The weakness of this compared to
automatic garbage collection is that it does not work with circular data structures (the count never goes below 1.)
After creating a counting pointer ``P1'', the reference count for the value pointed to is set to one.
counting_ptr<int> P2(P1);
When a second counting pointer ``P2'' is created from ``P1'', the object pointed to is not duplicated, but the
reference count is incremented.
counting_ptr<int> P3(P2);
When three counting pointers refer to the same object, the value of the counter is three.
P1.manage(new int(other));
As one of the pointers referring to the first object created is reinitialized to another object, the old reference count is
decremented (there are only two references to it now) and the new one is assigned a reference count of 1.
P2=P1;
When yet one of the pointers move attention from the old object to the new one, the counter for the old one is yet
again decremented, and for the new one it is incremented.
P3=P2;
Now that the last counting pointer referring to the old object moves its attention away from it, the old objects
reference count goes to zero, and the object is deallocated. Now instead the new object has a reference count of 3,
since there are three reference counting pointers referring to it.
Interface outline
The interface of the reference counting smart pointer will, for obvious reasons, share much with the auto pointer.
The differences lie in accessing the raw pointer and giving the pointer a new value. While these aspects could use
the same interface as does the auto pointer, giving the reference counting pointer an identical interface, this would
be very unfortunate since their semantics differ dramatically. Here is a suggested interface.
template <class T>
class counting_ptr
{
public:
explicit counting_ptr(T* t = 0);
counting_ptr(const counting_ptr<T>& t) throw();
template <class Y>
counting_ptr(const counting_ptr<Y>& ty) throw();
~counting_ptr() throw ();
counting_ptr<T>&
operator=(const counting_ptr<T>& t) throw ();
template <class Y>
counting_ptr<T>&
operator=(const counting_ptr<Y>& ty) throw();
T& operator*(void) const throw ();
T* operator->(void) const throw ();
T* peek(void) const throw();
void manage(T* t);
};
Compared to the auto pointer, the only differences are that some member functions do not have an empty exception
specification, and there is no member function corresponding to ``auto_ptr<T>::release()'' (which stops
the managing of the pointer.) The member function ``get'' is here named ``peek''. I think this name better
describes what is happening. To me the word ``get'' associates with a transfer, and there is no transfer occurring.
All we do is to peek inside and see what the internal raw pointer value is. The member function ``reset'' is here
named ``manage'', and the reason is a big difference in semantics.
Accessibility
The solution outlined above is so good it almost works. For compilers that do not support member templates, such
that the assignment and construction from a counting pointer of another type are impossible, it is all we need.
However, if we want the ability to assign a ``counting_ptr<T>'' object from a value of type
``counting_ptr<Y>'' if a ``T*'' can be assigned from a ``Y*'', we must think of something. The problem is that
``counting_ptr<T>'' and ``counting_ptr<Y>'' are two distinct types, so both the member variables are
private and thus inaccessible. The value of the raw pointer member can be accessed through the public ``peek''
member function, but we need a solution for accessing the counter without making it publicly available.
This kind of problem is exactly what ``friend'' declarations are for, but there is a two-fold problem with that. To
begin with, extremely few compilers support template friends; a rather new addition to the language. Second,
member templates open up holes in the type system you can only dream of. For the curious, please read Scott
Meyer's paper on the topic.
One step on the way towards a solution is to see that the management of the counter is independent of the type T, so
we can implement the counter managing code in a separate class. A reference counting class may look like:
class counting_base
{
public:
counting_base(unsigned count = 0);
counting_base(const counting_base& cb) throw();
counting_base&
operator=(const counting_base& cb) throw ();
~counting_base(void) throw();
int release() throw();
int reinit(unsigned count=1);
private:
unsigned* pcount;
};
The idea here is that the this class handles every aspect of the reference counter. We just need to tell it how to
behave, and it reports the results to us.
The default constructor allows us to choose whether we want a reference counter or not. If our reference counting
pointer is initialized with the 0 pointer, there is no need to waste time and memory by allocating a reference counter
for it.
The member functions ``release'' and ``reinit'' return 1 if the old counter is discarded (and hence, we should
delete the object we refer to) or 0 if it was just decremented.
Base implementation
It is fairly easy to implement, and makes life easier later on, even when member templates are not available.
inline counting_base::counting_base(unsigned count)
: pcount(count ? new unsigned(count) : 0)
{
if (count && !pcount) throw bad_alloc();
}
Initialize a counter with the value of the parameter. A count of 0 is represented by a 0 pointer. If you have a modern
compiler, the function body throwing the exception will not be necessary; operator new will throw ``bad_alloc''
in out-of-memory conditions. For older compilers, however, you need to define the ``bad_alloc'' class.
inline counting_base::counting_base(
const counting_base& cb
) throw ()
: pcount(cb.pcount)
{
if (pcount) ++(*pcount);
}
Copying a reference counting object means adding one to the reference counter (since there is now one more object
referring to the counter.) When we have 0 pointers, there is nothing to update. Note that the copy constructor never
involves any allocation or deallocation of dynamic memory. In fact, there is nothing in the copy constructor that can
throw exceptions.
inline counting_base::~counting_base() throw()
{
release();
}
Destroying a reference counting object means decrementing the reference counter and deallocating it if the count
goes to zero (last one out locks the door.) Since this code is needed in the assignment operator, as well as in the
public interface for use by the reference counted smart pointer class template, we implement that work in the
``release'' member function.
inline counting_base&
counting_base::operator=(
const counting_base& cb
) throw()
{
if (pcount != cb.pcount) {
release();
pcount=cb.pcount;
if (pcount) ++(*pcount);
}
return *this;
}
Assignment of reference counting objects means decrementing the reference count for the left hand side object, and
incrementing it for the right hand side object; since there will be one less object referring to the counter from the left
hand side object, and one more referring to the counter from the right hand side object.
inline int counting_base::release() throw ()
{
if (pcount && --(*pcount) == 0) {
delete pcount;
pcount = 0;
return 1;
}
pcount=0;
return 0;
}
If the reference count goes to zero the counter is deallocated. A return value of 1 means deallocation took place
(hinting to the user of this class that it should deallocate whatever object it refers to, since it is the last one referring
to it.) In both cases the pointer to the counter is set to 0 as a precaution. It may be that ``release'' is called just
prior to destruction, and the destructor calls ``release'' to deallocate memory. If the pointer to the counter is not
set to zero it means either referring to just deleted memory, or decrementing the reference count twice.
inline int counting_base::reinit(unsigned count)
{
if (pcount && --(*pcount) == 0) {
*pcount = count;
return 1;
}
pcount = count ? new unsigned(count) : 0;
if (count && !pcount) throw bad_alloc();
return 0;
}
Strictly speaking, ``reinit'' is not needed. Its purpose is to release from the current counter and initialize a new
one with a defined value. It is an optimization of memory handling, however, in that if this object was the last
reference the counter is reinitialized instead of deallocated and then allocated again. If it was not the last object
referring to the counter, a new counter must of course be allocated.
As an exercise, prove to yourself that this reference counting base class does not have any memory handling errors
(i.e. it always deallocates what it allocates, never deallocates the same area twice, never accesses uninitialized or just
deallocated memory, and never dereferences the 0 pointer.)
Accessibility again
As nice and convenient the above helper class is, it really does not solve the accessibility problem. It does make the
implementation a bit easier, though.
The problem remains. class ``counting_ptr<T>'' and ``counting_ptr<Y>'' are different classes and because
of this are not allowed to see each others private sections. The easy way out is to use public inheritance, and say that
every counting pointer is-a counting base. That is a solution that is simple, sweet and dead wrong. There is no is-a
relationship here, but an is-implemented-in-terms-of relationship. Such relationships are implemented through
private member data or private inheritance - almost. This is bordering on abuse, but it works fine. Instead of having
the member functions of the ``counting_base'' class public, they can be declared protected. As such they do not
become part of the public interface, and the is-a relationship is mostly imaginary.
Efficiency
There is no question about it; there is a cost involved in using reference counting pointers. Every time an object is
allocated, a pointer is also allocated, and vice-versa for deallocation. Depending on how efficient your compiler's
memory manager is for small objects this cost may be negligible, or it may be severe. Every time a counting pointer
is assigned, constructed, destroyed and copied, counter manipulation is done, and that costs a few CPU cycles.
Exercises
• What happens if ``~T'' throws an exception?
• What happens if allocation of a new counter fails?
• The ``counting_base'' implementation and use is suboptimal. For example at destruction, the
``release'' member function is called twice, and the first time the counter pointer is set to zero to prevent
decrementing the counter twice. Improve the implementation to never (implicitly) set the pointer to zero
and yet always be safe.
• Does the public derivation from ``counting_base'' open up any holes in the type safety, despite that all
member functions are protected?
Coming up
If I missed something, or you want something clarified further or disagree with me, please drop me a line and I'll
address your ideas in future articles.
Next month is devoted to Run Time Type Identification (RTTI,) one of the new functionalities in C++.
Part Part Part Part Part Part Part Part Part Part1 Part1 Part1 Part1
1 2 3 4 5 6 7 8 9 0 1 2 3
Introduction
Lucky number thirteen will conclude the long C++ series. Last topic is Run Time Type Identification, or RTTI for
short. It's a new addition to C++. I do not have access to any compiler under OS/2 that supports RTTI, so the
example programs are written for, and tested with, the egcs compiler (a fast-moving bleeding edge gcc,) under
Linux.
RTTI is a way of finding out about the type of objects at run-time. There are two distinct ways this can be done. One
is finding a type identification for an object, giving a unique identifier for each unique type. Another is a clever cast,
which allows casting of pointers and references only if the type matches.
Towards the end of the article there is also a discussion about various aspects of efficiency in C++, especially in
comparison with C.
Identifying types
Much more interesting is using explicit information about the type of an object. This solves a problem that cannot
reliably be worked around with clever designs.
There is a standard class called ``type_info'', which purpose is to carry information about types. It is defined in
the header named <typeinfo> (note, no ``.h'') and looks as follows:
class type_info
{
public:
virtual ~ type_info();
bool operator==(const type_info&) const;
bool operator!=(const type_info&) const;
bool before(const type_info&) const;
const char* name() const;
private:
type_info(const type_info&);
type_info& operator=(const type_info&);
};
The ``name'' member function gives you access to a printable form of the name of the type identified by the
``type_info'' object. However, the standard requires very little of the ``name'' member function. Most notably it
is not standardised what the printable form looks like for any given type, even the built-in ones. In fact, it is not even
required that the string is unique for each type. This unfortunately means that you cannot write portable (across
compilers) applications that rely on the names in any way.
The ``before'' member function gives you a sort order for types. Do not try to get a meaning from the sort order,
there may not be one. It does not need to have any meaning, other than that it makes it possible to keep sorted
collections of type information objects.
You get a ``type_info'' object for a value through the built-in operator ``typeid(x)''. Note that it is the run-
time type of ``x'' that is accessed, not the static type. This requires a little bit of care. Say you have this situation:
class parent {};
class child : public parent {};
parent* p=new child();
cout << typeid(p).name() << endl;
cout << typeid(*p).name() << endl;
If we have a compiler whose name of a type as given from ``type_info'' is indeed the name of the type as we
write it in the program, what do you think the above snippet prints?
The answer is ``parent*'' followed by ``child''. This came as a surprise to me, but it makes sense. The run-time
type of ``p'' is ``parent*''. It is what it points to that may differ, and that is exactly what is mirrored by the output.
Is this useful?
Suppose you need to store and retrieve objects from a typeless media, such as a file, or a socket. Storing them is
easy, but when reading, you need to know what type of object to create. If you are designing a complete system
from scratch, you can add some type identifier to all classes, but this is error prone, since it is easy to forget
changing it when creating a new class inheriting from another one, and it does not work at all with existing classes.
Chances are you will be extending existing code, or decide to use third party class libraries.
Here is an outline for how to do this (in a non-portable, and slightly restricted way. Generality and portability
requires more work.)
First we design a class for I/O objects, let us call it ``persistent''. It may look like this:
class persistent
{
protected:
virtual void store(ostream& os) const = 0;
virtual void retrieve(istream& is) = 0;
};
The intention is that all classes we want to store must inherit from ``persistent'' and implement the ``store''
and ``retrieve'' member functions. This is limiting, but not as limiting as it may seem. We can still make use of
third party libraries; we just need to create persistent versions of the classes.
Next we need something that does the work of calling the member functions and creating the objects when read. Let
us call this class ``persistent_store''. It may be defined as follows:
class persistent_store
{
public:
persistent_store(iostream& stream);
template <class T>
void register_class<T>();
void store_object(persistent*);
persistent* retrieve_object();
};
The interface should be reasonably obvious. Obviously only classes inherited from ``persistent'' may be stored
and retrieved. Only classes registered with the store may be used. There will, however, be other requirements put on
the type ``T'', but what those will be will depend on our implementation. I have chosen the only additional
constraints to be that they have a default constructor and may be created on the heap with ''operator new''.
The use of template functions that do not have any parameters of the template type is a recent addition to C++ that
few compilers support. The syntax is a bit ugly, but it is not too bad. Here is how to use the persistent store:
class persistent_X : public X, public persistent
{
protected:
virtual void store(ostream& os) const {
os << *this;
};
virtual void retrieve(istream& is) {
is >> *this;
};
};
int main(int argc, char* argv[])
{
fstream file(argv[1]);
persistent_store storage(file);
storage.template register_class<persistent_X>();//*
persistent* pp=storage.retrieve_object();
persistent_X* px=dynamic_cast<persistent_X*>(pp);
storage.store_object(px);
}
The line marked ``//*'' shows the syntax for calling template member functions without parameters of the template
type. Note the ``template'' keyword. It is ugly, but I guess one gets used to it.
Now there are a number of problems that must be solved:
• How to prevent classes not inheriting from ``persistent'' from being registered
• How to allocate an object of the correct type on heap, given a string representation of its type.
• How to store the type information on the stream such that it can be interpreted unambigously.
The first is extremely easy to check for. Here is one way of doing it:
template <class T>
void register_class()
{
persistent* p = (T*)0;
...
};
Any attempt to call ``register_class'' with a type not publicly inheriting from ``persistent'' will result in a
compilation error. Not too bad.
The second problem, deciding the type from a name is easy to understand conceptually, but requires a bit of trickery
to implement.
The solution lies in using a map from strings to a creation function. A map is a data structure acting like an array,
but allowing any kind of indexing type. In this case the indexing type will be a string. Every string in the map
corresponds to a function accepting no parameters and returning a ``persistent*''. That is the easy part. So ...
the implementation of ``register_class'' may look like this:
template <class T>
void register_class()
{
persistent* p=(T*)0; // type check.
persistent* (*creator_func)(void) = ???;
creator_map.insert(make_pair(typeid(T).name(),
func));
};
Here the map type from the C++ standard library is used. The tricky part is to find out what ``???'' is. Perhaps it
comes as no surprise that the solution is yet another template, this time a function template.
template <class T>
persistent* persistent_object_creator(void)
{
return new T;
}
This function template implicitly carries out the type test for us (since ``T*'' can only be returned as
``persistent*'' if ``T'' publicly inherits from ``persistent''. In other words, ??? can be replaced with
``persistent_object_creator<T>'' and the check line can be removed.
Now for how to store and retrieve the type name. My suggestion is simple; store the length of the name followed by
the name itself. That way, when reading, the length can be read, a character buffer allocated for the correct size and
exactly that many characters read. When storing the string is checked for in the map to make sure no unregistered
types are stored. When reading, the creator function is looked up and called. Here is how it is all implemented:
void store_object(persistent* p)
{
const char* name=typeid(*p).name();
maptype::iterator iter=creator_map.find(name);
if (iter == creator_map.end())
throw "unregistered type";
medium << strlen(name) << ' ' << name;
p->store(medium);
};
persistent* retrieve_object(void)
{
size_t len;
medium >> len;
medium.get(); // read past blank.
char* name=new char[len+1];
medium.read(name,len);
maptype::iterator iter=creator_map.find(name);
delete[] name; // no longer needed
if (iter == creator_map.end())
throw "unregistered type";
persistent* p=(iter->second)();
p->retrieve(medium);
return p;
};
As can be guessed, the line ``if (iter == creator_map.end())'' checks if the lookup was successful. If
true the string was not represented in the map, and thus the type was not registered.
That is a mini persistency library for you. Enjoy.
Recommended reading
I want to finish by recommending some books on C++.
``Effective C++, 2nd ed'', Scott Meyers. A must read for any C++ programmer. Contains 50 tips, with examples and
discussions, for how to improve your programs. Meyers' writing style is easy to follow and entertaining too.
``More effective C++'', Scott Meyers, Another 35 tips, this time introducing cleverer optimisation techniques like
copy-on-write.
``Advanced C++, Programming Styles and Idioms'', James O. Coplien. Without doubt this book from 1992 is getting
old, but it contains many useful tecniques. It is a mind opener in many ways.
``Scientific and Engineering C++'', John J. Barton and Lee R. Nackman. Often simply referred to as B&N. This is a
modern ``Advanced C++''. What they have done is to break almost every rule of thumb, and as a result they have
extremely extensible, slick, type-safe and fast designs. Beware, however, this book is not for beginners.
``The Design and Evolution of C++'', Bjarne Stroustrup. This book answers all the why's. Why does C++ have
multiple inheritance? Why does it provide assignment, destruction, copy-constructor and default constructor for
you? Why is RTTI there? Why no garbage collection? Why not dynamic types like in Smalltalk?
``Ruminations on C++'', Andrew Koening and Barbara Moo. Now, this is a pleasant book to read. Koenig is the only
author I know who can explain a problem to solve, which one immediately realizes will require a large program,
present an easy to understand one page solution just to say it was unnecessarily clumsy and reduce it to half a page
without sacrificing extensibility and generality. Entertaining and enlightening.
Also, do follow comp.lang.c++.moderated.