- •Thinking in C++ 2nd edition Volume 2: Standard Libraries & Advanced Topics
- •Preface
- •What’s new in the second edition
- •What’s in Volume 2 of this book
- •How to get Volume 2
- •Prerequisites
- •Learning C++
- •Goals
- •Chapters
- •Exercises
- •Exercise solutions
- •Source code
- •Language standards
- •Language support
- •The book’s CD ROM
- •Seminars, CD Roms & consulting
- •Errors
- •Acknowledgements
- •Library overview
- •1: Strings
- •What’s in a string
- •Creating and initializing C++ strings
- •Initialization limitations
- •Operating on strings
- •Appending, inserting and concatenating strings
- •Replacing string characters
- •Concatenation using non-member overloaded operators
- •Searching in strings
- •Finding in reverse
- •Finding first/last of a set
- •Removing characters from strings
- •Stripping HTML tags
- •Comparing strings
- •Using iterators
- •Iterating in reverse
- •Strings and character traits
- •A string application
- •Summary
- •Exercises
- •2: Iostreams
- •Why iostreams?
- •True wrapping
- •Iostreams to the rescue
- •Sneak preview of operator overloading
- •Inserters and extractors
- •Manipulators
- •Common usage
- •Line-oriented input
- •Overloaded versions of get( )
- •Reading raw bytes
- •Error handling
- •File iostreams
- •Open modes
- •Iostream buffering
- •Seeking in iostreams
- •Creating read/write files
- •User-allocated storage
- •Output strstreams
- •Automatic storage allocation
- •Proving movement
- •A better way
- •Output stream formatting
- •Internal formatting data
- •Format fields
- •Width, fill and precision
- •An exhaustive example
- •Formatting manipulators
- •Manipulators with arguments
- •Creating manipulators
- •Effectors
- •Iostream examples
- •Code generation
- •Maintaining class library source
- •Detecting compiler errors
- •A simple datalogger
- •Generating test data
- •Verifying & viewing the data
- •Counting editor
- •Breaking up big files
- •Summary
- •Exercises
- •3: Templates in depth
- •Nontype template arguments
- •Typedefing a typename
- •Using typename instead of class
- •Function templates
- •A string conversion system
- •A memory allocation system
- •Type induction in function templates
- •Taking the address of a generated function template
- •Local classes in templates
- •Applying a function to an STL sequence
- •Template-templates
- •Member function templates
- •Why virtual member template functions are disallowed
- •Nested template classes
- •Template specializations
- •A practical example
- •Pointer specialization
- •Partial ordering of function templates
- •Design & efficiency
- •Preventing template bloat
- •Explicit instantiation
- •Explicit specification of template functions
- •Controlling template instantiation
- •Template programming idioms
- •Summary
- •Containers and iterators
- •STL reference documentation
- •The Standard Template Library
- •The basic concepts
- •Containers of strings
- •Inheriting from STL containers
- •A plethora of iterators
- •Iterators in reversible containers
- •Iterator categories
- •Input: read-only, one pass
- •Output: write-only, one pass
- •Forward: multiple read/write
- •Bidirectional: operator--
- •Random-access: like a pointer
- •Is this really important?
- •Predefined iterators
- •IO stream iterators
- •Manipulating raw storage
- •Basic sequences: vector, list & deque
- •Basic sequence operations
- •vector
- •Cost of overflowing allocated storage
- •Inserting and erasing elements
- •deque
- •Converting between sequences
- •Cost of overflowing allocated storage
- •Checked random-access
- •list
- •Special list operations
- •list vs. set
- •Swapping all basic sequences
- •Robustness of lists
- •Performance comparison
- •A completely reusable tokenizer
- •stack
- •queue
- •Priority queues
- •Holding bits
- •bitset<n>
- •vector<bool>
- •Associative containers
- •Generators and fillers for associative containers
- •The magic of maps
- •A command-line argument tool
- •Multimaps and duplicate keys
- •Multisets
- •Combining STL containers
- •Creating your own containers
- •Summary
- •Exercises
- •5: STL Algorithms
- •Function objects
- •Classification of function objects
- •Automatic creation of function objects
- •Binders
- •Function pointer adapters
- •SGI extensions
- •A catalog of STL algorithms
- •Support tools for example creation
- •Filling & generating
- •Example
- •Counting
- •Example
- •Manipulating sequences
- •Example
- •Searching & replacing
- •Example
- •Comparing ranges
- •Example
- •Removing elements
- •Example
- •Sorting and operations on sorted ranges
- •Sorting
- •Example
- •Locating elements in sorted ranges
- •Example
- •Merging sorted ranges
- •Example
- •Set operations on sorted ranges
- •Example
- •Heap operations
- •Applying an operation to each element in a range
- •Examples
- •Numeric algorithms
- •Example
- •General utilities
- •Creating your own STL-style algorithms
- •Summary
- •Exercises
- •Perspective
- •Duplicate subobjects
- •Ambiguous upcasting
- •virtual base classes
- •The "most derived" class and virtual base initialization
- •"Tying off" virtual bases with a default constructor
- •Overhead
- •Upcasting
- •Persistence
- •MI-based persistence
- •Improved persistence
- •Avoiding MI
- •Mixin types
- •Repairing an interface
- •Summary
- •Exercises
- •7: Exception handling
- •Error handling in C
- •Throwing an exception
- •Catching an exception
- •The try block
- •Exception handlers
- •Termination vs. resumption
- •The exception specification
- •Better exception specifications?
- •Catching any exception
- •Rethrowing an exception
- •Uncaught exceptions
- •Function-level try blocks
- •Cleaning up
- •Constructors
- •Making everything an object
- •Exception matching
- •Standard exceptions
- •Programming with exceptions
- •When to avoid exceptions
- •Not for asynchronous events
- •Not for ordinary error conditions
- •Not for flow-of-control
- •You’re not forced to use exceptions
- •New exceptions, old code
- •Typical uses of exceptions
- •Always use exception specifications
- •Start with standard exceptions
- •Nest your own exceptions
- •Use exception hierarchies
- •Multiple inheritance
- •Catch by reference, not by value
- •Throw exceptions in constructors
- •Don’t cause exceptions in destructors
- •Avoid naked pointers
- •Overhead
- •Summary
- •Exercises
- •8: Run-time type identification
- •The “Shape” example
- •What is RTTI?
- •Two syntaxes for RTTI
- •Syntax specifics
- •Producing the proper type name
- •Nonpolymorphic types
- •Casting to intermediate levels
- •void pointers
- •Using RTTI with templates
- •References
- •Exceptions
- •Multiple inheritance
- •Sensible uses for RTTI
- •Revisiting the trash recycler
- •Mechanism & overhead of RTTI
- •Creating your own RTTI
- •Explicit cast syntax
- •Summary
- •Exercises
- •9: Building stable systems
- •Shared objects & reference counting
- •Reference-counted class hierarchies
- •Finding memory leaks
- •An extended canonical form
- •Exercises
- •10: Design patterns
- •The pattern concept
- •The singleton
- •Variations on singleton
- •Classifying patterns
- •Features, idioms, patterns
- •Basic complexity hiding
- •Factories: encapsulating object creation
- •Polymorphic factories
- •Abstract factories
- •Virtual constructors
- •Destructor operation
- •Callbacks
- •Observer
- •The “interface” idiom
- •The “inner class” idiom
- •The observer example
- •Multiple dispatching
- •Visitor, a type of multiple dispatching
- •Efficiency
- •Flyweight
- •The composite
- •Evolving a design: the trash recycler
- •Improving the design
- •“Make more objects”
- •A pattern for prototyping creation
- •Trash subclasses
- •Parsing Trash from an external file
- •Recycling with prototyping
- •Abstracting usage
- •Applying double dispatching
- •Implementing the double dispatch
- •Applying the visitor pattern
- •More coupling?
- •RTTI considered harmful?
- •Summary
- •Exercises
- •11: Tools & topics
- •The code extractor
- •Debugging
- •Trace macros
- •Trace file
- •Abstract base class for debugging
- •Tracking new/delete & malloc/free
- •CGI programming in C++
- •Encoding data for CGI
- •The CGI parser
- •Testing the CGI parser
- •Using POST
- •Handling mailing lists
- •Maintaining your list
- •Mailing to your list
- •A general information-extraction CGI program
- •Parsing the data files
- •Summary
- •Exercises
- •General C++
- •My own list of books
- •Depth & dark corners
- •Design Patterns
- •Index
} ///:~
TrashBinSet encapsulates all of the different types of TypedBins, along with the sortIntoBins( ) member function, which is where all the double dispatching takes place. You can see that once the structure is set up, sorting into the various TypedBins is remarkably easy. In addition, the efficiency of two virtual calls and the double dispatch is probably better than any other way you could sort.
Notice the ease of use of this system in main( ), as well as the complete independence of any specific type information within main( ). All other methods that talk only to the Trash baseclass interface will be equally invulnerable to changes in Trash types.
The changes necessary to add a new type are relatively isolated: you inherit the new type of Trash with its addToBin( ) member function, then make a small modification to TypedBin, and finally you add a new type into the vector in TrashBinSet and modify
DDTrashPrototypeInit.cpp.
Applying the visitor pattern
Now consider applying a design pattern with an entirely different goal to the trash-sorting problem. As demonstrated earlier in this chapter, the visitor pattern’s goal is to allow the addition of new polymorphic operations to a frozen inheritance hierarchy.
For this pattern, we are no longer concerned with optimizing the addition of new types of Trash to the system. Indeed, this pattern makes adding a new type of Trash more complicated. It looks like this:
Trash |
|
|
Visitor |
|
accept(Visitor&); |
|
|
visit(Aluminum*); |
|
|
|
|
|
visit(Paper*); |
|
|
|
|
visit(Glass*); |
|
|
|
|
|
|
|
|
|
visit(Cardboard*); |
Aluminum |
|
|
||
|
||||
accept(Visitor& v){ |
|
|
||
v.visit(this); |
|
|
||
|
PriceVisitor |
|||
} |
|
|
|
|
|
|
|
visit(Aluminum*){ |
|
|
|
|
|
|
|
|
|
|
// Aluminum- |
|
|
|
|
|
|
|
|
|
// specific work |
|
|
|
|
} |
|
|
|
|
visit(Paper*) { |
|
|
|
|
// Paper- |
|
|
|
|
// specific work |
|
|
|
|
} |
|
|
|
|
|
Chapter 16: Design Patterns |
497 |
Trash |
|
|
|
|
|
|
|
|
Visitor |
|
|
|
|||||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
accept(Visitor) |
|
|
|
|
|
|
|
|
Visit(Aluminum) |
|
|
|
|||||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Visit(Paper) |
|
|
|
||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|||||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Visit(Glass) |
|
|
|
||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|||||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Paper |
|
|
Glass |
|
|
|
|
|
|
|
|
|
|
|
|
||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
WeightVisitor |
|
|
etc. |
||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Aluminum |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|||||
accept(Visitor v) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
||||||||
|
|
|
PriceVisitor |
|
|
|
|||||||||||||||
|
v.visit(this); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|||||
|
|
|
|
|
|
visit(Aluminum) { |
|
|
|
||||||||||||
} |
|
|
|
|
|
|
|
|
|
|
// Perform Aluminum- |
|
|
|
|||||||
|
|
|
|
|
|
|
|
|
|
|
// specific work |
|
|
|
|||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|||||||||
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visit(Paper) { |
|
|
|
|||||||
|
|
|
|
|
|
|
|
|
|
|
// Perform Paper- |
|
|
|
|||||||
|
|
|
|
|
|
|
|
|
|
|
// specific work |
|
|
|
|||||||
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
Now, if t is a Trash pointer to an Aluminum object, the code:
PriceVisitor pv; t->accept(pv);
causes two polymorphic member function calls: the first one to select Aluminum’s version of accept( ), and the second one within accept( ) when the specific version of visit( ) is called dynamically using the base-class Visitor pointer v.
This configuration means that new functionality can be added to the system in the form of new subclasses of Visitor. The Trash hierarchy doesn’t need to be touched. This is the prime benefit of the visitor pattern: you can add new polymorphic functionality to a class hierarchy without touching that hierarchy (once the accept( ) methods have been installed). Note that the benefit is helpful here but not exactly what we started out to accomplish, so at first blush you might decide that this isn’t the desired solution.
But look at one thing that’s been accomplished: the visitor solution avoids sorting from the master Trash sequence into individual typed sequences. Thus, you can leave everything in the single master sequence and simply pass through that sequence using the appropriate visitor to accomplish the goal. Although this behavior seems to be a side effect of visitor, it does give us what we want (avoiding RTTI).
The double dispatching in the visitor pattern takes care of determining both the type of Trash and the type of Visitor. In the following example, there are two implementations of Visitor:
Chapter 16: Design Patterns |
498 |
PriceVisitor to both determine and sum the price, and WeightVisitor to keep track of the weights.
You can see all of this implemented in the new, improved version of the recycling program. As with DoubleDispatch.cpp, the Trash class has had an extra member function stub (accept( )) inserted in it to allow for this example.
Since there’s nothing concrete in the Visitor base class, it can be created as an interface:
//: C09:Visitor.h
//The base interface for visitors
//and template for visitable Trash types #ifndef VISITOR_H
#define VISITOR_H #include "Trash.h" #include "Aluminum.h" #include "Paper.h" #include "Glass.h" #include "Cardboard.h"
class Visitor { public:
virtual void visit(Aluminum* a) = 0; virtual void visit(Paper* p) = 0; virtual void visit(Glass* g) = 0; virtual void visit(Cardboard* c) = 0;
};
//Template to generate visitable
//trash types by inheriting from originals: template<class TrashType>
class Visitable : public TrashType { protected:
Visitable () : TrashType(0) {} friend class TrashPrototypeInit;
public:
Visitable(double wt) : TrashType(wt) {}
//Remember "this" is pointer to current type: void accept(Visitor& v) { v.visit(this); }
//Override clone() to create this new type: Trash* clone(const Trash::Info& info) {
return new Visitable(info.data());
}
};
#endif // VISITOR_H ///:~
Chapter 16: Design Patterns |
499 |
As before, a different version of the initialization file is necessary:
//: C09:VisitorTrashPrototypeInit.cpp {O} #include "Visitor.h"
std::vector<Trash*> Trash::prototypes;
class TrashPrototypeInit { Visitable<Aluminum> a; Visitable<Paper> p; Visitable<Glass> g; Visitable<Cardboard> c; TrashPrototypeInit() {
Trash::prototypes.push_back(&a); Trash::prototypes.push_back(&p); Trash::prototypes.push_back(&g); Trash::prototypes.push_back(&c);
}
static TrashPrototypeInit singleton;
};
TrashPrototypeInit
TrashPrototypeInit::singleton; ///:~
The rest of the program creates specific Visitor types and sends them through a single list of Trash objects:
//: C09:TrashVisitor.cpp
//{L} VisitorTrashPrototypeInit //{L} fillBin Trash TrashStatics
//The "visitor" pattern #include "Visitor.h" #include "fillBin.h" #include "../purge.h" #include <iostream> #include <fstream>
using namespace std;
ofstream out("TrashVisitor.out");
//Specific group of algorithms packaged
//in each implementation of Visitor: class PriceVisitor : public Visitor {
double alSum; // Aluminum double pSum; // Paper double gSum; // Glass double cSum; // Cardboard
Chapter 16: Design Patterns |
500 |
public:
void visit(Aluminum* al) {
double v = al->weight() * al->value();
out << "value of Aluminum= " << v << endl; alSum += v;
}
void visit(Paper* p) {
double v = p->weight() * p->value(); out <<
"value of Paper= " << v << endl; pSum += v;
}
void visit(Glass* g) {
double v |
= g->weight() * g->value(); |
out << |
|
"value |
of Glass= " << v << endl; |
gSum += v; |
|
} |
|
void visit(Cardboard* c) { |
|
double v |
= c->weight() * c->value(); |
out << |
|
"value |
of Cardboard = " << v << endl; |
cSum += v; |
|
} |
|
void total(ostream& os) { |
|
os << |
|
"Total |
Aluminum: $" << alSum << "\n" << |
"Total |
Paper: $" << pSum << "\n" << |
"Total |
Glass: $" << gSum << "\n" << |
"Total |
Cardboard: $" << cSum << endl; |
} |
|
};
class WeightVisitor : public Visitor { double alSum; // Aluminum
double pSum; // Paper double gSum; // Glass double cSum; // Cardboard
public:
void visit(Aluminum* al) { alSum += al->weight();
out << "weight of Aluminum = " << al->weight() << endl;
}
Chapter 16: Design Patterns |
501 |
void visit(Paper* p) { pSum += p->weight();
out << "weight of Paper = " << p->weight() << endl;
}
void visit(Glass* g) { gSum += g->weight();
out << "weight of Glass = " << g->weight() << endl;
}
void visit(Cardboard* c) { cSum += c->weight();
out << "weight of Cardboard = " << c->weight() << endl;
} |
|
|
void |
total(ostream& os) { |
|
os |
<< "Total weight |
Aluminum:" |
|
<< alSum << endl; |
|
os |
<< "Total weight |
Paper:" |
|
<< pSum << endl; |
|
os |
<< "Total weight |
Glass:" |
|
<< gSum << endl; |
|
os |
<< "Total weight |
Cardboard:" |
|
<< cSum << endl; |
|
} |
|
|
};
int main() { vector<Trash*> bin;
//fillBin() still works, without changes, but
//different objects are prototyped: fillBin("Trash.dat", bin);
//You could even iterate through
//a list of visitors!
PriceVisitor pv; WeightVisitor wv;
vector<Trash*>::iterator it = bin.begin(); while(it != bin.end()) {
(*it)->accept(pv); (*it)->accept(wv); it++;
}
pv.total(out);
wv.total(out);
Chapter 16: Design Patterns |
502 |