- •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
sumValue(bin); purge(bin); // Cleanup
} ///:~
In the factory method Trash::factory( ), the determination of the exact type of object is simple, but you can imagine a more complicated system in which factory( ) uses an elaborate algorithm. The point is that it’s now hidden away in one place, and you know to come to this place to make changes when you add new types.
The creation of new objects is now more general in main( ), and depends on “real” data (albeit created by another generator, driven by random numbers). The generator object is created, telling it the maximum type number and the largest “data” value to produce. Each call to the generator creates an Info object which is passed into Trash::factory( ), which in turn produces some kind of Trash object and returns the pointer that’s added to the vector<Trash*> bin.
The constructor for the Info object is very specific and restrictive in this example. However, you could also imagine a vector of arguments into the Info constructor (or directly into a factory( ) call, for that matter). This requires that the arguments be parsed and checked at runtime, but it does provide the greatest flexibility.
You can see from this code what “vector of change” problem the factory is responsible for solving: if you add new types to the system (the change), the only code that must be modified is within the factory, so the factory isolates the effect of that change.
A pattern for prototyping creation
A problem with the above design is that it still requires a central location where all the types of the objects must be known: inside the factory( ) method. If new types are regularly being added to the system, the factory( ) method must be changed for each new type. When you discover something like this, it is useful to try to go one step further and move all of the activities involving that specific type – including its creation – into the class representing that type. This way, the only thing you need to do to add a new type to the system is to inherit a single class.
To move the information concerning type creation into each specific type of Trash, the “prototype” pattern will be used. The general idea is that you have a master container of objects, one of each type you’re interested in making. The “prototype objects” in this container are used only for making new objects. In this case, we’ll name the object-creation member function clone( ). When you’re ready to make a new object, presumably you have some sort of information that establishes the type of object you want to create. The factory( ) method (it’s not required that you use factory with prototype, but they commingle nicely) moves through the master container comparing your information with whatever appropriate information is in the prototype objects in the master container. When a match is found, factory( ) returns a clone of that object.
In this scheme there is no hard-coded information for creation. Each object knows how to expose appropriate information to allow matching, and how to clone itself. Thus, the factory( ) method doesn’t need to be changed when a new type is added to the system.
Chapter 16: Design Patterns |
476 |
The prototypes will be contained in a static vector<Trash*> called prototypes. This is a private member of the base class Trash. The friend class TrashPrototypeInit is responsible for putting the Trash* prototypes into the prototype list.
You’ll also note that the Info class has changed. It now uses a string to act as type identification information. As you shall see, this will allow us to read object information from a file when creating Trash objects.
//: C09:Trash.h
// Base class for Trash recycling examples #ifndef TRASH_H
#define TRASH_H #include <iostream> #include <exception> #include <vector> #include <string>
class TypedBin; // For a later example class Visitor; // For a later example
class Trash { double _weight;
void operator=(const Trash&); Trash(const Trash&);
public:
Trash(double wt) : _weight(wt) {} virtual double value() const = 0;
double weight() const { return _weight; } virtual ~Trash() {}
class Info { std::string _id; double _data;
public:
Info(std::string ident, double dat) : _id(ident), _data(dat) {}
double data() const { return _data; } std::string id() const { return _id; } friend std::ostream& operator<<(
std::ostream& os, const Info& info) { return os << info._id << ':' << info._data;
}
};
protected:
//Remainder of class provides support for
//prototyping:
static std::vector<Trash*> prototypes;
Chapter 16: Design Patterns |
477 |
friend class TrashPrototypeInit; Trash() : _weight(0) {}
public:
static Trash* factory(const Info& info); virtual std::string id() = 0; // type ident virtual Trash* clone(const Info&) = 0;
// Stubs, inserted for later use: virtual bool addToBin(std::vector<TypedBin*>&) {
return false;
}
virtual void accept(Visitor&) {};
};
#endif // TRASH_H ///:~
The basic part of the Trash class remains as before. The rest of the class supports the prototyping pattern. The id( ) member function returns a string that can be compared with the id( ) of an Info object to determine whether this is the prototype that should be cloned (of course, the evaluation can be much more sophisticated than that if you need it). Both id( ) and clone( ) are pure virtual functions so they must be overridden in derived classes.
The last two member functions, addToBin( ) and accept( ), are “stubs” which will be used in later versions of the trash sorting problem. It’s necessary to have these virtual functions in the base class, but in the early examples there’s no need for them, so they are not pure virtuals so as not to intrude.
The factory( ) member function has the same declaration, but the definition is what handles the prototyping. Here is the implementation file:
//: C09:Trash.cpp {O} #include "Trash.h" using namespace std;
Trash* Trash::factory(const Info& info) { vector<Trash*>::iterator it;
for(it = prototypes.begin();
it != prototypes.end(); it++) {
//Somehow determine the new type
//to create, and clone one:
if (info.id() == (*it)->id()) return (*it)->clone(info);
}
cerr << "Prototype not found for "
<<info << endl;
//"Default" to first one in the vector: return (*prototypes.begin())->clone(info);
}///:~
Chapter 16: Design Patterns |
478 |
The string inside the Info object contains the type name of the Trash to be created; this string is compared to the id( ) values of the objects in prototypes. If there’s a match, then that’s the object to create.
Of course, the appropriate prototype object might not be in the prototypes list. In this case, the return in the inner loop is never executed and you’ll drop out at the end, where a default value is created. It might be more appropriate to throw an exception here.
As you can see from the code, there’s nothing that knows about specific types of Trash. The beauty of this design is that this code doesn’t need to be changed, regardless of the different situations it will be used in.
Trash subclasses
To fit into the prototyping scheme, each new subclass of Trash must follow some rules. First, it must create a protected default constructor, so that no one but TrashPrototypeInit may use it. TrashPrototypeInit is a singleton, creating one and only one prototype object for each subtype. This guarantees that the Trash subtype will be properly represented in the prototypes container.
After defining the “ordinary” member functions and data that the Trash object will actually use, the class must also override the id( ) member (which in this case returns a string for comparison) and the clone( ) function, which must know how to pull the appropriate information out of the Info object in order to create the object correctly.
Here are the different types of Trash, each in their own file.
//: C09:Aluminum.h
// The Aluminum class with prototyping #ifndef ALUMINUM_H
#define ALUMINUM_H #include "Trash.h"
class Aluminum : public Trash { static double val;
protected: Aluminum() {}
friend class TrashPrototypeInit; public:
Aluminum(double wt) : Trash(wt) {} double value() const { return val; } static void value(double newVal) {
val = newVal;
}
std::string id() { return "Aluminum"; } Trash* clone(const Info& info) {
return new Aluminum(info.data());
}
Chapter 16: Design Patterns |
479 |
};
#endif // ALUMINUM_H ///:~
//: C09:Paper.h
// The Paper class with prototyping #ifndef PAPER_H
#define PAPER_H #include "Trash.h"
class Paper : public Trash { static double val;
protected: Paper() {}
friend class TrashPrototypeInit; public:
Paper(double wt) : Trash(wt) {} double value() const { return val; } static void value(double newVal) {
val = newVal;
}
std::string id() { return "Paper"; } Trash* clone(const Info& info) {
return new Paper(info.data());
}
};
#endif // PAPER_H ///:~
//: C09:Glass.h
// The Glass class with prototyping #ifndef GLASS_H
#define GLASS_H #include "Trash.h"
class Glass : public Trash { static double val;
protected: Glass() {}
friend class TrashPrototypeInit; public:
Glass(double wt) : Trash(wt) {} double value() const { return val; } static void value(double newVal) {
val = newVal;
}
std::string id() { return "Glass"; }
Chapter 16: Design Patterns |
480 |
Trash* clone(const Info& info) { return new Glass(info.data());
}
};
#endif // GLASS_H ///:~
And here’s a new type of Trash:
//: C09:Cardboard.h
// The Cardboard class with prototyping #ifndef CARDBOARD_H
#define CARDBOARD_H #include "Trash.h"
class Cardboard : public Trash { static double val;
protected: Cardboard() {}
friend class TrashPrototypeInit; public:
Cardboard(double wt) : Trash(wt) {} double value() const { return val; } static void value(double newVal) {
val = newVal;
}
std::string id() { return "Cardboard"; } Trash* clone(const Info& info) {
return new Cardboard(info.data());
}
};
#endif // CARDBOARD_H ///:~
The static val data members must be defined and initialized in a separate code file:
//: C09:TrashStatics.cpp {O}
//Contains the static definitions for
//the Trash type's "val" data members #include "Trash.h"
#include "Aluminum.h" #include "Paper.h" #include "Glass.h" #include "Cardboard.h"
double Aluminum::val = 1.67; double Paper::val = 0.10; double Glass::val = 0.23;
Chapter 16: Design Patterns |
481 |
double Cardboard::val = 0.14;
///:~
There’s one other issue: initialization of the static data members. TrashPrototypeInit must create the prototype objects and add them to the static Trash::prototypes vector. So it’s very important that you control the order of initialization of the static objects, so the prototypes vector is created before any of the prototype objects, which depend on the prior existence of prototypes. The most straightforward way to do this is to put all the definitions in a single file, in the order in which you want them initialized.
TrashPrototypeInit must be defined separately because it inserts the actual prototypes into the vector, and throughout the chapter we’ll be inheriting new types of Trash from the existing types. By making this one class in a separate file, a different version can be created and linked in for the new situations, leaving the rest of the code in the system alone.
//: C09:TrashPrototypeInit.cpp {O}
//Performs initialization of all the prototypes.
//Create a different version of this file to
//make different kinds of Trash.
#include "Trash.h" #include "Aluminum.h" #include "Paper.h" #include "Glass.h" #include "Cardboard.h"
// Allocate the static member object: std::vector<Trash*> Trash::prototypes;
class TrashPrototypeInit { Aluminum a;
Paper p; Glass g; 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; ///:~
Chapter 16: Design Patterns |
482 |