- •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
This is taken a step further by making TrashPrototypeInit a singleton (the constructor is private), even though the class definition is not available in a header file so it would seem safe enough to assume that no one could accidentally make a second instance.
Unfortunately, this is one more separate piece of code you must maintain whenever you add a new type to the system. However, it’s not too bad since the linker should give you an error message if you forget (since prototypes is defined in this file as well). The really difficult problems come when you don’t get any warnings or errors if you do something wrong.
Parsing Trash from an external file
The information about Trash objects will be read from an outside file. The file has all of the necessary information about each piece of trash in a single entry in the form Trash:weight. There are multiple entries on a line, separated by commas:
//:! C09:Trash.dat
Glass:54, Paper:22, Paper:11, Glass:17, Aluminum:89, Paper:88, Aluminum:76, Cardboard:96, Aluminum:25, Aluminum:34, Glass:11, Glass:68, Glass:43, Aluminum:27, Cardboard:44, Aluminum:18, Paper:91, Glass:63, Glass:50, Glass:80, Aluminum:81, Cardboard:12, Glass:12, Glass:54, Aluminum:36, Aluminum:93, Glass:93, Paper:80, Glass:36, Glass:12, Glass:60, Paper:66, Aluminum:36, Cardboard:22,
///:~
To parse this, the line is read and the string member function find( ) produces the index of the ‘:’. This is first used with the string member function substr( ) to extract the name of the trash type, and next to get the weight that is turned into a double with the atof( ) function (from <cstdlib>).
The Trash file parser is placed in a separate file since it will be reused throughout this chapter. To facilitate this reuse, the function fillBin( ) which does the work takes as its first argument the name of the file to open and read, and as its second argument a reference to an object of type Fillable. This uses what I’ve named the “interface” idiom at the beginning of the chapter, and the only attribute for this particular interface is that “it can be filled,” via a member function addTrash( ). Here’s the header file for Fillable:
//: C09:Fillable.h
// Any object that can be filled with Trash #ifndef FILLABLE_H
#define FILLABLE_H
class Fillable { public:
virtual void addTrash(Trash* t) = 0;
};
Chapter 16: Design Patterns |
483 |
#endif // FILLABLE_H ///:~
Notice that it follows the interface idiom of having no non-static data members, and all pure virtual member functions.
This way, any class which implements this interface (typically using multiple inheritance) can be filled using fillBin( ). Here’s the header file:
|
//: C09:fillBin.h |
|
|
|
// Open a file and parse its contents into |
|
|
|
// Trash |
objects, placing each into a vector |
|
|
#ifndef FILLBIN_H |
|
|
|
#define FILLBIN_H |
|
|
|
#include |
"Fillablevector.h" |
|
|
#include |
<vector> |
|
|
#include |
<string> |
|
|
void |
|
|
|
fillBin(std::string filename, Fillable& bin); |
|
|
|
// Special case to handle vector: |
|
|
|
inline void fillBin(std::string filename, |
|
|
|
std::vector<Trash*>& bin) { |
|
|
|
Fillablevector fv(bin); |
|
|
|
fillBin(filename, fv); |
|
|
|
} |
|
|
|
#endif // FILLBIN_H ///:~ |
|
|
|
|
|
|
The overloaded version will be discussed shortly. First, here is the implementation: |
|
||
|
//: C09:fillBin.cpp {O} |
|
|
|
|
||
|
// Implementation of fillBin() |
|
|
|
#include |
"fillBin.h" |
|
|
#include |
"Fillable.h" |
|
|
#include |
"../C01/trim.h" |
|
|
#include |
"../require.h" |
|
|
#include |
<fstream> |
|
|
#include |
<string> |
|
|
#include |
<cstdlib> |
|
|
using namespace std; |
|
|
|
void fillBin(string filename, Fillable& bin) { |
|
|
|
ifstream in(filename.c_str()); |
|
|
|
assure(in, filename.c_str()); |
|
|
|
string |
s; |
|
|
while(getline(in, s)) { |
|
|
|
int comma = s.find(','); |
|
|
|
|
|
|
Chapter 16: Design Patterns |
484 |
// Parse each line into entries: while(comma != string::npos) {
string e = trim(s.substr(0,comma)); // Parse each entry:
int colon = e.find(':');
string type = e.substr(0, colon); double weight =
atof(e.substr(colon + 1).c_str()); bin.addTrash(
Trash::factory( Trash::Info(type, weight)));
// Move to next part of line: s = s.substr(comma + 1); comma = s.find(',');
}
}
} ///:~
After the file is opened, each line is read and parsed into entries by looking for the separating comma, then each entry is parsed into its type and weight by looking for the separating colon. Note the convenience of using the trim( ) function from chapter 17 to remove the white space from both ends of a string. Once the type and weight are discovered, an Info object is created from that data and passed to the factory( ). The result of this call is a Trash* which is passed to the addTrash( ) function of the bin (which is the only function, remember, that a Fillable guarantees).
Anything that supports the Fillable interface can be used with fillBin( ). Of course, vector doesn’t implement Fillable, so it won’t work. Since vector is used in most of the examples, it makes sense to add the second overloaded fillBin( ) function that takes a vector, as seen previously in fillBin.h. But how to make a vector<Trash*> adapt to the Fillable interface, which says it must have an addTrash( ) member function? The key is in the word “adapt”; we use the adapter pattern to create a class that has a vector and is also Fillable.
By saying “is also Fillable,” the hint is strong (is-a) to inherit from Fillable. But what about the vector<Trash*>? Should this new class inherit from that? We don’t actually want to be making a new kind of vector, which would force everyone to only use our vector in this situation. Instead, we want someone to be able to have their own vector and say “please fill this.” So the new class should just keep a reference to that vector:
//: C09:Fillablevector.h
// Adapter that makes a vector<Trash*> Fillable #ifndef FILLABLEVECTOR_H
#define FILLABLEVECTOR_H #include "Trash.h" #include "Fillable.h" #include <vector>
Chapter 16: Design Patterns |
485 |
class Fillablevector : public Fillable { std::vector<Trash*>& v;
public: Fillablevector(std::vector<Trash*>& vv)
: v(vv) {}
void addTrash(Trash* t) { v.push_back(t); }
};
#endif // FILLABLEVECTOR_H ///:~
You can see that the only job of this class is to connect Fillable’s addTrash( ) member function to vector’s push_back( ) (that’s the “adapter” motivation). With this class in hand, the overloaded fillBin( ) member function can be used with a vector in fillBin.h:
inline void fillBin(std::string filename, std::vector<Trash*>& bin) { Fillablevector fv(bin); fillBin(filename, fv);
}
Notice that the adapter object fv only exists for the duration of the function call, and it wraps bin in an interface that works with the other fillBin( ) function.
This approach works for any container class that’s used frequently. Alternatively, the container can multiply inherit from Fillable. (You’ll see this later, in DynaTrash.cpp.)
Recycling with prototyping
Now you can see the new version of the recycling solution using the prototyping technique:
//: C09:Recycle3.cpp //{L} TrashPrototypeInit
//{L} fillBin Trash TrashStatics
// Recycling with RTTI and Prototypes #include "Trash.h"
#include "Aluminum.h" #include "Paper.h" #include "Glass.h" #include "fillBin.h" #include "sumValue.h" #include "../purge.h" #include <fstream> #include <vector> using namespace std;
ofstream out("Recycle3.out");
int main() { vector<Trash*> bin;
// Fill up the Trash bin:
Chapter 16: Design Patterns |
486 |
fillBin("Trash.dat", bin); vector<Aluminum*> alBin; vector<Paper*> paperBin; vector<Glass*> glassBin;
vector<Trash*>::iterator it = bin.begin(); while(it != bin.end()) {
// Sort the Trash: Aluminum* ap =
dynamic_cast<Aluminum*>(*it);
Paper* pp = dynamic_cast<Paper*>(*it); Glass* gp = dynamic_cast<Glass*>(*it); if(ap) alBin.push_back(ap);
if(pp) paperBin.push_back(pp); if(gp) glassBin.push_back(gp); it++;
}
sumValue(alBin);
sumValue(paperBin);
sumValue(glassBin);
sumValue(bin);
purge(bin); } ///:~
The process of opening the data file containing Trash descriptions and the parsing of that file have been wrapped into fillBin( ), so now it’s no longer a part of our design focus. You will see that throughout the rest of the chapter, no matter what new classes are added, fillBin( ) will continue to work without change, which indicates a good design.
In terms of object creation, this design does indeed severely localize the changes you need to make to add a new type to the system. However, there’s a significant problem in the use of RTTI that shows up clearly here. The program seems to run fine, and yet it never detects any cardboard, even though there is cardboard in the list of trash data! This happens because of the use of RTTI, which looks for only the types that you tell it to look for. The clue that RTTI is being misused is that every type in the system is being tested, rather than a single type or subset of types. But if you forget to test for your new type, the compiler has nothing to say about it.
As you will see later, there are ways to use polymorphism instead when you’re testing for every type. But if you use RTTI a lot in this fashion, and you add a new type to your system, you can easily forget to make the necessary changes in your program and produce a difficult- to-find bug. So it’s worth trying to eliminate RTTI in this case, not just for aesthetic reasons – it produces more maintainable code.
Chapter 16: Design Patterns |
487 |