|














Am avut 371679 vizite de la lansarea siteului.

|
|
Why iterators?
Up until now you’ve seen the
mechanics of iterators, but understanding why they are so important takes a more
complex example.
It’s
common to see polymorphism,
dynamic object creation, and
containers used together in a true object-oriented program. Containers and
dynamic object creation solve the problem of not knowing how many or what type
of objects you’ll need. And if the container is configured to hold
pointers to base-class objects, an upcast occurs every
time you put a derived-class pointer into the container (with the associated
code organization and extensibility benefits). As the final code in Volume 1 of
this book, this example will also pull together various aspects of everything
you’ve learned so far – if you can follow this example, then
you’re ready for Volume 2.
Suppose you are creating a program that
allows the user to edit and produce different kinds of drawings. Each drawing is
an object that contains a collection of Shape objects:
//: C16:Shape.h
#ifndef SHAPE_H
#define SHAPE_H
#include <iostream>
#include <string>
class Shape {
public:
virtual void draw() = 0;
virtual void erase() = 0;
virtual ~Shape() {}
};
class Circle : public Shape {
public:
Circle() {}
~Circle() { std::cout << "Circle::~Circle\n"; }
void draw() { std::cout << "Circle::draw\n";}
void erase() { std::cout << "Circle::erase\n";}
};
class Square : public Shape {
public:
Square() {}
~Square() { std::cout << "Square::~Square\n"; }
void draw() { std::cout << "Square::draw\n";}
void erase() { std::cout << "Square::erase\n";}
};
class Line : public Shape {
public:
Line() {}
~Line() { std::cout << "Line::~Line\n"; }
void draw() { std::cout << "Line::draw\n";}
void erase() { std::cout << "Line::erase\n";}
};
#endif // SHAPE_H ///:~
This uses the classic structure of
virtual functions in the base class that are overridden in the derived class.
Notice that the Shape class includes a virtual
destructor, something you should
automatically add to any class with virtual functions. If a container
holds pointers or references to Shape objects, then when the
virtual destructors are called for those objects everything will be
properly cleaned up.
Each different type of drawing in the
following example makes use of a different kind of templatized container class:
the PStash and Stack that have been defined in this chapter, and
the vector class from the Standard C++ Library.
The “use”’ of the containers is extremely simple, and in
general inheritance might not be
the best approach (composition could make more sense), but in this case
inheritance is a simple approach and it doesn’t detract from the point
made in the example.
//: C16:Drawing.cpp
#include <vector> // Uses Standard vector too!
#include "TPStash2.h"
#include "TStack2.h"
#include "Shape.h"
using namespace std;
// A Drawing is primarily a container of Shapes:
class Drawing : public PStash<Shape> {
public:
~Drawing() { cout << "~Drawing" << endl; }
};
// A Plan is a different container of Shapes:
class Plan : public Stack<Shape> {
public:
~Plan() { cout << "~Plan" << endl; }
};
// A Schematic is a different container of Shapes:
class Schematic : public vector<Shape*> {
public:
~Schematic() { cout << "~Schematic" << endl; }
};
// A function template:
template<class Iter>
void drawAll(Iter start, Iter end) {
while(start != end) {
(*start)->draw();
start++;
}
}
int main() {
// Each type of container has
// a different interface:
Drawing d;
d.add(new Circle);
d.add(new Square);
d.add(new Line);
Plan p;
p.push(new Line);
p.push(new Square);
p.push(new Circle);
Schematic s;
s.push_back(new Square);
s.push_back(new Circle);
s.push_back(new Line);
Shape* sarray[] = {
new Circle, new Square, new Line
};
// The iterators and the template function
// allow them to be treated generically:
cout << "Drawing d:" << endl;
drawAll(d.begin(), d.end());
cout << "Plan p:" << endl;
drawAll(p.begin(), p.end());
cout << "Schematic s:" << endl;
drawAll(s.begin(), s.end());
cout << "Array sarray:" << endl;
// Even works with array pointers:
drawAll(sarray,
sarray + sizeof(sarray)/sizeof(*sarray));
cout << "End of main" << endl;
} ///:~
The different types of containers all
hold pointers to Shape and pointers to upcast objects of classes derived
from Shape. However, because of polymorphism, the
proper behavior still occurs when the virtual functions
are called.
Note that sarray, the array
of Shape*, can also be thought of as a
container.
Function templates
In drawAll( ) you see
something new. So far in this chapter, we have been using only
class templates, which
instantiate new classes based on one or more type parameters. However, you can
as easily create function
templates, which create new functions based on type parameters. The reason
you create a function template is the same reason you use for a class template:
You’re trying to create generic code, and you do this by delaying the
specification of one or more types. You just want to say that these type
parameters support certain operations, not exactly what types they
are.
The
function template drawAll( ) can be thought of as an
algorithm (and this is what most of the function templates in the
Standard C++ Library are called). It just says how to do something given
iterators describing a range of elements, as long as these iterators can be
dereferenced, incremented, and compared. These are exactly the kind of iterators
we have been developing in this chapter, and also – not coincidentally
– the kind of iterators that are produced by the containers in the
Standard C++ Library, evidenced by the use of vector in this example.
We’d also like
drawAll( ) to be a generic algorithm,
so that the containers can be any type at all and we don’t have to write a
new version of the algorithm for each different type of container. Here’s
where function templates are essential, because they automatically generate the
specific code for each different type of container. But without the extra
indirection provided by the iterators, this genericness wouldn’t be
possible. That’s why iterators are important; they allow you to write
general-purpose code that involves containers without knowing the underlying
structure of the container. (Notice that, in C++, iterators and generic
algorithms require function templates in order to work.)
You can see the proof of this in
main( ), since drawAll( ) works unchanged with each
different type of container. And even more interesting, drawAll( )
also works with pointers to the beginning and end of the array sarray.
This ability to treat arrays as containers is integral to the design of the
Standard C++ Library, whose algorithms look much like
drawAll( ).
Because container class
templates
are rarely subject to the inheritance and upcasting you see with
“ordinary” classes, you’ll almost never see virtual
functions in container classes. Container class reuse is implemented with
templates, not with
inheritance.
Summary
Container classes are an essential part
of object-oriented programming. They are another way to simplify and hide the
details of a program and to speed the process of program development. In
addition, they provide a great deal of safety and flexibility by replacing the
primitive arrays and relatively crude data structure techniques found in
C.
Because the client programmer needs
containers, it’s essential that they be easy to use. This is where the
template comes in. With templates the syntax for source-code reuse (as
opposed to object-code reuse provided by inheritance and composition) becomes
trivial enough for the novice user. In fact, reusing code with templates is
notably easier than inheritance and composition.
Although you’ve learned about
creating container and iterator classes in this book, in practice it’s
much more expedient to learn the containers and iterators in the Standard C++
Library, since you can expect them to be available with every compiler. As you
will see in Volume 2 of this book (downloadable from www.BruceEckel.com),
the containers and algorithms in the Standard C++ Library will virtually always
fulfill your needs so you don’t have to create new ones
yourself.
The issues involved with container-class
design have been touched upon in this chapter, but you may have gathered that
they can go much further. A complicated container-class library may cover all
sorts of additional issues, including multithreading, persistence and garbage
collection.
Exercises
Solutions to selected exercises
can be found in the electronic document The Thinking in C++ Annotated
Solution Guide, available for a small fee from
www.BruceEckel.com.
- Implement the inheritance
hierarchy in the OShape diagram in this
chapter.
- Modify the
result of Exercise 1 from Chapter 15 to use the Stack and iterator
in TStack2.h instead of an array of Shape pointers. Add
destructors to the class hierarchy so you can see that the Shape objects
are destroyed when the Stack goes out of
scope.
- Modify
TPStash.h so that the increment value used by inflate( ) can
be changed throughout the lifetime of a particular container
object.
- Modify
TPStash.h so that the increment value used by inflate( )
automatically resizes itself to reduce the number of times it needs to be
called. For example, each time it is called it could double the increment value
for use in the next call. Demonstrate this functionality by reporting whenever
an inflate( ) is called, and write test code in
main( ).
- Templatize
the fibonacci( ) function on the type of value that it produces (so
it can produce long, float, etc. instead of just
int).
- Using
the Standard C++ Library vector as an underlying implementation, create a
Set template class that accepts only one of each type of object that you
put into it. Make a nested iterator class that supports the “end
sentinel” concept in this chapter. Write test code for your Set in
main( ), and then substitute the Standard C++ Library set
template to verify that the behavior is
correct.
- Modify
AutoCounter.h so that it can be used as a member object inside any class
whose creation and destruction you want to trace. Add a string member to
hold the name of the class. Test this tool inside a class of your
own.
- Create a
version of OwnerStack.h that uses a Standard C++ Library vector as
its underlying implementation. You may need to look up some of the member
functions of vector in order to do this (or just look at the
<vector> header
file).
- Modify
ValueStack.h so that it dynamically expands as you push( )
more objects and it runs out of space. Change ValueStackTest.cpp to test
the new
functionality.
- Repeat
Exercise 9 but use a Standard C++ Library vector as the internal
implementation of the ValueStack. Notice how much easier this is.
- Modify
ValueStackTest.cpp so that it uses a Standard C++ Library vector
instead of a Stack in main( ). Notice the run-time behavior: Does
the vector automatically create a bunch of default objects when it is
created?
- Modify
TStack2.h so that it uses a Standard C++ Library vector as its
underlying implementation. Make sure that you don’t change the interface,
so that TStack2Test.cpp works
unchanged.
- Repeat
Exercise 12 using a Standard C++ Library stack instead of a vector
(you may need to look up information about the stack, or hunt through the
<stack> header
file).
- Modify
TPStash2.h so that it uses a Standard C++ Library vector as its
underlying implementation. Make sure that you don’t change the interface,
so that TPStash2Test.cpp works
unchanged.
- In
IterIntStack.cpp, modify IntStackIter to give it an “end
sentinel” constructor, and add operator== and operator!=. In
main( ), use an iterator to move through the elements of the
container until you reach the end
sentinel.
- Using
TStack2.h, TPStash2.h, and Shape.h, instantiate
Stack and PStash containers for Shape*, fill them each with
an assortment of upcast Shape pointers, then use iterators to move
through each container and call draw( ) for each
object.
- Templatize
the Int class in TPStash2Test.cpp so that it holds any type of
object (feel free to change the name of the class to something more
appropriate).
- Templatize
the IntArray class in IostreamOperatorOverloading.cpp from Chapter
12, templatizing both the type of object that is contained and the size of the
internal array.
- Turn
ObjContainer in NestedSmartPointer.cpp from Chapter 12 into a
template. Test it with two different
classes.
- Modify
C15:OStack.h and C15:OStackTest.cpp by
templatizing class Stack so that it automatically
multiply inherits from the contained class and from Object. The generated
Stack should accept and produce only pointers of the contained
type.
- Repeat
Exercise 20 using vector instead of
Stack.
- Inherit
a class StringVector from vector<void*> and redefine the
push_back( ) and operator[] member functions to accept and
produce only string* (and perform the proper casting). Now create a
template that will automatically make a container class to do the same thing for
pointers to any type. This technique is often used to reduce code bloat from too
many template
instantiations.
- In
TPStash2.h, add and test an operator- to PStash::iterator,
following the logic of
operator+.
- In
Drawing.cpp, add and test a function template to call
erase( ) member
functions.
- (Advanced)
Modify the Stack class in TStack2.h to allow full granularity of
ownership: Add a flag to each link indicating whether that link owns the object
it points to, and support this information in the push( ) function
and destructor. Add member functions to read and change the ownership for each
link.
- (Advanced)
Modify PointerToMemberOperator.cpp from Chapter 12 so that the
FunctionObject and operator->* are templatized to work with any
return type (for operator->*, you’ll have to use member
templates, described in Volume 2). Add and test support for zero, one and
two arguments in Dog member
functions.
[59]
With the exception, in Java, of the primitive data types. These were made
non-Objects for efficiency.
[60]
The OOPS library, by Keith Gorlen while he was at NIH.
[61]
The C++ Programming Language by Bjarne Stroustrup (1st edition,
Addison-Wesley, 1986).
[62]
The inspiration for templates appears to be ADA generics.
[63]
All methods in both Smalltalk and Python are weakly typed, and so those
languages do not need a template mechanism. In effect, you get templates without
templates.
 |
| |
|