- •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
A basic OOP design principle is “Use data members for variation in state, use polymorphism for variation in behavior.” Your first thought might be that the grab( ) member function certainly behaves differently for a vector that holds Paper than for one that holds Glass. But what it does is strictly dependent on the type, and nothing else.
1.TbinList holds a set of Tbin pointers, so that sort( ) can iterate through the Tbins when it’s looking for a match for the Trash object you’ve handed it.
2.sortBin( ) allows you to pass an entire Tbin in, and it moves through the Tbin, picks out each piece of Trash, and sorts it into the appropriate specific Tbin. Notice the genericity of this code: it doesn’t change at all if new types are added. If the bulk of your code doesn’t need changing when a new type is added (or some other change occurs) then you have an easily-extensible system.
3.Now you can see how easy it is to add a new type. Few lines must be changed to support the addition. If it’s really important, you can squeeze out even more by further manipulating the design.
4.One member function call causes the contents of bin to be sorted into the respective specifically-typed bins.
Applying double dispatching
The above design is certainly satisfactory. Adding new types to the system consists of adding or modifying distinct classes without causing code changes to be propagated throughout the system. In addition, RTTI is not as “misused” as it was in Recycle1.cpp. However, it’s possible to go one step further and eliminate RTTI altogether from the operation of sorting the trash into bins.
To accomplish this, you must first take the perspective that all type-dependent activities – such as detecting the type of a piece of trash and putting it into the appropriate bin – should be controlled through polymorphism and dynamic binding.
The previous examples first sorted by type, then acted on sequences of elements that were all of a particular type. But whenever you find yourself picking out particular types, stop and think. The whole idea of polymorphism (dynamically-bound member function calls) is to handle type-specific information for you. So why are you hunting for types?
The multiple-dispatch pattern demonstrated at the beginning of this chapter uses virtual functions to determine all type information, thus eliminating RTTI.
Implementing the double dispatch
In the Trash hierarchy we will now make use of the “stub” virtual function addToBin( ) that was added to the base class Trash but unused up until now. This takes an argument of a
TypedBin
add(Aluminum)
add(Paper)
add(Glass)
Chapter 16: Design Patterns 492 add(Cardboard)
AluminumBin |
|
PaperBin |
|
GlassBin |
|
CardboardBin |
|
|
|
|
|
|
|
add(Aluminum) |
|
add(Paper) |
|
add(Glass) |
|
add(Cardboard) |
|
|
|
|
|
|
|
container of TypedBin. A Trash object uses addToBin( ) with this container to step through and try to add itself to the appropriate bin, and this is where you’ll see the double dispatch.
Trash
addToBin(TypedBin[])
Aluminum |
Paper |
Glass |
|
Cardboard |
addToBin() |
addToBin() |
addToBin() |
|
addToBin() |
|
|
|
|
|
The new hierarchy is TypedBin, and it contains its own member function called add( ) that is also used polymorphically. But here’s an additional twist: add( ) is overloaded to take arguments of the different types of Trash. So an essential part of the double dispatching scheme also involves overloading (or at least having a group of virtual functions to call; overloading happens to be particularly convenient here).
//: C09:TypedBin.h #ifndef TYPEDBIN_H #define TYPEDBIN_H #include "Trash.h"
#include "Aluminum.h" #include "Paper.h" #include "Glass.h" #include "Cardboard.h" #include <vector>
//Template to generate double-dispatching
//trash types by inheriting from originals: template<class TrashType>
class DD : public TrashType { protected:
DD() : TrashType(0) {}
friend class TrashPrototypeInit; public:
DD(double wt) : TrashType(wt) {}
bool addToBin(std::vector<TypedBin*>& tb) { for(int i = 0; i < tb.size(); i++)
if(tb[i]->add(this)) return true;
return false;
}
//Override clone() to create this new type: Trash* clone(const Trash::Info& info) {
Chapter 16: Design Patterns |
493 |
return new DD(info.data());
}
};
//vector<Trash*> that knows how to
//grab the right type
class TypedBin : public std::vector<Trash*> { protected:
bool addIt(Trash* t) { push_back(t);
return true;
}
public:
virtual bool add(DD<Aluminum>*) { return false;
}
virtual bool add(DD<Paper>*) { return false;
}
virtual bool add(DD<Glass>*) { return false;
}
virtual bool add(DD<Cardboard>*) { return false;
}
};
// Template to generate specific TypedBins: template<class TrashType>
class BinOf : public TypedBin { public:
// Only overrides add() for this specific type: bool add(TrashType* t) { return addIt(t); }
};
#endif // TYPEDBIN_H ///:~
In each particular subtype of Aluminum, Paper, Glass, and Cardboard, the addToBin( ) member function is implemented, but it looks like the code is exactly the same in each case. The code in each addToBin( ) calls add( ) for each TypedBin object in the array. But notice the argument: this. The type of this is different for each subclass of Trash, so the code is different. So this is the first part of the double dispatch, because once you’re inside this member function you know you’re Aluminum, or Paper, etc. During the call to add( ), this information is passed via the type of this. The compiler resolves the call to the proper overloaded version of add( ). But since tb[i] produces a pointer to the base type TypedBin,
Chapter 16: Design Patterns |
494 |
this call will end up calling a different member function depending on the type of TypedBin that’s currently selected. That is the second dispatch.
You can see that the overloaded add( ) methods all return false. If the member function is not overloaded in a derived class, it will continue to return false, and the caller (addToBin( ), in this case) will assume that the current Trash object has not been added successfully to a container, and continue searching for the right container.
In each of the subclasses of TypedBin, only one overloaded member function is overridden, according to the type of bin that’s being created. For example, CardboardBin overrides add(DD<Cardboard>). The overridden member function adds the Trash pointer to its container and returns true, while all the rest of the add( ) methods in CardboardBin continue to return false, since they haven’t been overridden. With C++ templates, you don’t have to explicitly write the subclasses or place the addToBin( ) member function in Trash.
To set up for prototyping the new types of trash, there must be a different initializer file:
//: C09:DDTrashPrototypeInit.cpp {O} #include "TypedBin.h"
#include "Aluminum.h" #include "Paper.h" #include "Glass.h" #include "Cardboard.h"
std::vector<Trash*> Trash::prototypes;
class TrashPrototypeInit { DD<Aluminum> a; DD<Paper> p;
DD<Glass> g; DD<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; ///:~
Here’s the rest of the program:
//: C09:DoubleDispatch.cpp //{L} DDTrashPrototypeInit
//{L} fillBin Trash TrashStatics
Chapter 16: Design Patterns |
495 |
//Using multiple dispatching to handle more than
//one unknown type during a member function call #include "TypedBin.h"
#include "fillBin.h" #include "sumValue.h" #include "../purge.h" #include <iostream> #include <fstream> using namespace std;
ofstream out("DoubleDispatch.out");
class TrashBinSet |
: public vector<TypedBin*> { |
public: |
|
TrashBinSet() { |
|
push_back(new |
BinOf<DD<Aluminum> >); |
push_back(new |
BinOf<DD<Paper> >); |
push_back(new |
BinOf<DD<Glass> >); |
push_back(new |
BinOf<DD<Cardboard> >); |
};
void sortIntoBins(vector<Trash*>& bin) { vector<Trash*>::iterator it;
for(it = bin.begin(); it != bin.end(); it++) // Perform the double dispatch: if(!(*it)->addToBin(*this))
cerr << "Couldn't add " << *it << endl;
}
~TrashBinSet() { purge(*this); }
};
int main() { vector<Trash*> bin; TrashBinSet bins;
//fillBin() still works, without changes, but
//different objects are cloned: fillBin("Trash.dat", bin);
//Sort from the master bin into the
//individually-typed bins: bins.sortIntoBins(bin); TrashBinSet::iterator it;
for(it = bins.begin(); it != bins.end(); it++) sumValue(**it);
//... and for the master bin
sumValue(bin);
purge(bin);
Chapter 16: Design Patterns |
496 |