Applied C - Practical Techniqu
Applied C - Practical Techniqu
Applied C - Practical Techniqu
html
[ Team LiB ]
• Table of C ontents
• Examples
"I really like the software engineering advice given here. As the chief engineer/architect for a
large development group, I can say with certainty that the advice given in this book about
how real-world projects must work is right on the mark."
-Steve Vinoski, coauthor of Advanced CORBA Programming with C++, columnist for C/C++
Users Journal and IEEE Internet Computing, and Chief Architect, IONA Technologies
The authors, drawing on their extensive professional experience, teach largely by example. To
illustrate software techniques useful for any application, they develop a toolkit to solve the
complex problem of digital image manipulation. By using a concrete, real-world problem and
describing exact feature, performance, and extensibility requirements, the authors show you
how to leverage existing software components and the tools inherent in C++ to speed
development, promote reuse, and deliver successful software products.
Page 1
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
• Table of C ontents
• Examples
C opyright
The C ++ In-Depth Series
Titles in the Series
Preface
Intended Audience
How to Use This Book
C onventions We Use
Acknowledgments
C hapter 1. Introduction
Section 1.1. Imaging Basics
Section 1.2. Summary
C hapter 2. A Test Application
Section 2.1. Image C lass Design
Section 2.2. Thumbnail C lass
Section 2.3. Implementation
Section 2.4. Summary
C hapter 3. Design Techniques
Section 3.1. Memory Allocation
Section 3.2. Prototyping
Section 3.3. Summary
C hapter 4. Design C onsiderations
Section 4.1. C oding Guidelines
Section 4.2. Reusable C ode
Section 4.3. Designing in Debugging Support
Section 4.4. Summary
C hapter 5. System C onsiderations
Section 5.1. Multithreaded and Multiprocess Designs
Section 5.2. Exception Handling
Section 5.3. C ompile-Time Versus Run-Time Issues
Section 5.4. C oding for Internationalization
Section 5.5. Summary
C hapter 6. Implementation C onsiderations
Page 2
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Page 3
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Copyright
Many of the designations used by manufacturers and sellers to distinguish their products are
claimed as trademarks. Where those designations appear in this book, and Addison-Wesley
was aware of a trademark claim, the designations have been printed with initial capital letters
or in all capitals.
Intel Integrated Performance Primitives and Intel C++ Compiler are trademarks or registered
trademarks of Intel Corporation or its subsidiaries in the United States and other countries.
The authors and publisher have taken care in the preparation of this book, but make no
expressed or implied warranty of any kind and assume no responsibility for errors or omissions.
No liability is assumed for incidental or consequential damages in connection with or arising out
of the use of the information or programs contained herein.
The publisher offers discounts on this book when ordered in quantity for special sales. For
more information, please contact:
International Sales
(317) 581-3793
[email protected]
Romanik, Philip.
Applied C++ : practical techniques for building better software / Philip Romanik,
Amy Muntz.
p. cm.
Includes biographical references and index.
ISBN 0-321-10894-9 (alk. paper)
1. C++ (Computer program language) 2. Computer softwareDevelopment.
I. Muntz, Amy. II. Title.
QA76.73.C153R66 2003
005.13'321
2003040449
All rights reserved. No part of this publication may be reproduced, stored in a retrieval system,
or transmitted, in any form, or by any means, electronic, mechanical, photocopying, recording,
or otherwise, without the prior consent of the publisher. Printed in the United States of
America. Published simultaneously in Canada.
For information on obtaining permission for use of material from this work, please submit a
written request to:
Page 4
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
1 2 3 4 5 6 7 8 9 10CRS0706050403
Dedication
For Karen, my true love and best friend.
PR
For Gary, who makes all my dreams possible; and for Ethan, whose smile and laughter
brighten each day.
AM
[ Team LiB ]
Page 5
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
"I have made this letter longer than usual, because I lack the time to make it short."
BLAISE PASC AL
The advent of the ISO/ANSI C++ standard marked the beginning of a new era for C++
programmers. The standard offers many new facilities and opportunities, but how can a
real-world programmer find the time to discover the key nuggets of wisdom within this mass of
information? The C++ In-Depth Series minimizes learning time and confusion by giving
programmers concise, focused guides to specific topics.
Each book in this series presents a single topic, at a technical level appropriate to that topic.
The Series' practical approach is designed to lift professionals to their next level of
programming skills. Written by experts in the field, these short, in-depth monographs can be
read and referenced without the distraction of unrelated material. The books are
cross-referenced within the Series, and also reference The C++ Programming Language by
Bjarne Stroustrup.
As you develop your skills in C++, it becomes increasingly important to separate essential
information from hype and glitz, and to find the in-depth content you need in order to grow.
The C++ In-Depth Series provides the tools, concepts, techniques, and new approaches to
C++ that will give you a critical edge.
[ Team LiB ]
Page 6
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Applied C++: Practical Techniques for Building Better Software, Philip Romanik and Amy Muntz
The Boost Graph Library: User Guide and Reference Manual, Jeremy G. Siek, Lie-Quan Lee,
and Andrew Lumsdaine
C++ In-Depth Box Set, Bjarne Stroustrup, Andrei Alexandrescu, Andrew Koenig, Barbara E.
Moo, Stanley B. Lippman, and Herb Sutter
C++ Network Programming, Volume 1: Mastering Complexity Using ACE and Patterns, Douglas
C. Schmidt and Stephen D. Huston
C++ Network Programming, Volume 2: Systematic Reuse with ACE and Frameworks, Douglas
C. Schmidt and Stephen D. Huston
Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions, Herb Sutter
Modern C++ Design: Generic Programming and Design Patterns Applied, Andrei Alexandrescu
More Exceptional C++: 40 New Engineering Puzzles, Programming Problems, and Solutions,
Herb Sutter
[ Team LiB ]
Page 7
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Preface
This book is about applying C++ to solve the problems inherent in building commercial
software. Those of you who have worked on engineering teams building complex software will
know exactly what we mean by calling it commercial software.
Commercial software is delivered to customers (internal or external) who will rely on the
interface you provide. It may be in an embedded system, or it may be a software library or
application for standard platforms. No matter where it ultimately runs, the software must be
released at a particular time with all of the features it needs to be successful in the market. It
is software that is built by one group of engineers and potentially extended and maintained by
other engineers. The engineers who take over maintaining the software may not have been
part of the original team, and they might have to add features or try to fix problems while
visiting customer sites.
Getting a group of engineers to build a complex piece of software and deliver it on time with
full functionality is one of software engineering's biggest challenges. An even bigger challenge
is building that same software in such a way that it can be handed off to others to extend
and maintain. The C++ techniques and practical tips we have compiled into this book have
been used repeatedly to accomplish just this. In many cases, we draw a distinction between
the ideal solution and the practical one. We try to provide discussions of the trade-offs so
that you can make informed decisions, and we tell you what our criteria are when selecting
one method over another. We leave it to you to determine what works best in your
application. Our goal is to share practical techniques that we have found made our commercial
software efforts much more successful than they otherwise would have been. We hope you
will find them useful.
For those of you who prefer to learn by looking at the code, you will find plenty of examples.
We illustrate all of the techniques by using a concrete example that runs throughout the book.
Because it was our experiences with imaging software that prompted us to write this book,
our example comes from the image processing domain, but the C++ techniques are applicable
to any domain.
We start with a simple, although inadequate, application that generates thumbnail images. We
use this application in our prototyping phases to experiment with different C++ design and
implementation techniques. The application is simple to understand and the results of applying
various C++ techniques are immediately obvious, making it a nice candidate for prototyping.
This simple thumbnail image generator has many of the same inherent problems that our final
image framework will have to address. The application is:
Memory intensive. Working with images requires efficient use of memory, because
images can get quite large and unwieldy. Managing memory becomes critical to the
overall performance of the application.
Upon completion, you will have an image processing framework for manipulating your digital
images and a practical toolkit of C++ utilities. The framework will provide efficient image
storage and memory usage, routines for manipulating your digital images (like edge sharpening,
image resizing, noise reduction, edge detection, image subtraction, and more), interfaces to
third-party software, and many performance optimizations. It will be a useful piece of
software that has practical design and implementation features, so that you could even use it
Page 8
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
The complete source code for the thumbnail generator application, the prototypes, and the
final image framework can be found on the included CD-ROM. Any updates to the software
can be found at the web site: http://www.appliedcpp.com.
[ Team LiB ]
Page 9
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Intended Audience
We expect you to be familiar with C++ so that when we apply various constructs from the
language, you have seen or used them before. We also assume that you have built
applications either for personal or commercial use and are familiar with what the Standard
Template Library (STL) can provide. We hope to engage you in detailed discussions of the
advantages and disadvantages of certain C++ constructs. Finally, we hope you really like to
look at actual code examples, because the book is filled with them.
We do not attempt to provide a reference for the C++ language, but we do provide primers
and reviews of those topics that have exacting syntax or are not used as frequently. For the
basics of the C++ language, we refer you to The C++ Programming Language, Special Edition
[Stroustrup00] For in-depth discussions on certain C++ constructs, such as reference
counting, we refer you to Effective C++, Second Edition [Meyers98]. For information on the
Standard Template Library, we refer you to Effective STL [Meyers01]. For information on using
C++ templates, we refer you to C++ Templates [Vandevoorde03].
As for our chosen domain of digital imaging, we don't expect you to have any experience with
writing software that manipulates images. We provide some basic information about imaging
that you can review; if you are familiar with imaging, you can skip that section. Whenever we
talk about a particular operation that we apply to an image, we take the time to give a simple
explanation, as well as some before and after pictures, before proceeding to the code
example. If you want an in-depth, mathematical discussion of image processing operations, we
refer you to Digital Image Processing [Pratt01].
[ Team LiB ]
Page 10
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Chapter 2, A Test Application, introduces our simple, inadequate application, used as a test
bed for prototyping C++ techniques. We deliberately create this strikingly simple application
because it effectively demonstrates the trade-offs of various design and implementation
decisions.
Chapter 3, Design Techniques, begins our discussion of C++ design. We use lots of code
examples to demonstrate design strategies, and we provide a primer on templates since they
are used so heavily within the book. Finally, we prototype various aspects of the design and
build general utilities needed to support the design.
Chapter 4, Design Considerations, explores guidelines and additional strategies you may want
to use in your designs. We offer a practical set of coding guidelines, reusability strategies, and
a simple but effective debugging strategy.
Chapter 7, Testing and Performance, provides a reasonable strategy for integrating unit tests
into your software development cycle, including a full unit test framework and a discussion of
how you can extend it to meet your particular needs. We also focus on performance, giving
specific techniques that you can use to immediately improve the runtime performance of your
application.
Chapter 8, Advanced Topics, explores those issues that we felt warranted more detailed
discussion, such as copy on write, caching, explicit keyword usage, const, and pass by
reference. We also include a section on extending the image framework, to serve as a guide
for taking the existing framework and adding your own processing functions. We've highlighted
some routines that work particularly well for enhancing your digital photographs.
Appendix A, Useful Online Resources, provides links to those software tools and resources that
we thought might be helpful.
Appendix B, CD-ROM Information, outlines the contents of the CD-ROM included with this
Page 11
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
book. There is a great deal of code presented in this book. All of the source code for our test
application, prototypes, and image framework is included on the disc. In addition, we provide
all of the unit tests, unit test framework, and makefiles necessary to run the software on a
variety of platforms. We also include some useful third-party software: the freeware
DebugView utility by SysInternals for Microsoft Windows, the evaluation version of the Intel
Integrated Performance Primitives (IPP) library for Microsoft Windows and Linux, the evaluation
version of the Intel C++ Compiler for Microsoft Windows, the source code to build the JPEG file
delegate, and the source code to build the TIFF file delegate.
[ Team LiB ]
Page 12
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Conventions We Use
The conventions we use in this book are as follows:
[ Team LiB ]
Page 13
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Acknowledgments
This book would not have been possible without the tremendous encouragement, support, and
help we received from many people. We thank you all for your time and patience.
We would especially like to thank Donald D. Anderson, Louis F. Iorio, and David Lanznar, all of
whom spent enormous amounts of personal time reviewing and re-reviewing the entire
manuscript. Your insightful comments and technical expertise have made this a better text.
We would especially like to note Don's invaluable contributions and expertise in the area of
run-time issues.
We are also deeply appreciative of the comprehensive and thorough technical reviews and
suggestions provided by Mary Dageforde, Steve Vinoski, and Jan Christiaan van Winkel. Your
careful attention to details and persistent comments pushed us to improve both the code and
the manuscript.
The following people also made technical contributions on various subjects throughout the
text: Neil Jacobson, Benson Margulies, Neil Levine, Thomas Emerson, David Parmenter, Evan
Morton, and John Field. Thank you!
We feel lucky to have worked with such an incredible team at Addison-Wesley, whose
professionalism and dedication to exacting standards encouraged us to undertake and
complete this manuscript: Debbie Lafferty, Peter Gordon, Mike Hendrikson, Bernard Gaffney,
John Fuller, Tyrrell Albaugh, Chanda Leary-Coutu, Melanie Buck, Curt Johnson, and Beth Byers.
[ Team LiB ]
Page 14
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Chapter 1. Introduction
Everyone who uses C++, or any programming language, brings along their biases and
experiences to their software development efforts. In this book, we will apply C++ to solve
problems in our chosen problem space of digital imaging. What better way to apply C++
constructs and see the immediate effects than by manipulating digital images, perhaps from
your own digital camera.
We begin with a simple, inadequate application that generates thumbnail images from digital
pictures. Through prototyping, we test a variety of C++ techniques and design ideas on the
thumbnail application. We then use what we learn by applying the appropriate C++ techniques
to build a robust image framework, as shown in Figure 1.1.
Along the way, we explore such issues as: What's the best way to design this application,
using inheritance or templates? Should we do everything at static initialization time or use a
Singleton object? Does explicit template instantiation give us any syntactic or functional
advantages? Does reference counting using rep objects and handles add to the design? How
do we partition functionality between global functions and objects? What kind of framework
makes sense for handling exceptions? Does template specialization help us? How can I make
my routines run faster? We don't just discuss these issues; we write lots of code to test our
assumptions and to examine the trade-offs in choosing a particular application.
Our background is in developing commercial software, that is, software with the following
characteristics: built by many, expandable, maintainable, understandable, and stable. The C++
design and implementation techniques that we explore are evaluated in terms of all of these
characteristics:
Built by many: Software that is built by a team inherently means that no single
person has firsthand knowledge of all of the code. It also means that good
communication among the team members is critically important to the software's
success. Communication has to start at the beginning; everyone on the team must
share an understanding of what exactly is being built, for whom, and when it is
needed. By understanding the customers and their requirements, you can always go
back and ask yourself, "Is this feature I'm adding really necessary?" Understanding both
the business and product requirements can help you avoid the dreaded feature creep
or increasing scope that often delays products. Communication has to continue
throughout the process: conveying interface designs so that integration among
components runs smoothly; accurately and completely documenting your code; and
conveying new member functions and their purpose to the Software Quality Assurance
(SQA) and Documentation groups.
Page 15
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Expandable: You must be able to add new features quickly and extend existing
features in commercial software. These requests often seem to come when an
influential customer finds that they need just one more thing to solve their problem.
Even worse is when a limitation is discovered only after the product is released.
Whether or not code can be easily expanded to accommodate these requests is often
a function of the design. While it may be as easy as adding a new member function,
significant new features require that the design be extended. Put simply, you have to
think ahead when designing and writing code. Just implementing to a specification, with
no thought to future expansion or maintainability, makes future changes much more
difficult. A further complication is that most software won't allow features to be
deprecated once the product is in use. You can't simply remove a class that was ill
conceived, as it is certain that if you do so, you'll find a whole host of customers using
it. Backward compatibility is often a key design constraint.
Maintainable: No matter how many unit tests are written, automated tests are run, or
functional tests are executed, it is inevitable that bugs will be found after the product
has been released. For commercial software, the goal of all of the rigorous testing and
preparation is to ensure that any post-release bugs have minor consequences to the
product. What becomes crucial is that these bugs be easily and quickly corrected for
the next release. It is also likely that the person who implemented a particular
component won't be the person responsible for fixing the bug, either because enough
time has passed that he has been assigned to a different project, or there are
designated engineers responsible for support and maintenance. Given these
constraints, maintainability must be designed into the software. There has to be an
emphasis on solid design practices and clarity in coding styles, so that the software
can be efficiently maintained without further complicating it.
Stable: Commercial software must be stable; that is, it must be able to run for
extended periods of time without crashing, leaking memory, or having unexplained
anomalies.
Our biases are probably most evident in our approach to building software. We start each
product development cycle knowing that we will be successful. We reduce the risk by getting
things running as soon as possible. We start with the infrastructure and move on to the actual
code. By doing so, we ensure the success of the product in a number of ways:
We establish a code base or mainline and make sure it builds nightly from the very first
day.
We establish a simple unit test framework and make sure it runs every night using the
nightly build system.
We get very simplistic prototypes up and running quickly, so that we can apply
different C++ techniques and methods to achieve the appropriate design and final
implementation.
We never break the main code base; it is always running so that other engineers and
support groups, such as SQA and Documentation, always have a working base.
We never meet as a team for more than thirty minutes. If you can't say what you need
to in that amount of time, you need to rethink the purpose of the meeting or meet with
a smaller, more focused group.
Page 16
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
So what do you get when you finish this book, aside from a better understanding of when to
apply specific C++ techniques and the trade-offs in doing so? You get the complete source
code for both the thumbnail application and the imageframework. The image framework offers
efficient storage and manipulation of images, routines for processing images (such as
sharpening edges, reducing noise, and subtracting images), and interfaces to third-party
image processing and file format libraries. You can use the image framework to create your
own robust application, either by using the framework directly, interfacing to some third-party
image processing libraries, or by expanding the image processing routines we provide to create
your own library.
We also include all of the unit tests and the unit test harness that we designed to run those
tests. And, should you want to internationalize your software to use double-byte languages,
such as Chinese or Japanese, we've included a simple resource manager to help you.
[ Team LiB ]
Page 17
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
An image processing application is any program that takes an image as input, performs some
manipulation of that image, and produces a resulting image that may or may not be different
from the original image.
An image has a specific meaning in image processing. The final image framework will handle
many types of images, such as 8-bit and 32-bit grayscale and color images. However, as an
introduction to image properties, we'll start with 8-bit grayscale images.
You can think of a grayscale image as a picture taken with black and white film. An 8-bit
grayscale image is made up of pixels (picture elements, also referred to as pels) in varying
levels of gray with no color content. A pixel contains the image information for a single point in
the image, and has a discrete value between 0 (black) and 255 (white). Values between 0
and 255 are varying levels of gray.
We specify the image size by its width (x-axis) and height (y-axis) in pixels, with the image
origin (0,0) being in the top left corner. By convention, the image origin is the top left corner
because this makes it easier to map the coordinates of a pixel to a physical memory location
where that pixel is stored. Typically, increasing pixel locations (left to right) is in the same
direction as memory. These properties are illustrated in FIgure 1.2.
While most of us choose to take color pictures with our digital cameras, grayscale images are
important for a number of applications. Industrial applications, such as machine vision, x-rays,
and medical imaging rely on grayscale imaging to provide information. In the case of machine
vision, inspections for dimensional measurements like width, height, and circumference are
often taken from grayscale images. This is because grayscale images can be processed three
times faster than color images, which means greater throughput on production lines. Grayscale
images are also the standard for mass detection and analysis in x-ray technology.
Other sophisticated image processing applications require color images. Color may be required
for accurate analysis, for example in applications where sorting occurs based on color (such
as in pharmaceutical applications that use color to represent different types of pills). In
Page 18
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
addition, color pictures are the most frequently captured images with digital cameras.
As we mentioned, there are many other ways to represent an RGB value, and some are based
on human perception. In these color models, more bits are reserved for the green channel,
with fewer bits for the red and blue channels because our eyes have greater sensitivity to
green than to either red or blue. If your application produces images that are viewed by
people, this is an important issue. For applications employing machine analysis of images, this
is not as important.
Page 19
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
The saturation of the color is determined by how much the color is mixed with gray and white.
A fully saturated color contains no gray or white, only the color. As gray and white are mixed
into the color, it becomes less saturated. For example, the color of a lemon is a fully saturated
yellow, and the color of a banana is a less saturated yellow.
The intensity of a color (also called luminance in some graphics programs) is the brightness of
the color.
While it may be easier to describe colors using the HSI color space, it is not as fast for our
purposes. As with grayscale images, the size of a color image is its width and height in pixels.
Similarly, its x,y origin (0,0) is the top left corner. The properties of color images that we use
in our prototype are shown in Figure 1.4.
[ Team LiB ]
Page 20
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
1.2 Summary
In this chapter, we presented an overview of the commercial software characteristices that
shape the techniques presented in this book. We also introduced the image framework
application that we evolve throughout the book, as a test bed for various C++ constructs and
design techniques. Finally, we concluded with a basic introduction to images and image
processing terminology. We believe that this is all you will need to understand the examples in
the book and to subsequently, if desired, apply image processing techniques to your own
digital images using the framework provided here.
In Chapter 2, we will begin our journey by creating a simple test application that generates
thumbnail images, as a reference for future C++ experimentation.
[ Team LiB ]
Page 21
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
C++ Concepts
Class Design
Thumbnail Images
Our test application is a simple image processing application that accepts full-resolution
grayscale images and produces thumbnails of those images. This is a deliberately simple,
inadequate application that we use as a test bed for the C++ techniques we explore in later
chapters. Our goal is to use prototypes to evolve our design of a robust, commercial-quality
image framework by applying C++ techniques effectively.
A thumbnail image is a much smaller version of an image, which still contains enough image
content to look like the original. Web sites, for example, often make use of thumbnail images
because they are smaller and faster to transmit. By convention, clicking on a thumbnail image
often displays the original, full-resolution image.
With limited details about what this test application should do, the list of design objectives is
small. The application will
Compute a thumbnail image with a user-specified scale factor (such as 1/2, 1/3, or 1/4
of the size of the original).
It is fairly easy to design and implement a C++ class to solve this problem. Our initial design is
very simple, using only these design objectives and good programming practices as a guide.
Later, we will expand the design as more requirements are added.
We use this iterative process to show how a design that initially appears to meet the
objectives may not be the optimum solution. Techniques and tips introduced later in this book
will be used to ensure a solid expandable design that will ultimately meet the goals of a
comprehensive image framework.
[ Team LiB ]
Page 22
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
It allows random read/write access to the pixels in the image. Every pixel in an image
has coordinates (x,y) to describe its location. We specify the image origin (0,0) as the
top left corner of the image, and (width-1, height-1) represents the pixel in the
bottom right corner of the image.
It uses a simple memory allocation scheme for image data. Memory for an image whose
x-dimension is width and y-dimension is height is allocated by:
unsigned char* pixels = new unsigned char [width * height];
The address of any pixel (x,y) in the image is:
It throws a C++ exception, rangeError, if any attempts are made to reference pixels
not within the image.
Our initial design for the image class is this simple. On the surface, it appears to meet the
design objectives. The image class definition is shown in Section 2.3.1 on page 12.
[ Team LiB ]
Page 23
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Computes the thumbnail image given the input file and the reduction factor to use
(i.e., how small a thumbnail image to create).
Throws a C++ exception, invalid, if any errors are encountered. If the image class
throws a rangeError error, this exception is caught and the invalid exception is
thrown instead.
The complete definition of the thumbnail class is shown in Section 2.3.2 on page 16.
A picture helps clarify this equation. Each pixel in the original image P is reduced, by averaging
pixel values in image P, to a corresponding point in the thumbnail image T using the equation
from Figure 2.1. In Figure 2.2, we show how a group of pixels in the original image is reduced
to the group of pixels shown in the thumbnail image.
Page 24
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
To further simplify our application, we ignore the condition created by integer arithmetic
where the division by the reduction factor results in a fraction. For example, if the original
image is 640x480 pixels in size, and the desired reduction factor is 6, the thumbnail image will
be (640/6)x(480/6) pixels in size or 106.67x80. To avoid the fractional result, we ignore the
last 4 pixels in each line, effectively making the thumbnail image (636/6)x(480/6) pixels in
size or 106x80.
[ Team LiB ]
Page 25
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
2.3 Implementation
This section shows the implementation of both the image class and the thumbnail classes. For
brevity in the book, we have omitted many of the comments in the code snippets. We have
chosen instead to explain our design choices in a narrative fashion. The full implementation,
including complete comments, can be found on the CD-ROM.
class apImage
{
public:
apImage ();
apImage (int width, int height);
~apImage ();
private:
void init ();
void cleanup ();
// Initialize or cleanup the allocated image data
Instead of duplicating code every time the image data is allocated or deleted, we define
init() and cleanup() functions.
The init() function uses the new operator to allocate memory for storing image data of size
width x height.
The orthogonal function is cleanup() which deletes any memory allocated to the apImage
class, as shown here.
Page 26
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
void apImage::cleanup ()
{
// Put the object back into its original, null state.
delete [] pixels_;
width_ = 0;
height_ = 0;
pixels_ = 0;
}
void apImage::init ()
{
// All memory allocation passes through this function.
if (width_ > 0 && height_ > 0)
pixels_ = new unsigned char [width_ * height_];
}
Create orthogonal functions whenever possible. For example, if your
class has an open() function, you should also have a close() function.
By using init() and cleanup(), our constructors and destructors become very simple.
The default constructor for apImage creates a null image which is an image with no memory
allocation and zero width and height. Null images are very useful objects, and are necessary
because some image processing operations can legitimately be null. For example, image
processing routines often operate on a particular set of pixels present in multiple images. If
there is no overlap in pixels among the images, then a null image is returned. A null image is
also returned if an error occurs during an image processing operation. The isValid() member
function tests whether an image is null as shown here.
apImage input;
...
apImage output = input.arbitraryAlgorithm (args);
if (!output.isValid()) {
Page 27
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
We define an assignment operator and copy constructor, because the one that the compiler
automatically generates performs a member-wise copy of the class data, which incorrectly
copies the image data pointer pixels_. So, we use the standard C library memcpy() function
to duplicate the image data:
return *this;
}
Use the version of memcpy() or std::copy() supplied on your system.
It will be difficult to write a faster version.
As shown, our copy constructor and assignment operator use much of the same code. One
way to eliminate this duplication is to use Sutter's technique for writing safe assignment
operators. See [Sutter00]. In a nutshell, his technique requires that you define the
assignment operator by calling the copy constructor. In addition, his technique requires that
you define a swap() function, which switches the data of two apImage objects as shown
here.
Page 28
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
return *this;
}
The swap<> template function simply exchanges the data of two objects of the same type. It
allows us to implement a swap() method for the apImage class that exchanges all of the data
in the object with another apImage object. The assignment operator uses the copy
constructor to create a copy of src, which is then exchanged with the existing data members
of the object. When the assignment operator returns, the temporary apImage object is
automatically destroyed. This behavior is especially useful because it guarantees that if any
exceptions are thrown, the object will not be left in a partially constructed state.
Our apImage class is complete once we define the functions that read and write image pixels.
Both will throw rangeError if the coordinates do not specify a pixel within the image, or if the
image is null.
class apThumbNail
{
public:
apThumbNail ();
~apThumbNail ();
// The default copy constructor and assignment operator are ok
Page 29
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
private:
void readImage (const char* inputFile);
void writeImage (const char* outputFile) const;
unsigned char averagePixels (int x0, int y0, int factor);
Let's discuss the implementation of the thumbnail class, starting with the constructor and
destructor.
apThumbNail::apThumbNail ()
{}
apThumbNail::~apThumbNail ()
{}
Although they are both empty, they do serve a purpose. A common mistake during
development is to add a new data member to an object and forget to initialize it in the
constructor. Defining a constructor makes it more likely that you will remember to initialize
new data members.
In addition, you may wonder why these trivial functions are not placed in the header file as an
inline definition. Keeping these definitions in the source file is appropriate, because the
constructor and destructor are apt to be modified frequently during development and
maintenance. We have also found that on some platforms, especially embedded,
cross-compiled platforms, inlined constructors cause the memory footprint of an application to
increase, because the compiler adds additional housekeeping code. This isn't a problem if the
object is referenced only a few times in the code, but, if used frequently, it can dramatically
increase the memory footprint.
Let's skip ahead to the implementation of the readImage() and writeImage() functions.
These functions are designed to read an image from a file and convert it into an apImage
class, or to write an apImage to a file, respectively. There are numerous file formats designed
Page 30
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
to store image data, and we deal with them later. For now, we simulate these functions to
quickly verify that the design is working properly, before getting bogged down in the details of
supporting numerous file formats. readImage() creates an apImage and populates it with
image pixels, and writeImage() simply displays the apImage. readImage() also demonstrates
how we catch the rangeError thrown by apImage and rethrow it as an invalid error.
With that out of the way, we can look at the function that actually does the work.
createThumbNail() takes the name of an input file, the name of the output thumbnail file to
create, and how much reduction is desired. Once the input image is read and the thumbnail
image is allocated, the function loops through each pixel in the thumbnail image and computes
its value:
Page 31
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
image_.height() / factor);
writeImage (outputFile);
}
createThumbNail() calls averagePixels() to compute the average of pixels in the input
image needed to compute a single pixel in the thumbnail image. To be precise, the pixels in a
small factor x factor subset of image_ are summed and averaged as shown:
Page 32
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
To decrease the likelihood of bugs, the coordinates used in averagePixels() are all in terms
of the input image, image_. Likewise, the coordinates used in createThumbNail() are mostly
in terms of the thumbnail image.
The following line creates the thumbnail image, given the dimensions of the input image, and is
written to be self-documenting:
[
Tea
m LiB
]
Page 33
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
2.4 Summary
In this chapter, we created a test application that takes an image and generates a thumbnail
image, which is an image of reduced size. We designed and implemented an image class for
the original input image, and a thumbnail class that computes the thumbnail image. In doing
so, we employed a very simple memory management scheme for manipulating images, which
was useful in highlighting the deficiencies that we may want to correct in the final image
framework. We also used this simple application to explore assignment operators and how to
write safe ones using Sutter's technique. See [Sutter00].
In Chapter 3, we construct a more robust memory management object that can effectively
fulfill our defined requirements for a real image framework. We also review reference counting
and provide a primer on templates, including some of the exacting syntactic requirements. We
use this memory management object as we prototype various aspects of the image
framework, with each prototype employing different C++ constructs and techniques to arrive
at the final design for the image framework. Although we concretely apply these techniques to
an image framework to explore their advantages and disadvantages, these techniques can be
applied to other types of software.
[ Team LiB ]
Page 34
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
C++ Concepts
Singleton Object
Reference Counting
Template Primer
Prototyping Strategy
In this chapter, we lay the groundwork for extending our digital imaging framework. We begin
by designing a memory allocation object, and then continue with a templates primer that
provides a road map to subtleties of templates and their use. Finally, we apply C++ constructs
to specific aspects of the design by creating detailed prototypes, and discussing the
advantages and disadvantages of each technique.
[ Team LiB ]
Page 35
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
You really have to think about the purpose of an image before duplicating it. Duplication of
image data should only happen when there is a good reason to retain a copy of the image (for
example, you want to keep the original image and the filtered image result).
EXAMPLE
A simple example illustrates the inefficiencies that can occur when manipulating
images. Try adding two images together as follows:
apImage a (...);
apImage b (...);
apImage c = a + b;
The code allocates memory to store image a, allocates memory to store image b,
allocates more memory to store the temporary image (a+b), and finally allocates
memory for the resulting image c. This simple example is bogged down with many
memory allocations, and the time required for copying all the pixel data is excessive.
We use this example as a simple way of showing how much memory and time a seemingly
trivial operation can require. Note that some compilers can eliminate the temporary (a+b)
storage allocation by employing a technique called return value optimization [Meyers96].
Here's the list of requirements for our generic memory allocation object:
Allocates memory off the heap, while also allowing custom memory allocators to be
defined for allocating memory from other places, such as private memory heaps.
Uses reference counting to share and automatically delete memory when it is no longer
Page 36
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
needed.
Has very low overhead. For example, no memory initialization is done after allocation.
This is left to the user to do, if needed.
Throws Standard Template Library (STL) exceptions when invalid attempts are made to
access memory.
Aligns the beginning of memory to a specified boundary. The need for this isn't obvious
until you consider that certain image processing algorithms can take advantage of how
the data is arranged in memory. Some compilers, such as Microsoft Visual C++, can
control the alignment when memory is allocated. We include this feature in a
platform-independent way.
Before we move forward with our own memory allocation object, it is wise to see if any
standard solutions exist.
The STL is always a good resource for solutions. You can imagine where std::vector,
std::list, or even std::string could be used to manage memory. Each has its advantages,
but none of these template classes offer reference counting. And even if reference counting
were not an issue, there are performance issues to worry about. Each of these template
objects provides fast random access to our pixel data, but they are also optimized for
insertion and deletion of data, which is something we do not need.
Our solution is to create a generic object that uses reference counting to share and
automatically delete memory when finished. Reference counting allows different objects to
share the same information. Figure 3.3 shows three objects sharing image data from the same
block of memory.
Page 37
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
In Figure 3.3, the object containing the image data, Object Z, also contains the reference
count, 3, which indicates how many objects are sharing the data. Objects A, B, and C all
share the same image data.
Here's how it works in a nutshell. A memory allocation object allocates storage for an image.
When subsequent images need to share that storage, the memory allocation object
increments a variable, called a reference count, to keep track of the images sharing the
storage; then it returns a pointer to the memory allocation object. When one of those images
is deleted, the reference count is decremented. When the reference count decrements to
zero, the memory used for storage is deleted. Let's look at an example using our memory
allocation object, apAlloc<>.
Page 38
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Consider this simple image class that we want to convert to a template class:
class apImageTest
{
public:
apImageTest (int width, int height, char* pixels);
char* getPixel (int x, int y);
void setPixel (int x, int y, char* pixel);
private:
char* pixels_;
int width_, height_;
};
The conversion is as easy as substituting a type name T for references to char as shown:
Any placeholder name can be used to represent the template arguments. We use a single
letter (usually T), but you can use more descriptive names if you want. Consider the following:
The word class is used to define an argument name, but this argument does not have to be a
class. In our apImageTest example shown earlier, we wrote apImageTest<char>, which
expands the first line of the class declaration to:
Page 39
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
apImage<T1>::row_iterator i1 = src1.row_begin();
produces a warning:
apImage<apRGB>::row_iterator i1 = src1.row_begin();
There is a growing movement to use typename instead of class because of the confusion
some new programmers encounter when using templates. If you do not have a clear
preference, we recommend that you use typename. The most important thing is that you are
consistent in your choice.
You can supply default template arguments much like you can with any function argument.
These default arguments can even contain other template arguments, making them extremely
powerful. For example, our apAlloc<> class that we design in Section 3.1.5 on page 31 is
defined as:
Page 40
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
The syntax we used by adding a space between the two '>' characters is significant. Defining
the line as:
INLINE DEFINITION
Many template developers put the implementation inside the class definition to save a lot of
typing. For example, we could have given the getPixel() function in our apImageTest<>
object an inline definition in the header file this way:
NON-INLINE DEFINITION
We can also define getPixel() after the class definition (non-inline) in the header file:
template<class T>
apImageTest<T>::apImageTest (const apImageTest& src)
{...}
It is hard to get the syntax correct on an example like this one. The error messages generated
by compilers in this case are not particularly helpful, so we recommend that you refer to a
C++ reference book. See [Stroustrup00] or [Vandevoorde03].
Template Specialization
Templates define the behavior for all types (type T in our examples). But what happens if the
definition for a generic type is slow and inefficient? Specialization is a method where additional
member function definitions can be defined for specific types. Consider an image class,
apImage<T>. The parameter T can be anything, including some seldom used types like double
or even std::string. But what if 90 percent of the images in your application are of a
specific type, such as unsigned char? An inefficient algorithm is fine for a generic parameter,
but we would certainly like the opportunity to tell the compiler what to do if the type is
Page 41
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
unsigned char.
To define a specialization, you first need the generic definition. It is good to write this first
anyway to flesh out what each member function does. If you choose to write only the
specialization, you should define the generic version to throw an error so that you will know if
you ever call it unintentionally.
Once the generic version is defined, the specialization for unsigned char can be defined as
shown:
template<class T>
apImageTest<T>::apImageTest (int width, int height, T* pixels)
: width_ (width), height_ (height), pixels_ (0)
{
pixels_ = new T [width_ * height_];
T* p = pixels_;
for (int y=0; y<height; y++) {
for (int x=0; x<width; x++)
*p++ = *pixels++; // use assignment to copy pixels
}
}
This definition is careful to use assignment to copy each pixel from the given array to the one
controlled by the class. Now, let us define a specialization when T is an unsigned char:
apImageTest<unsigned char>::apImageTest
(int width, int height, unsigned char* pixels)
: width_ (width), height_ (height), pixels_ (0)
{
pixels_ = new unsigned char [width_ * height_];
memcpy (pixels_, pixels, width_ * height_);
}
We can safely use memcpy() to initialize our pixel data. You can see that the syntax for
specialization is different if you are defining the complete specialization, or just a single
member function.
Function Templates
C++ allows the use of templates to extend beyond objects to also include simple function
definitions. Finally we have the ability to get rid of most macros. For example, we can replace
the macro min():
#ifndef min
#define min(a,b) (((a) < (b)) ? (a) : (b))
Page 42
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
#endif
with a function template min():
Function templates can also have multiple template parameters, but don't be surprised if the
compiler sometimes selects a function you don't expect. Here is an example of a function that
we used in an early iteration of our image framework:
You must be careful with these mixed-type function templates. It is entirely possible that the
compiler will not be able to determine which version to call. We could not use the above
definition of add2<> with recent C++ compilers, such as Microsoft Visual Studio, because our
numerous overrides make it ambiguous, according to the latest C++ standard, as to which
version of add2<> to call. So, our solution is to define non-template versions for all our
expected data types, such as:
C++ allows you to explicitly instantiate one or more template arguments. We can rewrite our
add2() example to return the result, rather than pass it as a reference, as follows:
add2<double>(1.1, 2.2);
add2<int>(1.1, 2.2);
Page 43
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
We will use explicit template instantiation later in the book to specify the data type for
intermediate image processing calculations. For more information on explicit instantiation or
other template issues, see [Vandevoorde03].
Notation Meaning
X is a class
B is a kind of A (inheritance)
It consists of a base class, a derived class, and then the object class, which uses the derived
class as one of its arguments. All three classes use templates. Note that we have appended
an underscore character, _, to some class names to indicate that they are internal classes
used in the API, but never called directly by its clients.
apAllocatorBase_<> is a base class that manages memory and contains all of the required
functionality, except for the actual allocation and deallocation of memory. Its constructor
basically initializes the object variables.
apAlloc<> is a simple interface that the application uses to manage memory. apAlloc<> uses
an apAllocator_<> object as one of its parameters to determine how to manage memory. By
default, apAlloc<> allocates memory off the heap, because this is what our apAllocator_<>
Page 44
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
object does. However, if the application requires a different memory management scheme, a
new derived allocator object can be easily created and passed to apAlloc<>.
apAllocatorBase_<> Class
The apAllocatorBase_<> base class contains the raw pointers and methods to access
memory. It provides both access to the raw storage pointer, and access to the reference
count pointing to shared storage, while also defining a reference counting mechanism.
apAllocatorBase_<> takes a single template parameter that specifies the unit of memory to
be allocated. The full base class definition is shown here.
virtual ~apAllocatorBase_ () {}
// Derived classes will deallocate memory
protected:
virtual void allocate () = 0;
virtual void deallocate () = 0;
// Used to allocate/deallocate memory.
Page 45
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
You'll notice that the constructor in apAllocatorBase_ doesn't do anything other than
initialize the object variables. We would have liked to have had the base class handle
allocation and deallocation too, but doing so would have locked derived classes into heap
allocation. This isn't obvious, until you consider how objects are constructed, as we will see in
the next example.
EXAMPLE
Suppose we designed the base class and included allocate and deallocate
functions as shown:
We found it cleaner to define a base class, apAllocatorBase_<>, and later derive the object
apAllocator_<> from it. The derived object handles the actual allocation and deallocation.
This makes creating custom memory management schemes very simple. apAlloc<> simply
takes an apAllocator_<> object of the appropriate type and uses that memory management
scheme.
C ONVERSION OPERATORS
Page 46
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
It isn't always the right choice to define these conversion operators. We chose to use
operator T* because apAlloc<> is fairly simple and is little more than a wrapper around a
memory pointer. By simple, we mean that there is little confusion if the compiler were to use
operator T* to convert the object reference to a pointer.
EXAMPLE
For more complex objects, we could have used a data() method for accessing
memory, which would look like:
apAllocatorBase_ a (...);
T* p1 = a; // Requires operator T* to be defined.
T* p2 = a.data();
Note that the STL string class also chooses not to define conversion operators,
but rather uses the c_str() and data() methods for directly accessing the
memory. The STL purposely does not include implicit conversion operators to
prevent the misuse of raw string pointers.
REFERENC E C OUNTING
Next we'll look at the functions that manage our reference count.
MEMORY ALIGNMENT
Memory alignment is important because some applications might want more control over the
pointer returned after memory is allocated. Most applications prefer to leave memory
alignment to the compiler, letting it return whatever address it wants. We provide memory
alignment capability in apAllocatorBase_<> so that derived classes can allocate memory on a
specific boundary. On some platforms, this technique can be used to optimize performance.
This is especially useful for imaging applications, because image processing algorithms can be
optimized for particular memory alignments.
When a derived class allocates memory, it stores a pointer to that memory in pRaw_, which is
defined as a char*. Once the memory is aligned, the char* is cast to type T* and stored in
Page 47
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
reinterpret_cast<uintptr_t>(raw)
By subsequently casting the raw pointer to an uintptr_t, we're able to do the actual
alignment arithmetic, as follows:
For example, if align_ has the value of 4 (4-byte alignment), then the code would operate as
follows:
Page 48
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
The only thing we haven't discussed from apAllocatorBase_<> is the copy constructor and
assignment operator:
apAllocator_<> Class
private:
virtual void allocate () ;
// Allocate our memory for size_ elements of type T with the
// alignment specified by align_. 0 and 1 specify no alignment,
// 2 = word alignment, 4 = 4-byte alignment, ... This must
// be a power of 2.
virtual void deallocate ();
The apAllocator_<> constructor handles memory allocation, memory alignment, and setting
the initial reference count value. The destructor deletes the memory when the object is
destroyed.
public:
explicit apAllocator_ (unsigned int n, unsigned int align = 0)
: apAllocatorBase_<T> (n, align)
{
allocate ();
addRef ();
}
Page 49
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
...
private:
apAllocator_ (const apAllocator_& src);
apAllocator_& operator= (const apAllocator_& src);
// No copy or assignment is allowed.
MEMORY ALIGNMENT
The apAllocator_<> constructor takes two arguments, a size parameter and an alignment
parameter.
Although the alignment parameter is an unsigned int, it can only take certain values. A
value of 0 or 1 indicates alignment on a byte boundary; in other words, no special alignment is
needed. A value of 2 means that memory must be aligned on a word (i.e., 2-byte) boundary.
A value of 4 means that memory must be aligned on a 4-byte boundary.
EXAMPLE
Suppose we use operator new to allocate memory and we receive a memory
pointer, 0x87654325. This hexidecimal value indicates where storage was allocated
for us in memory. For most applications, this address is fine for our needs. The
compiler makes sure that the address is appropriate for the type of object we are
allocating. Different alignment values will alter this memory address, as shown in
Table 3.2.
0 or 1 0x87654325
2 0x87654326
4 0x87654328
8 0x87654328
REFERENC E C OUNTING
The constructor also calls addRef() directly. This means that the client code does not have
to explicitly touch the reference count when an apAllocator_<> object is created. The
reference count is set to 1 when the object is constructed.
What would happen if the client does call addRef()? This would break the reference counting
scheme because the reference count would be 2 instead of 1. When the object is no longer
used and the final instance of apAllocator_<> calls subRef(), the reference count would be
decremented to 1 instead of to 0. We would end up with an object in heap memory that would
never be freed.
Similarly, if we decided to leave out the call to addRef() in the constructor and force the
client to call it explicitly, it could also lead to problems. If the client forgets to call addRef(),
the reference count stays at zero. Our strategy is to make it very clear through the
comments embedded in the code about what is responsible for updating the reference count.
Page 50
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
We use the explicit keyword in the constructor. This keyword prevents the compiler from
using the constructor to perform an implicit conversion from type unsigned int to type
apAllocator_<>. The explicit keyword can only be used with constructors that take a
single argument, and our constructor has two arguments. Or does it? Since most users do not
care about memory alignment, the second constructor argument has a default value of 0 for
alignment (i.e., perform no alignment). So, this constructor can look as if it has only a single
argument (i.e., apAllocator_<char> alloc (5);).
Allocation
The constructor calls the allocate() function to perform the actual memory allocation. The
full implementation of this function is as follows:
protected:
virtual void allocate ()
{
if (size_ == 0) {
// Eliminate possibility of null pointers by allocating 1 item.
pData_ = new T [1];
pRaw_ = 0;
return;
}
if (align_ < 2) {
// Let the compiler worry about any alignment
pData_ = new T [size_];
pRaw_ = 0;
}
else {
// Allocate additional bytes to guarantee alignment.
// Then align and cast to our desired data type.
pRaw_ = new char [sizeof(T) * size_ + (align_ - 1)];
pData_ = alignPointer (pRaw_);
}
}
Our allocate() function has three cases it considers.
The first is what to do when an allocation of zero bytes is requested. This is a very
common programming concern, because we cannot allow the user to obtain a null
pointer (or worse, an uninitialized pointer). We can eliminate all of this checking by
simply allocating a single element of type T. By doing this, our definition for operator
T* in the base class never has to check the pointer first. And, because, our size()
method will return zero in this case, the client code can safely get a valid pointer that
it presumably will never use.
The next case that allocate() considers is when no memory alignment is desired.
Since this is a common case, we bypass our alignment code and let the compiler
decide how to allocate memory:
Page 51
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Our final case in allocate() is when a specific kind of memory alignment is desired;
for example, as it does when managing images using third-party imaging libraries. Many
libraries require memory alignment to avoid the performance hit of copying images. We
use the pRaw_ pointer (defined as a char*) for the allocation, and then align and
coerce the pointer to be compatible with pData_:
pRaw_ = new char [sizeof(T) * size_ + (align_ - 1)];
pData_ = alignPointer (pRaw_);
Our base class provides a function, alignPointer(), to handle the alignment and
conversion to type T*. We saw how this function can alter the memory address by up
to (align_-1) bytes during alignment. For this reason, we must allocate an additional
(align_-1) bytes when we make the allocation, to make sure we never access
memory outside of our allocation.
Deallocation
Our deallocate() function must cope with the pRaw_ and pData_ pointers. Whenever we
bypass performing our own memory alignment, the pRaw_ variable will always be null. This is all
our function needs to know to delete the appropriate pointer. The deallocate() definition is
as follows:
apAlloc<> Class
apAlloc<> is our memory allocation object. This is the object that applications will use
directly to allocate and manage memory. The definition is shown here.
Page 52
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
class apAlloc
{
public:
static apAlloc& gNull ();
// We return this object for any null allocations
// It actually allocates 1 byte to make all the member
// functions valid.
apAlloc ();
// Null allocation. Returns pointer to gNull() memory
protected:
A* pMem_; // Pointer to our allocated memory
Parameter T specifies the unit of allocation. If we had only a single parameter, the meaning of
it would be identical to the meaning for other STL types. For example,
Page 53
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
vector<int> v;
apAlloc<int> a;
describe instances of a template whose unit of storage is an int.
Parameter A specifies how and where memory is allocated. It refers to another template
object whose job is to allocate and delete memory, manage reference counting, and allow
access to the underlying data. If we have an application that requires memory to be allocated
differently, say a private memory heap, we would derive a specific apAllocator_<> object to
do just that.
In our case, the second parameter, A, uses the default implementation apAllocator_<> to
allocate memory from the heap. This allows us to write such statements as:
Our solution is to only ever have a single null object for each apAlloc<> instance. We do this
in a manner similar to constructing a Singleton object. Singleton objects are typically used to
create only a single instance of a given class. See [Gamma95] for a comprehensive description
of the Singleton design pattern. We use a pointer, sNull_, and a gNull() method to
accomplish this:
G NULL() METHOD
The only way to access this pointer is through the gNull() method, whose implementation is
shown here.
gNull() can be used directly, but its main use is to support the null constructor.
Page 54
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
pMem_ = gNull().pMem_;
pMem_->addRef ();
}
apAlloc<> contains a pointer to our allocator object, pMem_. The constructor copies the
pointer and tells the pMem_ object to increase its reference count. The result is that any code
that constructs a null apAlloc<> object will actually point to the same gNull() object. So,
why didn't we just write the constructor as:
*this = gNull();
This statement assigns our object to use the same memory as that used by gNull(). We will
discuss the copy constructor and assignment operator in a moment. The problem is that we
are inside our constructor and the assignment assumes that the object is already
constructed. On the surface it may look like a valid thing to do, but the assignment operator
needs to access the object pointed to by pMem_, and this pointer is null.
The assignment operator and copy constructor are similar, so we will only look at the
assignment operator here:
return *this;
}
First, we must detach from whatever memory allocation we were using by calling subRef() on
our allocated object. If this was the only object using the apAllocator_<> object, it would
be deleted at this time. Next, we point to the same pMem_ object that our src object is using,
and increase its reference count. Because we never have to allocate new memory and copy
the data, these operations are very fast.
Memory Access
Accessing memory is handled in two different ways. We provide both const and non-const
versions of each to satisfy the needs of our clients. To access the pointer at the beginning of
memory, we use:
Page 55
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Object Duplication
The duplicate() method gives us the ability to duplicate an object, while letting both
objects have separate copies of the underlying data. Suppose we have the following code:
apAlloc<int> alloc1(10);
apAlloc<int> alloc2 = alloc1;
Right now, these two objects point to the same underlying data. But what if we want to force
them to use separate copies? This is the purpose of duplicate(), whose implementation is
shown here:
// Shallow copy
T* src = *pMem_;
T* dst = *copy;
std::copy (src, &(src[pMem_->size()]), dst);
return copy;
}
Under most conditions, a shallow copy is appropriate. We run into problems if T is a complex
object or structure that includes pointers that cannot be copied by means of a shallow copy.
Our image class is not affected by this issue because our images are constructed from basic
types. Instead of using memcpy() to duplicate the data, we are using std::copy to
demonstrate how it performs the same function.
[ Team LiB ]
Page 56
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
3.2 Prototyping
Our strong recommendation is that any development plan should include some amount of time
for prototyping. Prototyping has a number of advantages and can directly affect the success
of a commercial software development effort. Our rules of thumb for prototyping are shown in
Figure 3.5.
Early Visibility to Problems. Prototypes help refine the design to produce the desired
final product. More than that, prototyping is a necessary and important step in the
design process. Errors can be caught during the prototyping stage instead of during
actual development. Prototyping allows you to modify the design to avoid mistakes and
it provides better visibility of what is required to complete the final product. Had you
discovered the mistake during the development phase, it could negatively affect both
the content of the product and the schedule for releasing the product.
Measurement of Performance and Code Size. The intent of the prototype isn't to
develop the product, but to develop ideas and a framework for the design. Good coding
practices are as important here as they are for any other part of the design, including
documentation and unit tests. Yes, unit tests. Otherwise, how else can you tell if the
prototype is performing correctly? The unit test framework is also a good way to
measure performance and code size. If a prototype is successful, it might be used as
the basis for the real design. Prototypes only need to implement a small portion of the
overall solution. By keeping the problem space limited, one or more features can be
developed and tested in a very structured environment.
Assurance of Cross-Platform Compatibility. Prototypes are also useful when the product
must run on multiple platforms or on an embedded system. It might be obvious how
something is designed on one platform, but the design may not work as well on others.
This is especially true in the embedded platform world, because the problem is
Page 57
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
constrained by execution time and hardware resources (i.e., processor speed, memory,
and the file system). Prototypes can also help decide which compiler(s) and version to
use. The C++ standard library has evolved in recent years and compiler vendors are
still trying to catch up. You should learn at the prototyping stage that your desired
compiler will or will not work as planned. Once the compiler is chosen, you still must
see if the included standard library will work, or whether a third-party version is
needed.
Test Bed for Language Features. Prototypes are also great for trying out new concepts
or language features. This is especially true with the somewhat complex nature of
templates. It is not always obvious how a final template object might look, or how it
might interact with other objects. This is often found during the design phase, and
some small prototypes can help guide the implementor. In our image class design, the
use of templates is not necessarily obvious from the beginning.
One of the most common fears is that prototypes will be turned into the actual released
software to save time and effort. This is especially true if your management is shown a
prototype that looks like the desired final product. It can give the erroneous impression that
the product is closer to completion than is actually the case. The problem with this scenario
isn't that the prototype gave an incorrect impression, but rather that the expectations of
management weren't properly set. Part of the development manager's role is to clearly set the
expectations for any demonstration. Clearly explaining exactly what is being shown is part of
that responsibility. If this is done well, management need not get a false impression.
Another common misconception about prototyping is that it will delay the actual design and
implementation phases and result in making the product late. In actuality, prototyping is an
iterative process with the design and implementation phases. By clarifying the most difficult
aspects of the design, prototyping can actually result in avoiding costly mistakes and bringing
the product in on time.
Page 58
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
We have chosen a prototyping strategy that lets us investigate three different aspects of the
problem. In Prototype 1, we look at the common elements of images with different pixel types
(8-bit versus 32-bit) to help us create a cleaner design. In Prototype 2, we explore whether
using templates is a better way to handle the similarities between images with different pixel
types. Once we started exploring templates, it became clear that there are both image data
and image storage components. In Prototype 3, we investigate the feasibility of separating
these components in our design.
Prototype 1 explores two types of monochrome images: an 8-bit image like our test
application, and a 32-bit image, as shown in Figure 3.7.
To be precise, our 8-bit image is represented by an unsigned char, and our 32-bit image is
represented by an unsigned int. Each prototype object defines a simple class to create an
image and supports one image processing operation, such as creating a thumbnail image.
Like any good prototyping strategy, we keep features from the test application that worked
and add new features, as shown in Table 3.3.
Access to pixel data via getPixel() and Access to pixel data via pixels() for faster
Page 59
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
setPixel() access
class apImage8
{
public:
apImage8 ();
apImage8 (int width, int height);
// Creates a null image, or the specified size
class apImage32
{
public:
apImage32 ();
apImage32 (int width, int height);
// Creates a null image, or the specified size
Page 60
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
When you ignore the image references, apImage8 and apImage32 are little more than a
wrapper around apAlloc<>. This is clear when you look at a few functions:
apImage8::~apImage8 () {}
It gets even better. We don't define a copy constructor or assignment operator, because the
default version works fine. If this isn't clear, look at what our copy constructor would look like
if we wrote one:
Page 61
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
}
pixels_ is an instance of apAlloc<> and width_ and height_ are just simple types. Since
apAlloc<> has its own copy constructor we just let the compiler take care of this for us.
The thumbnail() method performs the same function as in our test application; however, its
implementation is much cleaner. The output thumbnail image is created like any local variable,
and returned at the end of the function. We saw how simple the copy constructor is, so even
if the compiler creates some temporary copies, the overhead is extremely low. When we wrote
the thumbnail() definition this time, we were careful with our naming convention. Variables x
and y refer to coordinates in the original image and tx and ty refer to coordinates in the
thumbnail image. So even though there are 4 nested loops, the code is still fairly easy to
follow. The thumbnail() method is as follows:
THUMBNAIL() METHOD
return output;
}
If you compare the code for apImage8 and apImage32, you find that they are almost
identical. This is no great surprise, but the prototype shows this very clearly. This similarity
leads to two thoughts. The first (historically) is to see how derivation can help simplify our
design and maximize code reuse. The second is to see how templates can remove all of this
duplicate code.
EXAMPLE
Before templates were readily available, the image design could have been (and
often actually was) handled by deriving each image from a common base class. As
Figure 3.7 on page 47 indicates, we could derive our apImage8 object from an
apMonochromeImage object, which itself could be derived from apImageBase. Color
images could also be handled by this framework by deriving from an apColorImage
class. Although we aren't going to present a full solution for this type of framework,
let's look at one of the issues that would arise by taking a look at the thumbnail()
definition:
class apImageBase
{
public:
...
virtual apImageBase thumbnail (unsigned int reduction) const;
...
protected:
apAlloc<Pel8> pixels_;
Page 62
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
int type_;
int width_;
int height_;
};
You can see there is a new variable, type_, which is used to track what kind of
image this is. This might be just the pixel depth of the image, an enumeration, or
any other unique value to specify the image. The variables width_ and height_
have the same definition as in our prototype, but now pixels_ is always defined as
a buffer of Pel8s. This is not necessarily bad, although it means that pixels_ must
be cast to different types in derived classes. And these casts should exist in only a
single place to keep everything maintainable. Our thumbnail() function returns an
apImageBase object, not the type of the actual image computed in the derived
class. This is a common issue and is discussed at length in other books. See
[Meyers98]. As this example illustrates, it takes a bit of work, but you can
construct a framework that does hold together.
Moving forward, we want to investigate the use of templates in our next prototypes to figure
out how the final image class should be implemented.
Using apAlloc<> helped us eliminate our copy constructor and assignment operator,
made worrying about temporary images unimportant, and greatly improved the
readability of the code.
By extending the test application to explore two different image classes, we observed
that the implementations are very similar. This similarity seems to lend itself to a
derivation class design, or perhaps the use of templates. It is something we need to
explore in future prototypes.
We use templates and rewrite the image class, apImage, to take a template
parameter T that represents the pixel type.
We introduce a handle class idiom so that many apImage<> handle objects can share
the same underlying apImageRep<> representation object.
We verify that our design works with more complex image types, such as an RGB
image.
Use of Templates
Foremost in this prototype is the need to verify that a template object is the correct
representation to solve our problem. Our test application only handled an 8-bit monochrome
image (i.e., unsigned char), and Prototype 1 added a 32-bit monochrome image (i.e.,
unsigned int). Due to the similarity of the apImage8 and apImage32 objects, it makes sense
to turn it directly into a template object, as shown in Figure 3.8:
Page 63
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
In Prototype 2, we introduce the handle class idiom, where there is a representation class
that contains the data and performs all the operations, and a handle class that is a pointer to
the representation class. In our prototype, apImageRep<> is the representation class to which
the handle class apImage<> points.
We begin Prototype 2 by looking at the relevant parts of our apImage<> object from Prototype
1. It is not always clear from the outset how the prototype will be completed. Converting the
apImage<> object into a template object gives us:
EXAMPLE
Here is the original definition of thumbnail() from apImage8:
Page 64
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
TEMPLATE C ONVERSION
template<class T>
apImage<T> apImage<T>::thumbnail (unsigned int reduction) const
{
apImage<T> output (width()/reduction, height()/reduction);
T sum = 0;
...
sum += getPixel (tx*reduction+x, ty*reduction+y);
If T is an unsigned char, the compiler sees this:
As shown in Figure 3.8 on page 52, Prototype 2 defines apImage<T,E>, which has two
template arguments; the second argument is how we solve the thorny issue we just
discussed. The first argument, T, is still the pixel type, but E now represents the internal pixel
type to use during computation. For example, the definition apImage<unsigned char,
unsigned int > describes an image of 8-bit pixels, but uses a 32-bit pixel for internal
computations when necessary. Here are some other examples:
Page 65
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Make sure the default argument is what should be used most of the
time when deciding whether to supply one for your template class.
The handle class idiom has been used as long as C++ has been around. See [Coplien92]. This
is little more than reference counting attached to an object. It is commonplace to call the
shared object the representation object, and to call the objects that point to them the
handle objects. The representation class (or rep class, as it is sometimes called) contains the
implementation, does all the work, and contains all the data, while the handle class is little
more than a pointer to a rep class. A more in-depth discussion can be found in Stroustrup's
The C++ Programming Language, Special Edition, Section 25.7. See [Stroustrup00].
HANDLE OBJEC T
Page 66
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
protected:
apImage (apImageRep<T, E>* rep);
// Construct an image from a rep instance
The rep class apImageRep<T,E> object, which is shown here, is very similar to the templated
image object shown earlier on page 52.
Page 67
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
The first difference between the rep object and the image object (from page 52) is in the
definition of the null image. In the rep object, we use a Singleton method, gNull(), to be our
null object. We define gNull() as:
apImage<Pel8,Pel32> image;
if (image->width() == 0)
// Null object
This would fail if apImage<T,E> contained a null pointer to the apImageRep<T,E> object, and
we dereferenced it to get the width. An alternate, and less desirable, approach is to define
an isNull() method to test if the pointer is null before using it, as shown:
apImage<Pel8,Pel32> image;
if (!image.isNull())
// OK to use operator->
Null images are commonplace in applications. For example, an image operation that cannot
produce a resulting image returns a null image. To eliminate the need to create many null rep
images, we only need to allocate a single gNull(). By calling addRef() when the null image is
created, we ensure that this object never gets deleted.
The complete source for apImage<T,E> can be found on the CD-ROM. Let's look at one of the
constructors to reinforce that this object is little more than a wrapper:
THUMBNAIL() METHOD
Page 68
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
E sum = 0;
for (unsigned int y=0; y<reduction; y++) {
for (unsigned int x=0; x<reduction; x++)
sum += getPixel (tx*reduction+x, ty*reduction+y);
}
output->setPixel (tx, ty, sum / (reduction*reduction));
}
}
// Convert to apImage via the protected constructor
return output;
}
This approach differs from our first attempt at converting thumbnail() to a template function
(as shown on page 53). Rep classes are allocated on the heap and our handle classes can be
allocated anywhere. For this reason, we must use new to allocate our resulting object output.
Although our thumbnail() method returns an apImage<T,E> object, there is no explicit
reference to one in the function. We did this to avoid mixing references to apImage<T,E> and
apImageRep<T,E>. We end the function by executing:
return output;
The compiler converts this object into an apImage<T,E> object, using the protected
constructor:
RGB Images
So far our prototypes have dealt with monochrome images. Our design has gotten sufficiently
complex that we should look at other image types to make sure our implementation and design
are appropriate. We will do that now by looking at Red-Green-Blue (RGB) images. Depending
on the application, color images may be more prevalent than monochrome images. Regardless
of the file format used to store color images, they are usually represented by three
independent values in memory. RGB is the most common, and uses the three colors red, green,
and blue to describe a color pixel. There are many other representations that can be used,
but each uses three independent values to describe an image.
apImage<RGB> image;
The answer is no. Although we defined an RGB image, we did not define any operations for it.
For example, the thumbnail() method needs to be able to perform the following operations
with an RGB image:
Page 69
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Adding support for RGB images entails defining these operations. The compiler will always tell
you when you are missing a function, although some of the error messages are somewhat
cryptic.
While we are adding functions for an RGB data type, we also need to define an RGBPel32 type
so that we don't have the same overflow issue we discussed earlier. RGBPel32 is identical to
RGB, except that it contains three 32-bit values, rather than three 8-bit values. At a minimum,
we need to define these functions:
apImage<RGB,RGBPel32> image;
and even write a simple application:
Page 70
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Defining an RGB image type was a good way to validate that the design is flexible and
can handle many image types cleanly.
Using the handle idiom did not provide any obvious advantages for the design. We had
hoped that reference counting in our apAlloc<T> class, in conjunction with our
apImageRep<T,E> class, would simplify the design, but it didn't. We are going to reuse
the handle idiom in our next prototype, to see if it makes a difference when we
separate storage from the image object.
We need to intelligently manage large blocks of memory and allow access to a potentially large
set of image processing functions. Strictly speaking, the use of the apAlloc<> class is what
manages our image memory.
So what are the advantages of keeping our handle class? It does offer an insulation layer
between our client object and the object that does all the work. However, we also need
another construct to hold all of the data for storing and describing the image data. The image
data, such as the pixels and image dimensions, is contained within the apImageRep<> object,
as shown:
Page 71
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
image classes. This allows applications to customize the front end and reuse the image
storage object. We use Prototype 3 to separate the image storage from the actual image
object by extending the handle idiom we introduced in Prototype 2.
The purpose of Prototype 3 is to separate the image storage from the image processing. To
accomplish this, we create a class, apStorageRep, to encapsulate the image storage, and we
define apImage<> to be the object that performs the image processing. We connect the two
using a handle class, apImageStorage. The final design for Prototype 3 is shown in Figure 3.9.
Note that we have introduced a new naming convention here to make it clear how these
objects are related. Finding good names for closely related objects is not always easy. The
C++ language does not support a class named apImageStorage and one called
apImageStorage<>. The Tmpl suffix is added, renaming apImageStorage<> to
apImageStorageTmpl<T>, to make it clear this is a templated class.
Let's look at how we arrived at this design. Our first attempt to show how our objects are
related is by extending Prototype 2, as shown in Figure 3.10.
Although there is no inheritance in this example, the handle (apImageStorage<T>) and rep (
apStorageRep<T>) classes are very closely related. Stacking them vertically in Figure 3.10
helps to show this relationship and clarifies that apImage<T,E> is related to the other two
classes.
Before we start writing code, let us take one more step in improving our design. The compiler
Page 72
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
will instantiate a template object for each pixel type. Even if your users only need limited
types, the image processing routines may need additional types for temporary images and the
like. We have not worried about code bloat issues in our prototype, but now we need to
consider how to handle them.
Memory is nothing more than a series of bytes that the user's code can access and treat as
other data types. This is pretty much what new does. It retrieves memory from the heap (or
elsewhere, if you define your own new operator) and returns it to the user. When coding with
C, it was customary to call malloc(), then cast the returned pointer to the desired data
type.
There is no reason why we cannot take a similar approach and handle allocation with a
generic object. Our code will perform all the allocation in a base class, apStorageRep, and
perform all other operations through a derived template class, apStorageRepTmpl<T>, as
shown in Figure 3.11.
Given the handle idiom we have decided to use, our rep object, apStorageRep, will contain
generic definitions for image storage, as well as the handle-specific functionality, as shown:
class apStorageRep
{
public:
static apStorageRep* gNull ();
// Representation of a null image storage.
apStorageRep ();
apStorageRep (unsigned int width, unsigned int height,
unsigned int bytesPerPixel);
Page 73
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Since apStorageRep is not a template class, let's look specifically at some of the differences,
as shown:
Once an image is constructed, we allow access to all the parameters of the object, as well as
the memory pointer (as an unsigned char). We chose to leave the base() method public to
allow for future functionality. We continue to use the gNull() definition, so we only have one
instance of a null image (remember, this is an image that has a valid pointer, but has a
width() and height() of zero).
Since apStorageRep is not a templated object, we put its definition in a header file and put
much of its implementation in a source file (.cpp in our case). Not having it be a templated
object has the advantage of giving us control of how the object will be compiled, and lets us
control code bloat. For example, if this were a templated object, compilers would expect
classes to be defined in a single translation unit (or by means of nested include files). This
gives the decision of what to inline, and what not to, to the compiler.
By putting most of the functionality into the base class, we can have a very simple templated
class, apStorageRepTmpl<>, that redefines base() to return a T* pointer (which matches the
pixel type) instead of an unsigned char*, as shown here.
template<class T>
class apStorageRepTmpl : public apStorageRep
{
public:
apStorageRepTmpl () {}
apStorageRepTmpl (unsigned int width, unsigned int height)
: apStorageRep (width, height, sizeof (T)) {}
Page 74
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
virtual ~apStorageRepTmpl () {}
Our handle class, apImageStorage, looks very similar to the apImage<> handle object we
designed in Prototype 2. A portion of the object is shown here.
class apImageStorage
{
public:
apImageStorage (); // A null image storage
apImageStorage (apStorageRep* rep);
virtual ~apImageStorage ();
protected:
apStorageRep* storage_;
};
The one major difference is that our handle object, apImageStorage, is not a template. That
is because this object is a handle to an apStorageRep object and not an
apStorageRepTempl<> object. The complete definition for the apImageStorage object can be
found on the CD-ROM.
template<class T>
class apImageStorageTmpl : public apImageStorage
{
Page 75
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
public:
apImageStorageTmpl () {}
apImageStorageTmpl (unsigned int width, unsigned int height)
: apImageStorage (new apStorageRepTmpl<T> (width, height))
{}
virtual ~apImageStorageTmpl () {}
Like our rep class, we also have to make a cast to get our operator-> to work correctly.
static_cast<> will safely cast the rep class (apStorageRep) pointer, kept by our base class,
to an apStorageRepTmpl<> object. These casts may look complicated, but they really aren't.
Two casts are needed for an apImageStorageTmpl<T> object to access
apStorageRepTmpl<T>. And because of these casts, we can put most of our functionality
inside base classes that are reused by all templates. Figure 3.12 shows the relationship
between these two objects.
We are missing just one piece from this prototype. We need an object that actually performs
the image processing operations on our image storage.
We chose to use apImage<> for this task with two template parameters:
Page 76
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
In this apImage<> object, all aspects of storing and maintaining the pixels are part of the
storage object pixels_. apImage<> exposes only that part of the apImageStorageTmpl<>
interface that it needs to for providing access to width(), height(), and pixels(). The
definitions for getPixel() and setPixel() as they appear in Prototype 3 are as follows:
When we say:
pixels_->base()
it is clear we are calling the base() method of our rep class by means of the handle. Then,
the following statement:
(pixels_->base())[y*width() + x]
becomes the lookup of a pixel, given its coordinates.
THUMBNAIL() METHOD
The last method we need to look at is thumbnail(), and how it appears in Prototype 3 as:
Page 77
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
E sum = 0;
for (unsigned int y=0; y<reduction; y++) {
for (unsigned int x=0; x<reduction; x++)
sum += getPixel (tx*reduction+x, ty*reduction+y);
}
output.setPixel (tx, ty, sum / (reduction*reduction));
}
}
return output;
}
If you removed the template references, this function is very similar to the thumbnail()
method we designed in Prototype 1. This similarity to our very simple example means that we
have a nice clean design.
Let's contrast Prototype 3's simpler version of thumbnail() with that in Prototype 2 to
highlight the differences:
Prototype 2
apImageRep<T,E>* output =
new apImageRep<T,E> (width()/reduction,
height()/reduction);
...
output->setPixel (tx, ty, sum / (reduction*reduction));
Prototype 3
apImage<T,E> output(width()/reduction,
height()/reduction);
...
output.setPixel (tx, ty, sum / (reduction*reduction));
The simple handle object in Prototype 2 meant our image processing routines had to access
the computed images using pointers. We were able to access pixels in the current image using
a normal method call, but access to a new image required access by means of the handle. In
Prototype 3, access is consistent, regardless of which image we are trying to access.
Dividing the image into storage and processing pieces enhances the design. It makes
accessing the apImage<> object very direct using the . operator, even though the
implementation of the object is slightly more complicated.
Accessing image data from inside image processing routines is very clean.
Writing custom versions of apImage<> with access to the same underlying image data
is very simple using the current design. This element works extremely well, and we will
keep it as an integral component of the final design.
Using handles in our prototype has not shown a significant benefit to the design.
apAlloc<> already allows the raw memory to be shared, avoiding the need to make
needless copies of the data. Based on our prototypes, we have decided not to use
handles in the final design.
[ Team LiB ]
Page 78
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
3.3 Summary
In this chapter, we designed and implemented an object to efficiently manage memory
allocation. We outlined the requirements of such an object in terms of a commercial-quality
image framework. Then we designed and implemented the solution, using reference counting
and memory alignment techniques to achieve those requirements. Because the solution
involved heavy use of templates, we also provided a review of some of the syntactic issues
with templates, including class conversion to templates, template specialization, function
templates, and function template specialization.
With our memory allocation object complete, we began prototyping different aspects of the
image framework design. Our first prototype explored two different types of images, 8-bit and
32-bit images, to determine if there were enough similarities to influence the design. We found
that the classes were indeed very similar, and we felt that the design should be extended to
include inheritance and/or templates to take advantage of the commonality.
In our second prototype, we extended our first prototype by using templates to handle the
similarity among image objects. In addition, we introduced the handle class idiom, so that
image objects of different types could share the same underlying representation object. Then
we took the prototype one step further by exploring more complex image types, such as RGB
images. We found that the use of templates resulted in an efficient design that leveraged the
similarities; however, the handle idiom did not provide any obvious advantages.
In our final prototype, we explored separating the image storage component from the image
object, because of the amount of data our image classes contained. We felt this strategy
might allow the eventual reuse of the image storage object by various image objects. Once
again, we tried to apply the handle idiom in our solution. We found that dividing the image into
storage and processing pieces enhanced the design, but the handle idiom did not provide any
obvious benefits.
In Chapter 4, we consider other issues for the final design of our image framework, including:
coding guidelines and practices, object creation with the goal of reusability, and the
integration of debugging support into the framework's design.
[ Team LiB ]
Page 79
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
C++ Concepts
Coding Guidelines
Streams
Binary Strings
Object Registry
STL Components
Reusability
Sinks
In this chapter we discuss issues that should be considered at the beginning of the design
process, including guidelines for coding and reusability. We also demonstrate a number of
techniques for adding debugging support to your design.
[ Team LiB ]
Page 80
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
From our own experience, we have found that balancing the needs of the organization without
suppressing the creativity of the developers can be achieved. A standards document that
addresses the most important issues, as well as guidelines for other issues, is a workable
compromise. Developers can be challenged during code reviews if the software deviates from
the standards, and any changes needed are agreed upon by those present. A coding standard
should be thought of as a set of coding guidelines.
Guidelines should be agreed upon by the team before starting a project. If a company-wide
standard exists, it should be reviewed to make sure the guidelines are applicable. Make the
guidelines simple and easy to follow, and keep in mind their purpose: you want software that
is readable and maintainable, and you want development to progress smoothly and efficiently.
Guidelines should, at a minimum, include such items as:
Naming conventions
Indentation
Namespace usage
Template restrictions
For example, consider a small change you need to make to some code: the fixed coordinates
are to be replaced by the actual coordinates of the image. The existing code looks like this:
Page 81
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Most developers (and we are no exception) like to work with a certain coding style, but that
is not a sufficient reason to change any more lines of code than are necessary. Legacy code
should always be viewed as fragile, and making any unnecessary changes should be avoided.
We recommend using a system utility, such as diff, to view the changes made to a file,
If we did this for the changes mentioned earlier, the output of the diff would be as follows:
1,6c1,4
Page 82
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Traditional C code has used the underscore character to construct long variable names. For
example, total_count and max_size are variable names that are both descriptive and
readable. But just as readable are names like totalCount and maxSize. We prefer to use
capitalization, but both methods work better as names than totalcount and maxsize.
Consider a better example: compare bigE, and big_e, and bige.
Naming conventions are numerous. We will look at two sets of them in a little more detail: one
is the set of conventions used in this book, while the other is popular among Microsoft
developers.
The Microsoft convention defines class names using the prefix C. For example, the Microsoft
Foundation Classes (MFC) contains the classes CPen, CBrush, and CFont. Our code examples
use an ap prefix to avoid name collisions with other classes, as in the examples apRect and
apException. If we had used the Microsoft convention, we would have wanted to call our
classes CRect and CException, but both are already defined in the MFC (and probably
countless other C++ libraries). Feel free to choose whatever prefix you want, but it is
important to distinguish class names (and structs) from other identifiers.
Namespaces were added to the language to prevent name collisions, but aside from their use
in the standard C++ library, we have seen little use of this feature elsewhere. There is a
common habit among developers to effectively disable namespaces by adding using
namespace std;. We do not recommend this approach. Namespaces are important. It is not
difficult to get into the habit of writing std:: when you want to access a feature from the
standard library. For a nice description of how to safely migrate software toward using
namespaces, see [Sutter02].
Member variables should also be distinguishable from other variables, such as local variables or
arguments. The Microsoft convention adds two pieces of information to a variable name. The
first is an m_ prefix to indicate a member variable, and the second group of letters, such as
str, indicates the type of variable. Examples include:
Page 83
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
m_bUpdatable boolean
m_nQuery short
m_lCollatingOrder long
m_strConnect CString
m_pvData void* pointer
Our convention is to add a simple underscore _ suffix to identify member variables. While not
as descriptive as the other convention, it also doesn't tie our variable name to a specific data
type. The variable name without the underscore remains available for other uses, such as a
local variable. We also apply this convention to internal class names that are included in the
API, but are never called directly by the API's clients.
Using the underscore suffix, the variables from the previous example become:
updatable_
query_
collatingOrder_
connect_
data_
Here's a simple example:
class apExample {
public:
apExample (int size) : size_ (size) {}
int size () const { return size_;}
private:
int size_;
};
Our convention also includes a few other rules, as shown in Table 4.1.
class apExample {
public:
enum eTypes {eNone, eSome, eAll};
static apExample& gOnly()
{ if (!sOnly_) sOnly_ = new apExample();
return *sOnly_;}
eTypes type () const { return type_;}
private:
static apExample* sOnly_;
eTypes type_;
Page 84
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
apExample* apExample::sOnly_ = 0;
This object may not do anything interesting, but it does show that with just a few simple
rules, a piece of code can become quite readable. apExample is a singleton object. Access to
member functions is through gOnly(), which returns a reference to the only instance of
apExample.
One last area we mention is template type arguments. Expressive names for templates are
nice, but they consume a lot of space. Consider this example, where we define the copy
constructor:
Note that in Standard Template Library (STL), the template parameter names are very long
because they form an API, and the use of long names is very descriptive.
4.1.3 Indentation
Indentation is also a controversial issue. If you are designing a class library that you intend to
sell to other developers, consistent indentation across all files is expected. However, if you
are developing an application where only in-house developers have access to the sources,
you can relax this standard to keep the indentation style consistent across a file (instead of
ideally across all sources). Like we demonstrated earlier, modifications to an existing
indentation style should be avoided unless most of the contents of a file are being rewritten.
Otherwise, many unnecessary changes result that could delay releasing the code, as each
difference has to be examined and tested.
We strongly recommend using program editors that can be configured to many desired styles,
reducing the number of keystrokes necessary to use any particular style. For example, our
Page 85
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
4.1.4 Comments
Yes - and the more the better! Comments are almost always a good thing, as long as they
accurately represent what the code does. Someone unfamiliar with a piece of software will
usually look at the comments before looking at the code itself. And just like it takes some time
to learn how to write good software, it does take time to learn how to write good comments.
It is not the length of the comments, or the style in which you write them; rather it is the
information they contain. Comments are more than just a written explanation of what the
software does. They also contain thoughts and ideas that the developer noted while the code
was being written. A casual observer may not understand the complexity of a block of code or
the limitations that were added because of scheduling or other factors.
Block comments are usually placed at the beginning of the file or before any piece of code
that is not obvious. The length is not important, as long as the block comment conveys:
We will discuss our recommendations for header file comments in the next section, but first,
look at an example of right ways and wrong ways to document.
Page 86
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Taken out of context, it is not clear what this code does. From the member name,
apCache::clear, one could deduce that this code is deleting information from a cache
object, but it requires a bit of study to figure out what this code is doing in all cases.
Developers write code like this all the time. When it was written, it was very clear to the
developer how and why it was written this way. But to other observers, or perhaps even the
developer after a few weeks, the subtleties of this code might be lost. Now let's look at this
example again, but with a very concise comment:
Comments should also be added to single lines in order to clarify what is happening, or remind
the reader why something is being done. It is a good idea to use this for conditional
statements, as shown:
void function ()
{
if (isRunning()) {
...
}
else {
// We're not running so use default values
...
}
}
It is very clear after the conditional statement what the block of code is doing, but this
thought might be lost when the else code is reached. Adding a comment here is a good idea
Page 87
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
just to get the readers mind back in sync with the code.
Each file should have comments at the beginning containing any copyright information required
by the company. Even if you are writing software for yourself, you should get in the habit of
adding a standard block to all files. Following this, you should have at least a one-line
description of what the file contains. Our files look like this:
// bstring.cpp
//
// Copyright (c) 2002 by Philip Romanik, Amy Muntz
// All rights reserved. This material contains unpublished,
// copyrighted work, which includes confidential and proprietary
// information.
//
// Created 1/22/2002
//
// Binary string class
The copyright block is often much longer than this, but it is surprising how much proprietary
software contains no copyright information at all. If you are in the business of writing
commercial software, no software you create should be missing this block. Even free software
contains an extensive copyright notice, an assignment of rights, and a list of any restrictions
placed on the code.
Adding a line to tell when the file was created helps to distinguish older implementations from
one another. Developers will often add a change list to describe all changes made to the file
since it was created. There is nothing wrong with this, although its use has dwindled since
version control systems have become popular.
Include guards will prevent a header file from being included more than once during the
compilation of a source file. Your symbol names should be unique, and we recommend
choosing the name based on the name of the file. For example, our file, cache.h, contains this
include guard:
#ifndef _cache_h_
#define _cache_h_
...
#endif // _cache_h_
Lakos describes using redundant include guards to speed up compilation. See [Lakos96]. For
large projects, it takes time to open each file, only to find that the include guard symbol is
already defined (i.e., the file has already been included). The effects on compilation time can
be dramatic, and Lakos shows a possible 20x increase in compilation times when only standard
include guards are used.
Since the header file is the public interface to an object, it makes sense to document each
member function here with a simple description. We also recommend adjusting the alignment
of similar lines of code to make it easy to see the differences. For example, here are two inline
member functions:
Page 88
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
constructors
other methods
member functions
member variables
member functions
member variables
For any class that does not require a user-written copy constructor or assignment operator,
place a comment in the header file stating that it was a conscious decision to omit it. The
absence of either a copy constructor or a comment saying that one is not necessary is a
warning sign that the object needs to be checked.
4.1.6 Restrictions
Page 89
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
The C++ standard defines the capabilities of the language, but just because a compiler allows
a certain syntax or feature, does not mean that you should be using it. Organizations and
projects have very good reasons, such as multi-platform or legacy concerns, to restrict the
use of features.
The compiler used to produce a piece of software, or any tool used for development, should
be controlled during the development and maintenance cycle of a project. You must not just
change to a more recent version of the compiler without the same amount of testing done for
a general release. It is very common for software released on multiple platforms or embedded
platforms to use a specific language subset. With multi-platform releases, you are faced with
designing to the least-common denominator (i.e., you build to the most restrictive features of
each compiler). With embedded platforms, you have to deal with memory and performance
issues that may force certain features to be avoided.
A good example is exception handling. As the C++ standard library has evolved, so have many
semi-compatible or outright incompatible library versions out there. But replacing these older
libraries has a cost associated with it. And, since the performance of exception handling can
vary greatly on different platforms (see Item 15 in [Meyers96]), your organization may decide
that exception handling should not be used.
Templates are another issue. When compilers first started to support templates, the level of
support varied greatly by compiler vendor. Even today, some compilers have limitations
regarding function templates and partial specialization. We were quite surprised when we took
some of our code samples and compiled them on different platforms. We found that some
compilers were able to figure out which template function or conversion to apply, while others
gave up because of ambiguity. It turns out that the ambiguous template instantiation error is
the correct behavior, according to the C++ standard. The standard describes a scoring
method to deduce template parameters and to decide which template to instantiate. If there
is more than one template definition producing an equally good score, an error is produced
because of the ambiguity. For example, consider this function template:
Page 90
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
It is pretty obvious to us what should happen; however, compilers that comply with the C++
standard will generate an error, because both templates are considered equally good for many
data types. Our workaround is using a macro to selectively compile code and to define specific
versions, such as those shown below:
But even with such ways to curb the size of applications using templates, some organizations
will simply choose to avoid the issue by eliminating or restricting the use of templates. For
example, a single instance of std::vector<void*> can be used to handle a list of pointers.
The main disadvantage of this is that casting will be needed when pointers are extracted from
this container. It does not have to be a maintenance nightmare if all access to this object
happens from a single function to handle the cast:
class apImageList{
public:
...
const apImage* operator[] (unsigned int index) const
{ return reinterpret_cast<const apImage*>(list_[index]);}
private:
std::vector<void*> list_;
};
We could use the same template instantiation many times with complete safety, because
access to the container passes through the necessary casting. Any use of casting should be
documented in the code to explain why it is done.
[
Tea
m LiB
]
Page 91
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
In this section we discuss reusable software components. By reusable, we mean not just
software that can be used on multiple projects and/or platforms, but also software that can
serve multiple purposes in the same application. A component's reusability does not guarantee
a well-designed or cleanly written object, but it certainly increases the odds of being such. It
is not uncommon to have developers whose entire mission is to develop reusable components
for an organization or group.
To see what we mean, let us look at a well-known and well-designed reusable component: the
string object in the Standard Template Library. String classes have been around since the
beginning of C++. The std::string class is an elegantly designed package that has almost
everything you could need. But even with this functionality, you don't have to search too
hard to find somebody who thinks the library has too little functionality or, conversely, too
much functionality. Let's use this class to explore the issues surrounding reusable software.
Reusable software solves a generic problem. It should address the needs of a company,
workgroup, or class of applications. For example, the standard library string class is globally
generic. It not only handles character manipulation of char-sized data, it can also work with
characters of various sizes:
Let us assume that the standard library did not exist and we were forced to design our own
string class. What functionality would it support and what limitations would we impose? There
is no single correct answer, because you have to consider a number of factors:
Can C-style string functions be used to provide some of the needed functionality?
Page 92
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
The answers to these questions may not be obvious until you examine how and where you
could use a string class. Let's face it, if no string class existed at all, software would still get
written, and it might look like this:
At the other extreme, you can write a string class that does only what is necessary to get
the job done as follows:
class apString
{
public:
apString (int size);
~apString ();
apString (const apString& src);
apString& operator= (const apStringc& src);
int size () const;
char& operator[] (int index);
private:
char* p_;
};
Is this a string class? Yes it is, but as you can see it does very little. Such an object may
prove invaluable in one application, but we do not consider it reusable because it does not do
anything very useful. Let's extend our definition of reusable code to include the following
attributes:
Can be described in a few sentences using concrete terms. If you can describe an
object in simple terms, it will tend to be a simple object. For example, we can describe
a string class by saying, "An object that manipulates character arrays, replacing
C-style string functionality in most applications."
Makes few assumptions about how or when the class can be used. For example, what
if our string class only worked on strings with fewer than 100 characters? Or, what if
the class only supported the storage of letters but no numbers? All designs have some
limitations, but these limitations should not limit the object's general-purpose use.
Page 93
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
developers and one of them wants to change the interface? Should it be easy to add new
member functions? Who is responsible for preventing the object from getting too complex?
Smaller groups can handle the management of reusable components internally. If the software
is only used within a single group, a single individual can be charged with maintaining the
code, just like any other piece of software. The group's manager can act as the clearing
house for requests and decide when it is appropriate to make changes.
The process is more interesting for mid-sized organizations that consist of more than one
development team. Each team has different schedules and requirements, and there is no
centralized group to manage the shared software. Control of the reusable code should belong
to a team created from members of all the separate development groups. One developer from
each group should act as spokesperson for his/her group to ensure that changes and
enhancements will not negatively impact their project. Having multiple developers from the
same project attend these meetings is discouraged, because it tends to slow down and
complicate the process. After all, you don't want an entirely new bureaucracy to develop that
will slow down your development cycle. This group should be led by a development manager or
project manager to keep it on track.
Here are some common situations you might be faced with. Imagine there are two
development project teams, team A and team B, that are each using the same reusable
components.
Team A discovers that some enhancements are needed to continue to use a reusable
component in their product. There is sufficient time to make these changes before the
next release. Team B, however, has no desire to see any changes made to their
software base.
Team A makes a small bug fix to the reusable component and commits the change to
the source control system. Team B unknowingly uses this version and strange problems
appear in their application.
These issues are not unique to reusable software components, but they can create situations
that cannot be resolved by a single development team. In the first example above,
configuration management software can allow team B to use the current version of a
component while team A uses a different version. These changes can consist of a combination
of new functionality, extensions to existing functionality, or bug fixes. Whatever the reason,
no changes to the software should be made by team A without the knowledge and approval of
team B. Version control is very important for reusable components, so much so that using
timestamps or other simple methods to select which version of software to use may not be
sufficient. For this reason, we recommend adding version or release tags to new versions. This
treats an internally developed component the same way as one purchased from a third-party
source. It also impresses on the team the notion that changes to a reusable component
should happen at the same frequency as one would expect new versions of third-party
libraries.
Page 94
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
The second issue can be caused by assuming that the most recent version of a component is
always the one to select. The solution to this problem is easy: don't make this assumption.
This is no different than taking a new compiler or library update from a vendor and assuming it
will work. However, this scenario can easily happen because most changes made to mature
software involve fixing bugs. If we had developed the standard library string class internally,
who would not just want to use the most recent version when it becomes available? We
sometimes place more trust in something or someone than we should. Consider this simple
function as part of a reusable component:
Now let us assume that the person maintaining the isValid() function makes an
enhancement to the function as shown:
Page 95
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
documentation better reflects what the function actually does. It would not surprise us if the
documentation continues to ignore the behavior of null strings. This can happen because
documentation changes do not always occur when changes are made to the code. Many
developers will argue that the code itself is the documentation, and any documentation
included in the header file is just a synopsis of what the function does. We won't get into this
issue right now because it will only get us off track.
Let us look at what happens when a newer version of isValid() is available in a software
update. The decision to use the updated version of isValid() will most likely be made based
on the function's accepting alphanumeric characters and not just alphabetic characters. Even
if the update includes information regarding the behavior of nulls, this change can easily fall
through the cracks and not get noticed. It may not even be obvious to the developer if this
special null case is even an issue. When multiple teams use the same piece of reusable code,
it is possible that all the developers on team A will use the null behavior of isValid() while
team B never does. If the person responsible for maintaining isValid() is part of team B,
they may change the null behavior of isValid(), thinking it has no effect on the code, when
in actuality it could greatly affect team A.
The real costs of creating a reusable component are now becoming clear. A piece of software
goes through a number of steps before it becomes a reusable component. For smaller
organizations, it all starts with the creation of a piece of software that solves a particular
problem, but may be generic enough to be used in other places. Once it is recognized as a
possible reusable component, a set of well-defined steps should be followed:
1. Proposal. A short proposal should describe the overall functionality, and explain why
this component is necessary to the project and why it can be used by other projects.
A short example of how the code is used is helpful. At this stage, the proposal is only
meant to see if there is enough interest in the component.
2. Approval and assignment of a maintainer (most likely the developer) and the group to
review it.
6. Initial check-in.
A binary string class may not sound very interesting until you realize all the uses there are for
it. If we renamed our example to be something like "object streaming" or "object persistence,"
it might appear more important. To achieve either goal, software is needed to collect and
manipulate binary streams of data. These data streams can represent anything from image
data to the contents of objects. We aren't going to tackle a complete object persistence
mechanism here, but we will show you one important reusable component.
To be more precise, our binary string object manages tagged data. By this we mean that
every item written to our stream consists of two parts. The first part is a tag, which specifies
the type of data written, and is followed by the data itself. A raw binary stream of data can
be difficult to decode, especially if its format is modified over time. Tagging the data makes it
easier to interpret and allows anyone to read a stream of data, even when its meaning is not
known. Our object, apBString, tags data in the following formats:
Page 96
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Float (4 bytes)
Double (8 bytes)
String
Data
apBString
The tag field is one byte and precedes the data. Most tagged formats are pretty obvious,
because they follow the way the underlying data types are stored in memory. A string is
written as a length (4 bytes), followed by the string data. A data block is written in the same
way and is used to represent arbitrary data. apBString objects can also be nested inside of
other apBString objects, allowing this object to encapsulate other binary objects. It is this
behavior that paves the way for object streaming. The definition of apBString is shown here.
// Insertion operators
apBString& operator<< (Pel8 b);
apBString& operator<< (Pel16 w);
apBString& operator<< (Pel32s l);
apBString& operator<< (Pel32 l);
apBString& operator<< (float f);
apBString& operator<< (double d);
apBString& operator<< (const std::string& s);
Page 97
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
// Extraction operators
apBString& operator>> (Pel8& b);
apBString& operator>> (Pel16& w);
apBString& operator>> (Pel32s& l);
apBString& operator>> (Pel32& l);
apBString& operator>> (float& f);
apBString& operator>> (double& d);
apBString& operator>> (std::string& s);
apBString& operator>> (apBString& bstr);
private:
std::string string_;
int offset_;
bool match_;
void add (eTypes type, const void* data, unsigned int size);
// Add the specified data to our buffer
Let us look at how a variable of type Pel16 is treated. First, there needs to be a way to add
data to our binary string:
Page 98
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
switch (type) {
case ePel8:
w = readPel8 (p);
break;
case ePel16:
w = readPel16 (p);
match = true;
break;
case ePel32s:
w = (Pel16) readPel32s (p);
break;
case ePel32:
w = (Pel16) readPel32 (p);
break;
case eFloat:
w = (Pel16) readFloat (p);
break;
case eDouble:
w = (Pel16) readDouble (p);
break;
case eString:
w = (Pel16) atoi (readString(p).c_str());
break;
default:
// Unsupported type. We don't have to do anything
break;
}
Page 99
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
So, how reusable is apBString? In general terms, it is a very basic building block to manage
binary data that satisfies all of our definitions for a reusable object. But, as we have designed
it, it is only portable on machines with the same memory architecture. Our class copies data,
byte for byte, into our binary string. If we use apBString on a machine in little-endian format
(the low-order byte is stored in memory at the lowest address, such as with Intel processors),
it cannot be read properly on a big-endian machine (the high-order byte is stored in memory
at the lowest address, such as with Sun SPARC). We could have chosen to address the
endian issue in our design by making sure all of our string data was written in the chosen
endian format; however, that was not one of our design guidelines. In this regard, apBString
is not reusable between different types of machines. The code is portable, but the data files
are not.
[
Tea
m LiB
]
Page 100
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Besides using software debuggers, profilers, and other tools, you can also insert statements
to log information during the debugging phase. This information is subsequently written to the
console, file, or other device for later review; or, it is simply discarded.
There are advantages and disadvantages to adding your own debugging code. The biggest
advantage is that you are in total control. You can decide if your code waits for specific
conditions to occur or if it generates reams of information immediately. This lets you detect
many timing-related bugs that would otherwise be almost impossible to diagnose.
One of the biggest disadvantages, however, is that debugging code is not present in
production releases. Sometimes this results in timing-related bugs that appear only in the
production version of the software.
Many people handle the debugging statements issue in code that looks very similar to this:
#ifdef DEBUG
std::cerr << "Some debugging messages" << std::endl;
#endif
During development, the makefile will define the variable DEBUG to compile the debugging code
into the application. Production releases do not define this variable, effectively removing all of
this code from the product.
In this section, we present a strategy for handling debugging information that is:
First, we design a generalized debugging stream. Next, we create destination objects, called
sinks, for the debugging output. Once we have the destinations, we create an object to
control the amount of debugging information that is actually output to those destinations.
Finally, we extend our debugging environment to allow remote access to objects through an
object registry.
Figure 4.1 illustrates the components that make up our debugging environment.
Page 101
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
If you want to continue using std::cout, it is possible to redirect this stream if you need to
create a permanent copy. We can temporarily redirect std::cout to a file, redirect.txt,
with the following code:
#include <iostream>
#include <fstream>
...
std::cout << "This should be written to the console" << std::endl;
The C++ standard stream library consists of a very full set of classes to allow streams to be
created and manipulated. However, the classes can be very complicated to use and
understand. Fortunately, we do not have to jump in and completely understand std::ostream
and everything that goes with it. We can choose a subset that meets our needs. Since this is
only being used in debug mode, we can afford to choose a solution that is not optimized for
Page 102
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
performance. This is an especially good trade-off if the solution is easy to understand and
implement.
To create our new debugging stream, cdebug, we first create regular static objects. In our
header file, we declare the objects at global scope, as shown:
apDebugStringBuf<char> debugstream;
std::ostream cdebug (&debugstream);
cdebug is an instance of std::ostream that connects a stream with our string buffer object.
debugstream is our global stream buffer object (an instance of apDebugStringBuf<>, which is
defined on page 100) that forwards the stream data to the appropriate destination, called a
sink. (Sinks are fully described on page 96.)
cdebug << "This line goes to our null sink" << std::endl;
debugstream.sink (apDebugSinkConsole::sOnly);
cdebug << "This line goes to std::cout" << std::endl;
apDebugSinkConsole::sOnly.showHeader (true);
cdebug << "Also to std::cout, but with a timestamp" << std::endl;
apDebugSinkFile::sOnly.setFile ("test.txt");
debugstream.sink (apDebugSinkFile::sOnly);
cdebug << "This line goes to test.txt" << std::endl;
apDebugSinkFile::sOnly.showHeader (true);
cdebug << "Also to test.txt, but with a timestamp" << std::endl;
If you look at the file, test.txt, it will contain:
Our base class, apDebugSink, defines the basic interface that any derived sink object must
implement. Its definition is shown here.
class apDebugSink
{
public:
apDebugSink ();
Page 103
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
protected:
std::string standardHeader ();
std::string apDebugSink::standardHeader ()
{
std::string header;
return header;
}
In our framework, we define four different types of sinks: null, console, file, and windows.
NULL SINK
private:
apDebugSinkNull ();
};
We only need a single instance of each sink, so we have defined a static instance, sOnly. In
Page 104
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
previous prototypes, we used a function, gOnly(), to access our singleton object. In this
case, however, we are directly constructing the static object because we will be passing it
around by reference. This is obvious when we look at how sink objects connect with the
cdebug stream.
The remaining lines of code contained in the source file are as follows:
apDebugSinkNull::apDebugSinkNull () {}
CONSOLE SINK
protected:
virtual void display (const std::string& str);
// Output the string. Derived classes can override this
apDebugSinkConsole ();
virtual ~apDebugSinkConsole ();
std::string buffer_;
};
FILE SINK
private:
virtual void display (const std::string& str);
apDebugSinkFile ();
virtual ~apDebugSinkFile ();
std::string file_;
};
As you can see, apDebugSinkFile actually derives from apDebugSinkConsole. We do this
because the only difference between these objects is where the data is written when the
Page 105
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Flushing a Sink
Flushing data from the internal buffer occurs in the following cases:
void apDebugSinkConsole::flush ()
{
if (buffer_.size() == 0)
return;
if (enableHeader_)
buffer_ = header() + buffer_;
display (buffer_);
buffer_.clear ();
}
Since flush() does all the work of formatting the buffer, display() becomes very simple, as
shown:
Page 106
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
WINDOWS SINK
Let's look at one more useful sink before we leave this topic. If you are developing under
Microsoft Windows, there is a useful function, OutputDebugString(), that outputs a string to
the debugger if one is running. And, thanks to a great piece of freeware called DebugView,
which is provided on the CD-ROM included with this book, you can view these strings
whenever you want, even in release builds. Adding a new sink to support this type of output
is easy. Its definition is shown here.
We need to override this function to insert characters into our apDebugSink object, as
shown:
Page 107
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
return traits_type::not_eof(c);
}
private:
apDebugSink* sink_;
};
This object looks confusing because of how the stream library is written, but it is actually very
powerful. With this object, we can write to any kind of apDebugSink object (file, console,
null, or windows) without worrying about the details.
apDebugStringBuf<char> debugstream;
std::ostream cdebug (&debugstream);
We can define an object, apDebug, to keep track of the current amount of debugging detail to
be generated. Its definition is shown here.
class apDebug
{
public:
static apDebug& gOnly ();
private:
static apDebug* sOnly_;
int debug_;
apDebug ();
};
debug_ is an integer value that indicates the amount of debugging output to generate. We
use the convention of the higher the number, the more detail to include. Whenever the
debugging level of a piece of debugging code is greater than or equal to the current
debugging level, the debugging code should be executed. We can automate this decision by
constructing a clever macro. In general, we avoid macros, but this case really needs one:
Page 108
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
#ifdef NODEBUG
#define DEBUGGING(level,statements)
#else
#define DEBUGGING(level,statements) \
if (apDebug::gOnly().isDebug(level)) { \
statements \
}
#endif
The macro, DEBUGGING, is disabled if the symbol NODEBUG is defined during compilation. This
makes it easy to remove all debugging code from your application with a simple change to
your makefile. You can treat this macro just like a function:
DEBUGGING(level, statements);
where level is the debugging level of these statements, and statements is one or more C++
statements that should be executed when debug_ >= level. In case you aren't used to the
syntax of macros, the \ characters at the end of some lines indicate that the macro
continues on the next line. Using this character allows us to make the macro look more like
lines of code.
Note that our symbol, NODEBUG, is different than the NDEBUG symbol, which is used by
compilers to indicate when assertions should be included in a build. We want our debugging
interface to be included in production builds during testing and early releases, so we use a
separate symbol, NODEBUG, for this purpose. If we used NDBEUG for both purposes, we could
not have our debugging support included without assertions also being present.
One big downside to using macros is interpreting error messages during compilation. Macros
are expanded, meaning that one line of your source code actually becomes many lines. If an
error is found while compiling the macro, some compilers may or may not report the correct
line of the error. And for those that do, you may still be left with some fairly cryptic
messages. Here's another example:
if (0 >= 1) {
first debugging statement ...
}
if (0 >= 2) {
second debugging statement ...
}
Although these statements add extra bytes to the application, they add very little overhead
when they are inactive. Activating either or both of these lines is easy:
Page 109
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
0 Debugging disabled
We can extend this technique to change the meaning of a debugging level by using the
specific bits instead of the value. And, if an integer does not contain enough bits to contain
all your combinations, you can modify apDebug to use a std::bitset to store an arbitrary
number of bits, as follows:
class apDebug
{
public:
static apDebug& gOnly ();
private:
static apDebug* sOnly_;
std::bitset<32> debug_; // 32 is an arbitrary value
apDebug ();
};
Page 110
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
In this implementation, instead of setting a debugging level, you are setting a specific bit to
enable or disable a specific debugging feature. Our earlier example now looks like:
enum {
eGUIDebug = 0, // GUI debugging
eImageDebug = 1, // Image operations
eStorageDebug = 2, // Image storage operations
...
};
apDebug::gOnly().reset (); // Disable all debugging
apDebug::gOnly().set (eImageDebug); // Enable bit-1
DEBUGGING(eImageDebug, cdebug << "Image Debugging Enabled";);
If you only ever have one instance of an object, accessing it would be easy. We are very
fond of using a gOnly() method to reference a singleton object, and this reference is
available throughout the application. For example, enabling debugging using the apDebug
object we presented in the previous section is as easy as:
apDebug::gOnly().set (eImageDebug);
The problem is more complicated when multiple instances of an object exist. Without some
listing of all objects in existence, you cannot easily communicate with a specific instance. An
object registry consists of two parts, as shown in Figure 4.2.
The first part tracks what kinds of objects are registered, with one entry per object type. The
second part tracks the current instances of the specific object types. With such a registry,
Page 111
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
we can quickly see if an object type is contained in the registry, and, if so, the specific
instances of the object that exist.
EXAMPLE
Years ago when we first learned C++, it was common to see examples such as:
class Object
{
Object ();
virtual ~Object ();
};
Fortunately, those days are behind us. Our design gives the developer the choice of whether
an object should be registered, on both an object or instance basis. We also keep it simple so
that you can use only the functionality you need. This design is shown in Figure 4.3.
apObjectMgr manages a list of all object types that are using our debugger interface.
apObjectInfoBase is the base class that you derive objects from when you want an
object that keeps track of all instances of a specific type.
apObject<T> is the base class that you derive objects from when you want an object
to have remote debugging capability.
user object is an object that must be derived from apObject<T> if the user wants it
Page 112
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Every object that enters the registry must derive from a common base class, because we
need to maintain a list of objects in the registry. The base class allows us to track all
instances of the object as well as do some basic control. We start by designing a singleton
object for each object that we want to track. Using the gOnly() method to access the
object also means that it is only created the first time the object is created. Like we have
done in other examples, we split the objects into a common non-template object and a
template object, as follows:
class apObjectInfoBase
{
public:
typedef std::map<void*, char> INSTANCEMAP;
protected:
int debug_; // Debug level for this object
INSTANCEMAP mapping_; // List of all current objects
};
apObjectInfoBase uses a std::map object to keep a list of all instances of a particular
object. By storing the object as a void* pointer, as opposed to a native pointer, we are able
to keep our solution generic. But why did we use a std::map object when a std::vector
object would also work, as would a std::list object? We define mapping_ as a
std::map<void*, char>, where void* is the key (the address of the object), and char is
the value (an arbitrary number). The purpose of mapping_ is to be a list of object pointers.
We chose a std::map object because it can efficiently handle additions and deletions and do
all the work for us. For example, look at the code that adds or removes an object instance
from this list:
Keep in mind that there are many alternatives to using std::map. It is easy to spend too
much time researching the problem in hopes of finding the most efficient STL component. For
example, we could have also chosen to use std::set, as this matches the requirements of
Page 113
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
our object very closely. With so many components to choose from, this can take longer than
it takes to implement the entire object. If you have not used many STL components, you will
soon discover that you end up using only a small subset of objects. This is not a bad thing.
For commercial software, this can give you an edge. While some teams are crafting the
"perfect solution," you already have a solution implemented and working.
Our base class, apObjectInfoBase, also contains a debugging interface very similar to the
apDebug object we presented earlier. In this implementation, we use the debug_ variable as a
level, rather than as a sequence of bits. The class also defines pure virtual methods
process() and dump(). dump() produces a human readable description of the object.
process() is a general interface that we can use for remote debugging. To use this
functionality, however, we still need one instance for every object type. That is exactly what
the apObjectInfo<> class provides:
private:
static apObjectInfo<T>* sOnly_;
std::string name_;
apObjectInfo ();
};
The template argument, T, for apObjectInfo<> is the name of the object type we want to
debug. In addition to being a singleton object and defining our usual gOnly() function,
Page 114
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
INSTANCEMAP::iterator i;
for (i=mapping_.begin(); i != mapping_.end(); i++) {
sprintf (buffer, " %d", i->first);
str += buffer;
}
return str;
}
Our object maintains a list of all instances, so the dump() method produces a list of instances
by address. We could have made the default implementation of dump() do much more, like
display the object details of each instance, but this goes beyond the scope of
apObjectInfo<>. If we didn't restrict ourselves to a minimal representation of this object, we
would have added some means of iterating on all the instances of the object. This would give
the client the ability to access any instance. Besides, dump() is more of a debugging aid to
monitor how many instances exist.
We should be spending our time deciding what process() does. When we first considered
process(), we didn't know exactly what the function should do, so we wrote a stub function
instead:
As the stub function indicates, a command string is sent to process() for processing and any
result is returned as a string. Both the argument and return type are strings, because we
want to keep the interface very generic. Using strings comes at a price, because the
command string must be parsed each time to decide what to do.
Page 115
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
} apToken;
class apStringTools
{
public:
...
static apToken sParse (const std::string& str,
const std::string& term = sStandardTerm);
return result;
}
C OMMAND PARSER
With this function, we can now write a simple command processor to parse our string. We
support three functions.
execute <string> calls the process() method for each instance and passes it the
remaining string.
Page 116
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
A process() method to support these commands with simple if statements is shown here.
INSTANCEMAP::iterator i;
std::string result; // Our result string
char buffer[16]; // Buffer for sprintf()
if (token.parsed == "list") {
// "list" Returns list of all instance addresses
for (i=mapping_.begin(); i != mapping_.end(); ++i) {
sprintf (buffer, "%x ", i->first);
result += buffer;
}
}
else if (token.parsed == "execute") {
// "execute <command>" Send the remaining string to instances
for (i=mapping_.begin(); i != mapping_.end(); ++i) {
T* obj = reinterpret_cast<T*>(i->first);
result += obj->process (token.remainder);
result += " ";
}
}
else if (token.parsed == "to") {
// "to <instance> <command>" Send remaining string to a
// specific instance. Matching is by string because the list
// command returns a list of strings
apToken instance = apStringTools::sParse (token.remainder);
for (i=mapping_.begin(); i != mapping_.end(); ++i) {
sprintf (buffer, "%x", i->first);
if (instance.parsed == buffer) {
T* obj = reinterpret_cast<T*>(i->first);
result += obj->process (instance.remainder);
}
}
}
else {
// Unknown command. Don't do anything
}
return result;
}
If you had a large number of commands to support, you could make the parsing faster by
using some shortcuts. For example, we can rewrite the comparison portion of our previous
example to group commands by their length, as shown:
switch (token.parsed.size()) {
case 2:
if (token.parsed == "to") {
// 'to' processing
}
break;
Page 117
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
case 4:
if (token.parsed == "list") {
// 'list' processing
}
break;
case 7:
if (token.parsed == "execute") {
// 'execute' processing
}
break;
default:
}
This example may look unimpressive, but imagine what would happen if you had 50 commands
and implemented process() with this style. Instead of performing 50 comparisons in the worst
case, you would probably perform no more than 10 to 15.
Perhaps a better solution when you have many commands to process is to use a std::map
object to map the command name (a string) to a function or object that handles the request.
This solution is beyond the scope of this book, but the idea is to define the following inside
apObjectInfo<>:
Before going any further, let us review how using templates has drastically improved the
design. Before templates, we might have used macros to construct the equivalent of the
apObjectInfo<> object. Macros are workable for short definitions, but for anything longer
than a few lines, they can be difficult to follow and maintain.
For example, a macro to declare, but not define the object, is as shown.
#define CREATEINSTANCECLASS(classname) \
class apObjectInfo_##classname : public apObjectInfoBase \
{ \
public: \
static apObjectInfo_##classname gOnly (); \
virtual std::string process (const std::string& command); \
virtual std::string dump (); \
private: \
static apObjectInfo_##classname* sOnly_; \
apObjectInfo_##classname (); \
};
To create an object similar to apObjectInfo<T>, we do as follows:
CREATEINSTANCECLASS(T);
where T is the name of the object of which you want to track the instances. This creates an
object apObjectInfo_T. Another macro is still needed to supply the definition of the object. If
templates did not exist, we would still exploit macros to avoid duplicate code. It is our
experience that writing a macro for the first time is not too difficult, especially if you already
have the class design. The real trouble begins when you later try to extend or correct
problems with it. This happens because it is hard to visualize the function when it is written in
macro format. The merging operator inside macros (i.e., ##) also reduces the readability a
great deal.
Page 118
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
OBJEC T REGISTRY
We have two more pieces to go. apObjectInfo<> contains information about each instance
of a particular object. We still need a way to keep track of each apObjectInfo<> in
existence. This is also a singleton object, which represents the overall object registry:
class apObjectMgr
{
public:
typedef std::map<std::string, apObjectInfoBase*> OBJMAP;
private:
static apObjectMgr* sOnly_;
apObjectMgr ();
debugMessage() is a general purpose function that you can use to write to the cdebug
stream. It does nothing more than write a header (which is particular to an object type) and a
message, as shown here.
apObjectInfo<T>::apObjectInfo<T> ()
{
// Setup our object name. This is compiler and platform
// dependent so this function can be modified to create
// a more uniform string
name_ = typeid(T).name();
Page 119
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Since apObjectInfo<> is a singleton object, this constructor only runs once. We use
typeid() to specify the object name to use in the registry. Keep in mind that typeid() is
compiler and platform dependent so the value of this string may surprise you. For example: in
Microsoft Visual Studio on a Windows platform, using typeid() for an object, apTest, returns
class apTest. On a FreeBSD system using gcc (version 2.95.3), it returns 6apTest. For our
purposes this is fine because the string is unique on any platform.
There is no subtract() method in this object, because once an object is first constructed, it
always stays in mapping_. Our singleton object is only destroyed when the application closes,
so there is no need to remove an item from our map.
find() does nothing more than return a specific pointer to an apObjectInfo<> object, as
shown here.
Now that we have a top-level registry object, as well as one that can track all the instances
of an object, we need to add some functionality to the objects themselves. We do this by
creating a base class for all objects that need debugging. By writing it as a template class,
the compiler will enforce the data types for us. We will keep the interface very simple.
Page 120
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
char buffer[32];
sprintf (buffer, " (0x%0x): ", obj);
std::string h = apObjectInfo<T>::gOnly().name();
h += buffer;
return h;
}
process() is the method most objects should override. When we showed a sample command
processor earlier, we defined an execute command to send a string to all object instances.
process() is the method that will receive that string, do some processing, and return a result
string. process() is not a pure virtual function, and will return an empty string if not
overridden. You can see an example where process() is overridden to output a debugging
string in the unit test for debugging on the CD-ROM.
Our debugging registry is somewhat heavy, in that there are many template objects that are
created to manage the interface. This registry is not designed to be used by all objects in a
Page 121
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
system. Rather, it is suitable for higher-level objects that are complex or contain a lot of
information, such as an image object. Often there will be from just a couple to a few hundred
instances of an image in existence at any one time. All told, there may be no more than ten to
twenty objects that require this type of interface. You certainly do not want or need this
interface for a simple class like:
class apSum
{
public:
apSum : sum_ (0) {}
void sum (double d) { sum_ += d;}
double sum () const { return sum_;}
private:
double sum_;
};
[ Team LiB ]
Page 122
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
4.4 Summary
In this chapter, we explored other aspects that need to be considered for our final design.
They included such coding guidelines as naming conventions, comment styles, indentation
style, and header file rules. These are often passionately debated issues, and we offered
practical advice to selecting a workable set of guidelines. Next, we discussed reusability and
what that means for the design of objects. We also touched briefly on some of the functional
and testing issues that arise with reusable components.
And finally, we spent much time discussing a debugging environment that could be integrated
into the design of the framework. It offers many advantages, the main one being that it is
present in production releases, but requires little overhead. This environment included a
generalized debugging stream that outputs debugging information, as controlled by a
separate apDebug object, to various sinks (or destinations). During our design and
implementation, we explored STL components that were useful in the solution. We extended
the environment to allow remote access to objects through an object registry, which is
capable of handling many objects.
In Chapter 5, we take a look at the system-level issues that may affect the final image
framework design. These issues include multithreaded and multiprocess designs, as well as
strategies for using exceptions. We also take the time to explore run-time and compile-time
issues, and their effects on performance as it relates to the design. Finally, we touch on
adding support for future expansion into non-English and double-byte languages.
[ Team LiB ]
Page 123
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
C++ Concepts
Multithreaded Design
Multiprocess Design
Template Specialization
Internationalization
Heap Manager
In this chapter we discuss issues that influence high-level software design. In addition to
covering C++ issues like exception handling and virtual functions, we discuss such system
considerations as multithreading and internationalization
[ Team LiB ]
Page 124
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
#include <iostream>
int main ()
{
std::cout << "Hello World" << std::endl;
return 0;
}
Each process is insulated from all others, even in the case of multiple instances of the same
application. As an application designer, you typically do not need to concern yourself with the
details of what other applications or even what the operating system is doing. However, this
does not imply that different processes cannot work in unison to perform a task. In this
section, we explore how partitioning a problem into many separate pieces can create a solid
design that helps decrease the development time and increase the robustness of your
application.
Unlike processes, threads are not insulated from each other. A process can consist of one or
more threads that in many ways behave like separate processes. You can write a thread as
though it exists by itself, but the operating system does not treat it this way. All threads in a
process share the same memory space, which has both positive and negative effects. This
means that the developer needs to decide when threads can and should be used to improve
performance and reliability. Even if the operating system does not support threads natively, it
is possible to use third-party packages to get this functionality.
The techniques we discuss here are also applicable to embedded systems. The dynamics of
embedded systems are different from those of full-blown operating systems, such as Microsoft
Windows or the many UNIX variations. Most embedded systems are deterministic, meaning
they have the ability to guarantee a certain response time or processing rate. They usually
support processes and threads. Embedded systems often have a very simple user interface, or
none at all. In addition, they often have limited memory and other resources. And,
significantly, they are designed to run indefinitely without requiring rebooting.
To use threads and processes successfully, you must be able to communicate between them,
which is referred to as interprocess communication. Although the functionality differs among
operating systems, we concern ourselves with the most important components:
Synchronizing access to common resources using threads (Section 5.1.2 on page 126)
In our discussion of these features, we focus on ways to improve reliability and decrease
development time. We only use features if they offer a clear advantage for commercial
software development. Consequently, we also talk about when these features should be
avoided.
5.1.1 Threads
Threads are one of the first elements to consider when designing an application. Many
applications lend themselves well to this mechanism, and threads are widely available on most
Page 125
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
platforms. Still, we must consider whether or not to incorporate threads into a design.
Debugging and correcting problems in a multithreaded application is usually more difficult than
in a non-threaded application.
EXAMPLE
Let's look at a completely hypothetical threaded application that is woefully
inadequate, but demonstrates our point:
void thread1 ()
{
while (true) {
processingStep1 ();
step = 1;
processingStep2 ();
step = 2;
resetProcessing ();
}
}
void thread2 ()
{
while (true) {
if (step != analyzed) {
switch (step) {
case 1:
analyzeStep1 ();
analyzed = 1;
break;
case 2:
analyzeStep2 ();
analyzed = 2;
break;
}
}
}
}
In this example, we create two functions, thread1() and thread2(), which run in
separate threads. Assume that when the application starts, these two functions
start executing. The first thread performs two different processing steps, resets
itself, and then performs these steps again. The second thread analyzes the results
from each processing step. When the application starts, thread1() will run and do
whatever processing is needed for step 1. thread2() will wait until the processing
is complete and will analyze it. This process continues with step 2, and then the
whole process repeats itself.
The first question you might ask is, "Will this application work?" The best answer we
have for you is that we have no idea. There is no explicit control over the threads.
It is up to the underlying system to define how and when these threads will
execute.
Threads are often written as functions that never end, because a thread usually
does not end. The thread's lifetime is the same as the application itself. This is why
we ignore the issues surrounding starting and stopping threads in our example.
Page 126
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Full-blown operating systems, such as Microsoft Windows and many UNIX versions, offer a
fully preemptive multithreaded environment. The operating system takes care of how and
when each thread receives a slice of processing time. In other words, thread1() and
thread2() can be written with very little knowledge about what the other thread function is
doing.
At the other end of the spectrum are cooperative multithreaded environments. In this
environment, you must control when one thread stops and another thread runs. While this
offers complete control over the switching from one thread to another, it also means that, if
poorly written, one thread can consume 100% of the processor time. Cooperative
multithreading is often found in small embedded systems or as third-party libraries for
platforms that have no native multithreading.
If you have the choice, use the preemptive model to ensure that deadlocks are minimized. A
deadlock is a situation where no thread can continue executing, causing the system to
effectively hang. Besides, you can always use thread priorities to make a preemptive
multithreaded system behave like a cooperative system. On some systems, a high priority
thread simply gets more processing time than lower priority threads. On other systems, a
lower priority thread gets no processing time while a higher priority thread is running.
POSIX
The number of threading APIs has fortunately become much smaller in recent years. IEEE
Standard 1003.1 (also known as POSIX) is available and defines a complete interface to
thread functionality, including control and synchronization. The specification is available online
(currently located at http://www.opengroup.org/onlinepubs/007904975/toc.htm). On most
platforms with native thread support, a POSIX interface is available (on Win32 platforms, for
example, a fairly complete interface can be found at
http://sources.redhat.com/pthreads-win32).
POSIX is complicated and somewhat intimidating. In keeping with our desire to keep things
simple, we wrap the C interface in a simple class to handle our threading needs. If this simple
interface is insufficient for your needs, you can extend it as necessary. We are not offering
this sample as a class that can be used in all circumstances, but you may be surprised at how
useful it is. We present two versions of this object: one for POSIX for UNIX platforms, and one
for Win32 for Microsoft platforms. We keep our operating system-specific versions in different
directories that are accessed by a top-level include file. The file hierarchy is:
/include
thread.h
/win32
thread.h
/unix
thread.h
The top-level version of thread.h loads the implementation-specific version of thread.h, or
defines a default implementation of apThread. Although there is a pthreads compatibility
library available on Microsoft Windows, we have chosen to use native Win32 calls because it is
a simpler interface and is only going to be used in the Win32 environment. The Microsoft
Win32 version of thread.h is as shown.
class apThread
{
public:
apThread () : threadid_ (-1) {}
~apThread () {if (threadid_ != -1) stop();}
Page 127
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
bool start ()
{
threadid_ = _beginthreadex (NULL, 0, thread_, this,
CREATE_SUSPENDED,
(unsigned int*) &threadid_);
if (threadid_ != 0)
ResumeThread ((HANDLE)threadid_);
return (threadid_ != 0);
}
// Start the thread running
bool stop ()
{
TerminateThread ((HANDLE) threadid_, -1);
return true;
}
// Stop the thread
protected:
int threadid_;
apThread is very easy to use. You can derive an object from apThread and then override the
thread() member function. This function will execute when start() is called and continue
executing until the application is finished, or the stop() method is called. The default
implementation has the following behavior:
Page 128
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Obviously this is not the desired behavior, but without thread support you cannot expect the
application to run properly. We originally thought about defining start() like this:
The stop() method should be used very sparingly. Thread termination is very abrupt and can
easily cause locking issues and other resource leakage. You should always provide a more
graceful way to terminate your threads, such as using a flag to specify when a thread can
safely shut down. The full UNIX and Win32 implementations can be found on the CD-ROM.
Let's look at the start() and stop() methods for UNIX and Win32 implementations.
bool start () {
threadid_ = _beginthreadex (NULL, 0, thread_, this,
CREATE_SUSPENDED,
(unsigned int*) &threadid_);
if (threadid_ != 0)
ResumeThread ((HANDLE)threadid_);
return (threadid_ != 0);
}
bool stop () {
TerminateThread ((HANDLE) threadid_, -1);
return true;
}
protected:
int threadid_;
UNIX
bool start () {
int status;
Page 129
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
bool stop () {
pthread_cancel (threadid_);
return true;
}
protected:
pthread_t threadid_;
void thread1::thread ()
{
for (int i=0; i<10; i++) {
std::cout << threadid() << ": " << i << std::endl;
sleep (100);
}
}
int main()
{
thread1 thread1Inst, thread2Inst;
thread1Inst.start ();
thread2Inst.start ();
thread1Inst.wait ();
thread2Inst.wait ();
return 0;
}
Two worker threads are created: each prints ten lines of output and then exits. Beyond that,
it is difficult to predict what will actually be output. In addition, what will be output also
depends upon the platform on which it runs. On Microsoft Windows, for example, the output is
very orderly, as shown:
2020: 0
2024: 0
2020: 1
2024: 1
2020: 2
2024: 2
2020: 3
2024: 3
2020: 4
2024: 4
2020: 5
2024: 5
2020: 6
Page 130
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
2024: 6
2020: 7
2024: 7
2020: 8
2024: 8
2020: 9
2024: 9
However, you can't rely upon the behavior of the operating system to control the output. For
example, if sleep(100) is removed from the thread() definition, the output changes to be as
shown:
2020: 0
2020: 1
2024: 0
2024: 1
2024: 2
2024: 3
20242020: 2
2020: 3
2020: 4
2020: 5
2020: 4
2024: 5
2024: 6
2024: 7
2024: 6
2020: 7
2020: 8
2020: 9
: 8
2024: 9
When the operating system decides to switch from one thread to another, it is usually after a
thread has consumed a certain amount of processing time. This can happen any time,
including in the middle of executing a line of code. If each thread was completely independent
of the others, this would not be an issue. But even in our simple example, both threads use a
common resource: they both generate output to the console.
This example highlights the primary challenge when using threads. It is imperative that access
to shared resources be carefully controlled. A shared resource can be more than just an
input/output stream or file. It might be something as simple as a global variable that can be
accessed by many threads. As the number of threads increases, the complexity of managing
them increases as well. You might wonder why we always seem to encapsulate a functionality
like threads into its own class. After all, if your application only ever runs on a single platform,
you might consider using the native API calls directly. But encapsulation does serve another
important purpose. In addition to ensuring that all users of our thread object get the same
behavior, encapsulation allows us to use our debugging resources to observe what is
happening. Most thread problems occur with missing or incorrect synchronization, an issue we
will talk about shortly. But another common problem occurs when the thread itself goes out of
scope and closes. Consider this example:
Page 131
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
int main()
{
thread thread1;
thread1.start ();
{
thread thread2;
thread2.start ();
}
...
}
thread2 goes out of scope when the closing brace is reached, causing the thread to stop
running. Before you say that you would never write code like this, you need to realize how
easy it is to write code that results in such behavior. For example:
One or more apThread objects is controlled by another object that goes out of scope.
An exception is thrown, and the apThread object goes out of scope during stack
unwinding.
One solution to the scoping problem is to allocate apThread objects on the heap with
operator new. While you can be very careful not to delete heap-based objects prematurely,
remembering to delete them at all is another matter. It is not uncommon for bad coding
practices like this to surface in multithreaded code. Single-threaded applications often rely on
the operating system to cleanly shut down an application, and therefore this issue is ignored.
These practices do not work with multithreaded applications unless the lifetime of all threads
is the same as that of the application itself.
This demonstrates yet another benefit of encapsulating a thread in apThread. Your apThread
-derived object can control the object lifetime of other components that exist only to serve a
thread. Although you can do this inside the constructor and destructor of your derived object,
we recommend overriding start() and stop() and taking care of it there. Doing so in these
functions delays the construction and destruction of other components until they are needed,
rather than when the apThread object is constructed.
We recommend that Singleton objects be used for threads that persist for the entire lifetime
of an application. Construction happens when the object is first referenced, presumably when
the application begins execution.
Use Singleton objects for threads that persist for the entire lifetime of
the application.
Page 132
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
thread::gOnly().start ();
it causes the thread to be constructed and begin execution.
Applications that use threads, especially those that frequently create and destroy them,
should be watched closely to detect problems during development and testing. You must make
sure that global resources, such as heap, are properly allocated and freed by threads to
prevent serious problems later. Heap leakage is one of the easier problems to find, but it
usually takes more time to fix. You are far better off assuming that your thread has a memory
problem than assuming that it does not. If you take this stance during the design, you will be
very sensitive to memory allocation and deallocation. If your design calls for many threads to
execute the same piece of code, you should account for this in your unit tests by creating at
least as many threads as you expect to use in the actual application.
If many threads are required for a piece of code, make sure your unit
tests include at least as many threads as you expect in the actual
application.
The execution of many threads consumes more than just heap memory. Other resources, both
system- and user-defined, must be monitored to make sure they are properly allocated and
freed. This is easy if you encapsulate your resources inside a Singleton object to manage
them. Besides the obvious advantage of having a single point where resources are allocated
and freed, the resource manager can keep track of how many, and to whom, each resource is
allocated. If all the resources become exhausted, the list maintained by the resource manager
can be examined to track down the culprit.
void thread1::thread ()
{
for (int i=0; i<10; i++) {
std::cout << threadid() << ": " << i << std::endl;
sleep (100);
}
}
int main()
{
thread1 thread1Inst, thread2Inst;
thread1Inst.start ();
thread2Inst.start ();
thread1Inst.wait ();
thread2Inst.wait ();
return 0;
}
This example creates two threads that both write to std::cout. The output from this
Page 133
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
example cannot be predicted because thread execution is dependent upon the operating
system. The line that outputs information to the console:
Shared resources can also be less tangible things like bandwidth, the amount of information
your application can send or receive per unit of time. For example, many threads can
simultaneously request information from sockets, such as fetching web pages or other
information. Most operating systems can manage hundreds or thousands of simultaneous
connections and will patiently wait for information to arrive. The management is not the
problem, but the timely receipt of information is. If the machine running your application needs
a constant stream of information, you may find that you are trying to access more information
than you have available bandwidth to receive.
Before we discuss how to use synchronization to control access to shared resources, let us
discuss something you should never (or almost never) do. Most operating systems can give an
application almost complete control of a system. For example, a process can be made to
consume most of the processor time, while other processes are made to wait. A single thread
can be made to run such that no other thread will execute. This is extremely dangerous. If
you are considering doing this because your existing machine is not fast enough, you probably
should consider running on a faster machine. After all, if a machine can only execute N
instructions per second and you must run N+1 instructions, no amount of optimization will help
you. More likely, the current design is lacking the techniques to make the pieces interact
properly.
Threads can be made to interact nicely with each other by synchronizing access to any
resources that are shared. Most operating systems support many types of synchronization
objects, but we will only discuss one of them. The big difference among most synchronization
methods is their scope. By scope, we mean whether shared resources can be accessed by
different threads in the same process, different processes, or even different machines.
Remember, the larger the scope, the more overhead that must be paid in order to use it. By
restricting ourselves to communication between threads, we can add synchronization with
very little cost.
AP LOC K
As we did when we presented threads, we will show two implementations of apLock: POSIX
for UNIX platforms and Win32 for Microsoft platforms. The file hierarchy looks the same:
/include
lock.h
/win32
lock.h
/unix
lock.h
The locking metaphor is very descriptive of what this object does. When one thread obtains a
lock, all other threads that wish to obtain the lock must wait for it to be freed. As with
Page 134
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
apThread, the top-level version of lock.h loads the appropriate version of lock.h, or a
default version if necessary.
class apLock
{
public:
apLock () {}
~apLock () {}
#endif
One apLock object is constructed for each resource whose access must be limited to one
thread at a time. The default version always returns immediately as though the lock/unlock
operation were successful. We can modify our previous example to include locking by creating
a global object to control access to the console. To work correctly, the lock must be obtained
before something is written to the console, and then unlocked when finished.
apLock consoleLock;
class thread1 : public apThread
{
void thread ();
};
void thread1::thread ()
{
for (int i=0; i<10; i++) {
consoleLock.lock ();
std::cout << threadid() << ": " << i << std::endl;
consoleLock.unlock ();
sleep (100);
}
}
int main()
{
thread1 thread1Inst, thread2Inst;
thread1Inst.start ();
thread2Inst.start ();
thread1Inst.wait ();
thread2Inst.wait ();
return 0;
}
The differences from our previous example are:
Page 135
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
apLock consoleLock;
...
consoleLock.lock ();
std::cout << threadid() << ": " << i << std::endl;
consoleLock.unlock ();
...
When this snippet of code executes, you will no longer see lines of output broken by output
from another thread. It will produce output similar to this:
2020: 0
2024: 0
2020: 1
2024: 1
2024: 2
2020: 2
2024: 3
2020: 3
2020: 4
2024: 4
2020: 5
2024: 5
2024: 6
2020: 6
2024: 7
2020: 7
2020: 8
2024: 8
2020: 9
2024: 9
If this were actual production code, we never would have defined consoleLock as a global
object. We probably would not use a Singleton object either, because consoleLock is used
only for console I/O. The best solution is to define an apLock object in a class that manages
console I/O. For instance, we could modify our debugging stream interface (see Section 4.3.1
on page 94) to include a lock so that the cdebug stream is synchronized between threads.
To simplify the locking and unlocking required to use consoleLock, we can take advantage of
a technique called Resource Acquisition Is Initialization, also referred to as RAII. To use this
method, we define a simple wrapper object that guarantees the lock will be freed when the
object is destroyed. We create a new object, apConsoleLocker, to manage and own the lock
as shown.
class apConsoleLocker
{
public:
apConsoleLocker () { consoleLock_.lock();}
~apConsoleLocker () { consoleLock_.unlock();}
private:
static apLock consoleLock_;
...
Page 136
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
{
apConsoleLocker lock;
std::cout << threadid() << : << i << std::endl;
}
...
The use of braces is very important, as the destruction of apConsoleLocker is what releases
the lock so that other threads can use the resource that the lock controls. If you do not want
the lifetime of your apConsoleLocker object to match that of the function it is defined in,
you can use braces to control its lifetime.
The full UNIX and Win32 implementations are found on the CD-ROM, but the important
sections are shown here. For our UNIX implementation with pthreads, we use a mutex object
(named because it coordinates mutually exclusive access to a resource). Since only one
thread at a time can own a mutex, this mechanism solves our problem nicely. Microsoft
Windows has mutex support as well, but it also allows them to be used between processes. A
slightly faster solution is to use a critical section, which performs the same job as a mutex,
but can only be used within the same process.
UNIX
class apLock
{
public:
apLock () { pthread_mutex_init (&lock_, NULL);}
~apLock () { pthread_mutex_destroy (&lock_);}
private:
mutable pthread_mutex_t lock_;
};
class apLock
{
public:
apLock () { InitializeCriticalSection (&lock_); }
~apLock () { DeleteCriticalSection (&lock_);}
private:
mutable CRITICAL_SECTION lock_;
};
We made lock() and unlock() into const methods so that they can be used without
Page 137
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
}
void thread2 ()
{
...
lock2.lock ();
// Do something else
lock1.lock ();
...
}
The following conditions will cause a deadlock:
Both of these threads are now deadlocked and will never exit. While it is possible to write a
lock() method that will time out if the lock cannot be obtained, you are still faced with an
undesired situation (for pthreads, see pthread_mutex_trylock(); for Win32, see
TryEnterCriticalSection() or WaitForSingleObject()). A better solution is to avoid
deadlock conditions completely. Don't be fooled into thinking that you need many threads and
many synchronization objects before you need to worry about deadlocks. If one thread
forgets to release a synchronization object, you can easily face a partial deadlock when
another thread waits for that lock.
You will decrease the chances of a deadlock condition if you minimize the amount of code that
must execute while you possess a lock. Consider these two examples:
Page 138
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
In Example 1, the console is locked while data is computed and written to the stream. In
Example 2, the output data is computed first, then the lock is obtained for the shortest
amount of time possible. Although this example is trivial, it does demonstrate how you can
make simple changes to improve the dynamics of your application.
Use locking around the smallest section of code possible. This will
improve readability and reduce the chances of deadlock conditions.
It may not be enough to simply reduce the chances for deadlocks; rather, using a simple rule
can ensure that deadlocks are impossible. If each thread always locks items in the same order
(such as, first lock A, then B, then C, ...), deadlocks can be completely avoided. Of course,
such a strategy may involve more extra work than you are willing to do. See [Nichols97].
Now that we understand the issues of locking and unlocking, we can show a generic interface
to the RAII technique. There are two steps: first we construct a global apLock object (see
page 128) to control access to a resource; then, we define a class, apLocker, that locks the
lock when it is constructed and unlocks the lock when it is destroyed. apLocker is shown
here.
class apLocker
{
public:
apLocker (apLock& lock) : lock_ (lock) { lock_.lock();}
~apLocker () { lock_.unlock();}
private:
apLock& lock_;
Page 139
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
application will use it. Although it is possible for multiple threads to create this situation, it is
unlikely to occur. In this particular example, a bug is created if addRef() is called after
subRef() has already decremented ref_ and deletes the object. This is no different than an
application that attempts to use an object that goes out of scope. The problem is not missing
locking; it is poor design. If an object must persist beyond the scope of a thread, it should be
created and owned by a different thread that will not go out of scope. Please keep in mind
that the Standard Template Library (STL) is not guaranteed to be thread safe.
5.1.3 Processes
Depending upon the application, a problem can sometimes be divided into separate distinct
pieces. Before all these pieces are committed to being separate threads, you should also
consider if they should be separate processes. A process has its own address space and is
completely insulated from other processes. In a multithreaded application, for example, an
error in a thread can cause an entire application to shut down. However, an error in one
process will not cause another process to shut down.
To help you decide if you should be adding a thread or another process to your application,
you should study what resources are needed and whether the application needs any
information in a timely fashion. Choose threads when there is a tight coupling of resources,
especially when timing is important. It is less clear-cut when there is a loose coupling between
functions. For example, suppose an application generates a large volume of data by servicing
requests by means of sockets or the Internet. Summary information is then written to a log
file for each request. Every few minutes some statistics must be computed based on these
results. If we implement this using only threads, it can be done without much difficulty, as
follows:
Periodically, a separate thread runs to compute the actual statistics. This thread
copies the existing statistics and resets them, so that summary information can be
built up for the next interval.
Let's see how this changes when we use separate processes for the implementation:
Periodically, the summary file is renamed so that new summary records are written to a
different file.
Another process detects this file rollover, extracts information from each summary
record, and computes the necessary statistics.
This solution is clearly more work, but does it result in a more reliable solution? Although we
left out many details, the answer is probably yes. There are two distinct pieces here: a
request processor and a log analyzer, and they have separate requirements. We haven't said
anything about throughput, but it is possible that requests for an imaging application must be
processed at the rate of 50 or more requests per second. With other types of application,
rates can be as high as hundreds or thousands of requests per second. The generation of
statistics happens at a much slower rate; from every few minutes to every few hours. By
writing the summary information to a file, we can share the necessary information so that
these statistics can be computed by a separate process.
Now let us consider what happens when an error condition occurs. If we used threads to
implement our solution, an error in one thread can cause the entire application to shut down.
Any incremental calculations will be lost and the application must be restarted. If we use
Page 140
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
separate processes to implement our solution, a failure of one process will not interfere with
the other. The operating system will happily continue executing one of the processes, even
though the other has stopped running. If the request processor dies, no data will be written
to the summary file until it begins running again. The statistics process can still analyze this
information and generate reports. If the statistics process dies, the requests will be processed
and summary information will build up in one or more files for later processing.
Another advantage of using processes to implement this solution is the well-defined interface
between the two pieces. There are only so many ways that information can be transferred
from one process to another. And in each of them, you transfer a discrete amount of
information. Whether you are using the file system, sockets, or pipes, one process can
transmit information to another process. This destination can also be on another machine
entirely, but that is beyond the scope of this book. The point is that a rigid interface develops
between the processes. If more information must be exchanged at a future point, this
interface will be modified. With threads, there is a tendency for these interfaces to get
blurred, because exchanging information is as easy as setting a variable.
[ Team LiB ]
Page 141
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
BRIEF OVERVIEW
int main()
{
try {
// Your application goes here
}
catch (...) {
std::cerr << "Exception caught" << std::endl;
return 1;
}
return 0;
}
By placing your application inside a try() block, the listed exceptions can be caught. In this
case, all exceptions are caught. This is not uncommon inside main() because it acts as the
last chance to catch an exception. If no exception handler is defined, std::terminate() is
called, which then calls abort(). You can use throw to generate an exception, using a
built-in class or one of your own, as shown:
int main()
{
try {
// Your application goes here
}
catch (std::exception& ex) {
std::cerr << "Standard Library exception caught: " << ex.what()
<< std::endl;
return 1;
}
catch (...) {
std::cerr << "Exception caught" << std::endl;
return 1;
}
return 0;
}
The order in which you list catch() blocks is very significant, because the first matching block
will execute. In this example, we are catching any object of type std::exception we also
catch any exception derived from this class. If the throw statement above is executed, the
following will be displayed to cerr:
Page 142
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
In general, you can generate exceptions or catch them whenever you want. However, the use
of exceptions in constructors and destructors requires care to avoid heap or resource leakage,
or unexpected program termination.
The function of a constructor is to create and initialize an object. Since a constructor does
not return a value, you may have wondered what your application can do when an error is
detected during construction. Because constructors are not intended to fail, throwing an
exception is a reasonable strategy. This strategy allows you to place code at a higher level,
where it can best be decided how to handle the error. What happens is that, even though the
object may not be completely constructed, the destructor is called for all class members that
were fully initialized before the exception is thrown. As a result, the object is brought back to
the state it was in before the object was constructed.
To prevent heap or resource leakages when an exception is thrown inside a constructor, you
should use the Resource Acquisition Is Initialization (RAII) technique we discussed on page
130. By wrapping a resource object inside another object, it is guaranteed that the resource's
destructor will be called if the resource object was constructed before an exception is thrown.
The resource's destructor will release the resource and return the system to the state it was
in before the original constructor was ever called. If you do not use this technique, you can't
guarantee that an exception thrown from within a constructor will not cause a heap or
resource leak. For detailed information, see [Stroustrup00].
Generating exceptions within a destructor must be avoided. To see why, let's consider what
happens when an exception is thrown. A catch handler is written to handle an exception and
continue execution. Objects constructed inside the try() block are destroyed when the
exception is caught. If any of these destructors were to generate an exception of their own,
the system would be in a hopeless state because there is no way to know what the proper
course of action should be. If this condition actually happens, the application calls
terminate(). Unless you write a custom termination handler, abort() is called and the
application shuts down. As a matter of practice, exceptions should never be called from within
a destructor unless you catch them before they propagate outside the destructor.
Regardless of how much or how little you use exception handling, you need to take some
steps to catch any errors before they cause your application to terminate. Even if your code
does not use exception handling, the standard library does. At the very least your application
needs to have a top-level catch handler, as we showed above. We recommend two additions
to your top-level catch handler. The first is to catch specific types of errors before your
catch-all handler does. Your application should make every attempt to restart itself, or
gracefully fail, before you give up and terminate the application. The second addition is to
include another catch handler as a backup to the first. Adding too much logic in the first
catch handler can actually trigger another exception. The backup catch handler should
attempt to write error information to an error log or console, exit, and then if possible restart
the application.
To get in the habit of having a top-level catch handler, you should put your application code
in a function other than main(). This will make it easier to add an exception handling scheme
to suit your needs, as shown.
#include "debugstream.h"
Page 143
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
int catchMain()
{
int retval = 0;
bool running = true;
int restart = 0; // Restart count
while (running) {
try {
// Run the application
retval = yourMain (restart);
}
catch (std::exception& ex) {
cdebug << "catchMain: Standard Library exception caught: "
<< ex.what() << ". Restarting ..." << std::endl;
restart++;
// Add code to decide if we should fail instead of restarting
}
catch (...) {
cdebug << "catchMain: Unknown exception caught" << std::endl;
retval = 1;
running = false;
}
}
cdebug << "catchMain: Stopping with exit code " << retval
<< std::endl;
return retval;
}
int main()
{
// Set up our debug stream
debugstream.sink (apDebugSinkConsole::sOnly);
apDebugSinkConsole::sOnly.showHeader (true);
The call to yourMain() is wrapped in a try block. We have shown two catch statements:
one for standard library exceptions, and a catch-all for any others. You can add catch
statements for other categories of errors, especially for exceptions that you define. In our
example, we restart the application after any standard library exception is caught. Please
keep in mind that global and static objects are not reinitialized after an exception is caught
and yourMain() is called again. You must explicitly reinitialize any global and static objects to
prevent subtle bugs from appearing in subsequent runs of yourMain(). We could have defined
the variable restart to be a bool, but then the application would not know how many times
the application was restarted. By using an int, the application can decide what should
happen if the application is restarted too many times. For example, if we define yourMain()
to be:
Page 144
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Before you think the design issues of a top-level catch handler are resolved, you must
consider what happens if an exception is caught in catchMain(), which throws an exception.
In our example, the catch handlers are short and simple. In reality they may be much more
complicated, and we must assume that exceptions might be thrown within them. We
recommend adding a second level of exception handling by splitting catchMain() into two
pieces, as shown:
int primaryMain()
{
int retval = 0;
bool running = true;
int restart = 0; // Restart count
while (running) {
try {
// Run the application
retval = yourMain (restart);
}
catch (std::exception& ex) {
cdebug << "primaryMain: Standard Library exception caught: "
<< ex.what() << ". Restarting ..." << std::endl;
restart++;
// Add code to decide if we should fail instead of restarting
}
catch (...) {
cdebug << "primaryMain: Unknown exception caught" << std::endl;
retval = 1;
running = false;
}
}
cdebug << "primaryMain: Stopping with exit code " << retval
<< std::endl;
return retval;
}
int catchMain()
{
try {
// Run the application
return primaryMain ();
}
catch (...) {
cdebug << "catchMain: Unknown exception caught" << std::endl;
return 1;
}
return 0;
}
We have moved the functionality that was in catchMain() into a new function
primaryMain(). The new catchMain() function calls primaryMain() but catches all errors.
No recovery is attempted. In this example, we write a string to cdebug and exit. In production
Page 145
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
code, it is best to save the state of the application in such a way that it uses as few
resources as possible. The only reason the catch handlers inside catchMain() are used is if
some kind of catastrophic failure has occurred.
What happens if a call to operator new fails because there is insufficient memory? A
user-defined error handler function will be called if one is defined. Otherwise,
std::bad_alloc() is called. A handler function is set by calling std::set_new_handler()
with a pointer to the function. If all you want to do is generate an error when there is
insufficient heap, the solution is simple:
void newHandler ()
{
cdebug << "memory allocation failure" << std::endl;
throw std::bad_alloc ();
}
int main ()
{
std::set_new_handler (newHandler);
...
}
Once newHandler() is established as our error handler, it will be called when any heap
allocation fails. The interesting thing about the error handler is that it will be called
continuously until the memory allocation succeeds, or the function throws an error. If you
were to write the function as:
void newHandler () {}
your application will be effectively dead if a heap allocation error occurs. Since no error is
thrown by the handler, the allocation will be attempted again once newHandler() is done,
which will simply fail again.
If you decide to use global handlers, such as std::set_new_handler(), you should only use
them in top-level functions. These handlers should not be defined in resusable pieces of
software because this prevents your application from using these features. If your global
handlers only deal with one or more special cases in your application, remember to call the
previous handler function to deal with any cases you do not process.
In our original handler there is a danger of triggering another heap allocation error when
writing to the I/O stream (using our cdebug stream). Issues like these should always be
considered when writing global handler functions. To solve this problem, we create a Singleton
object, apHeapMgr, to catch heap errors and also attempt some limited recovery.
We want to try and recover from a heap allocation error, and the best way to do this is to
reserve a block of heap memory when the application starts. The idea is that the handler
releases this heap memory when an error occurs, allowing the allocation to succeed. This
technique doesn't solve all heap-related problems, but if the size of the reserve buffer is large
enough, at least the application can continue running. And, if you can't continue running, it is
very important to notify users and give them a chance to save the state of the application
before another error occurs. Heap exhaustion does not just happen when there is no more
heap memory available. If heap becomes fragmented, it is very possible that an allocation of
the desired size is not possible. The apHeapMgr Singleton object (because we have only one
instance of it) is defined here.
class apHeapMgr
{
public:
Page 146
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
private:
static void newHandler ();
// Our memory handler when new cannot allocate the desired memory
apHeapMgr ();
The source code can be found on the CD-ROM, but the error handler function is repeated
here.
void apHeapMgr::newHandler ()
{
if (apHeapMgr::gOnly().nested_) {
// We have recursed into our handler which is a catastrophe
throw std::bad_alloc ();
}
apHeapMgr::gOnly().nested_ = true;
if (apHeapMgr::gOnly().state_) {
// Free our memory if we have not already done so
if (apHeapMgr::gOnly().reserve_) {
apHeapMgr::gOnly().releaseMemory ();
apHeapMgr::gOnly().nested_ = false;
return;
}
}
Page 147
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
We have not used exception specifications in our production code. The specifications are
instructions to the compiler regarding what exceptions a function can throw. For example:
The first issue is that specifications are not completely checked at compile time. If your code
throws an exception that is not listed in your specification, a function called
std::unexpected() is called. If you do not define your own function by way of
std::set_unexpected(), the application will terminate. That is an extremely harsh thing to
do, and it does not matter if you have a catch-all handler defined in your code. In addition,
you are somewhat limited in what your std::unexpected() function can do. For example, you
can certainly do this:
void myUnexpected ()
{
std::cout << "myUnexpected" << std::endl;
}
int main()
{
std::set_unexpected (myUnexpected);
try {
willthrow ();
}
catch (...) {
std::cout << "catch-all" << std::endl;
}
return 0;
}
Page 148
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
You might be surprised at what happens. In this example, a willthrow() function is called,
which is defined not to throw any errors. However, it does throw an exception. willthrow()
is wrapped in a try block to catch all errors. set_unexpected() is first called to install our
handler, myUnexpected(), to run instead of std::unexpected(). This example will do one of
the following things:
The behavior is compiler-specific and should be documented in the release notes of whatever
C++ compiler you are using. However, this discrepancy makes it unusable for multi-platform
products. Obviously, the desired solution is to have the compiler detect and flag all invalid
exception specifications. If this were the case, we would probably be using this feature. You
might expect that the catch-all will run, but this is contrary to what exception specifications
are all about. If the compiler can only determine at run-time that a compiler specification has
been violated, it must shut down the application or call your handler function first.
Can exceptions be thrown from within our handler function? The answer is yes, an error can
indeed be thrown. The problem is that this exception must be listed in every exception
specification that may be traversed until the exception is caught. If this is not done, the
application will call std::terminate(). For a large system, this amounts to adding an
exception specification to every function, unless you understand the dynamics of your
application perfectly. It is also important that you catch all exceptions within your destructor;
otherwise, std::terminate() will be called as well in this case.
Page 149
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Designing your own exception framework is not difficult because little information needs to be
carried with an exception. A string is usually sufficient to detail the specific error. To address
this issue of having two different exception frameworks (your own and standard library
exceptions), we derive our apException base class from std::exception. The base class
defines a virtual function, what(), that returns a string describing the error, as shown.
virtual ~apException () {}
protected:
std::string name_;
};
We don't recommend that you derive exceptions directly from std::exception because there
is no way to separate standard library exceptions from your own. Adding your own base class,
like apException, allows you to write code to only catch your own exceptions, such as:
try {
...
}
catch (apException& ex) {
// Our exception
}
catch (...) {
// Some other exception
}
But you can also write code, to catch your own exceptions, as well as standard library ones,
like this:
try {
...
}
catch (std::exception& ex) {
// Our exception or any standard library exception
}
catch (...) {
// Some other exception
}
Derive an object from apException for each type of exception you
expect to have in the application.
Page 150
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
For example, suppose your imaging application frequently verifies that the coordinates passed
to a function are correct. Using std::out_of_range error is not appropriate because this
error is not specific enough. We can derive an object, apBoundsException, to handle this
error condition, as shown here.
try {
throw apBoundsException ("Hello");
}
int sum ()
{
int total = 0;
for (int y=0; y<height(); y++) {
for (int x=0; x<width(); x++) {
total += getPixel (x, y);
...
}
}
return total;
Page 151
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
}
If you wrote sum(), would you include a try/catch block? And if you did, you might write one
of the following:
int sum1 ()
{
int total = 0;
try {
for (int y=0; y<height(); y++) {
for (int x=0; x<width(); x++) {
total += getPixel (x, y);
}
}
}
catch (apBoundsException& ex) {
cdebug << ex.what() << std::endl;
}
return total;
}
int sum2 ()
{
int total = 0;
for (int y=0; y<height(); y++) {
for (int x=0; x<width(); x++) {
try {
total += getPixel (x, y);
}
catch (apBoundsException& ex) {
cdebug << ex.what() << " at " << x << "," << y << std::endl;
}
}
}
return total;
}
sum1() wraps the entire function in a single try block. If an exception occurs, it prints a
message and returns the current total. sum2() wraps each call to getPixel(), which allows
the error message to be more clear. An exception will print a message and the loops will
continue to run.
sum2() is the most effective at trapping errors but is also the slowest, since the try block is
set up width()*height() times. As we have written these functions, an exception will never
be thrown because the loops never deliver invalid coordinates. In this case, it makes no sense
to incur the expense of a catch handler on every iteration.
Low-level functions are not good candidates because they are called frequently. In our
example, a better solution is to call the exception where it is generated, as shown here.
class apImage
{
...
char getPixel (int x, int y);
Page 152
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
int sum ()
//Description Compute the sum of all pixels in the image
//Returns Sum of all pixels.
};
You must document your exception strategy. In our example, it is very clear from the
comments what getPixel() does. It states that exceptions are not thrown, and that 0 is
returned if invalid coordinates are passed. sum() does not say anything about exceptions
because none are thrown; and the reason none are thrown is that sum() knows as much
information about the size of the image as getPixel() does.
For example, if we eliminate exception support from our previous example, it makes this code
much simpler, as shown here.
int apImage::sum ()
{
int total = 0;
for (int y=0; y<height(); y++) {
for (int x=0; x<width(); x++) {
total += getPixel (x, y);
}
}
return total;
}
In this case, it really makes sense to eliminate exception support in getPixel() since the
caller can easily determine if the coordinates are valid. In apImage::getPixel() above, the
function returns 0 if the coordinates are invalid. This is a silent check, meaning that no error is
generated if this is ever true. If getPixel() is used properly, this condition will never be
relevant.
C-style functions typically use the return value to indicate an error condition, as shown here.
Page 153
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
}
Is there anything wrong with doing this? Absolutely not. After all, if you use std::ifstream
to read from a file, it doesn't throw an exception if the file does not exist. Exceptions are
intended for exceptional cases; that is, cases that are not expected in the normal course of
execution. Trying to open a nonexistent file might be considered a fatal error in some
applications, but in general, an application should always test to see if the operation
succeeds. Before you call throw from a function, you should consider whether the error is
really an exception. We prefer to do things like this:
It seems like we are finding lots of reasons not to use exceptions. So, when should they be
used? If it is very unusual for a function to fail, you might consider using an exception. One of
the disadvantages of functions that return status is that you have to check the status. If you
have a large nesting of functions, this means that checks must be performed at every level.
Another good time to use exceptions is when a number of processing steps are treated as one
unit. For example, consider a machine vision system that receives a signal of some sort, takes
a picture (acquiring an image), and then processes and subsequently analyzes the image. If
you take a high-level look at these steps, each step must be completed before the next step
can begin, as follows:
bool inspect ()
{
try {
acquire ();
process ();
analyze ();
}
catch (apException& ex) {
... //Report error
return false;
}
...
return true;
}
Let's look at the process() step and what it might do:
void process ()
{
int i;
bool ok;
for (i=0; i<nFilterSteps; i++) {
ok = filterImage (i);
if (!ok) throw apException ();
}
Page 154
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
If you look at the inspect() function, we have the opposite behavior. We catch any errors
during processing and return the overall status as a bool. This makes sense because whoever
calls inspect() is interested in the result, and it is up to inspect() to catch and handle any
relevant exceptions. We did not try to catch all errors here because we are expecting only
errors derived from apException (or application-specific errors) to be thrown. If a different
exception is thrown while inspect() is running, it will be caught by a higher-level catch
handler; probably the one installed when the application started. For instance, what would
happen if a memory error is thrown (std::bad_alloc)? inspect() would not know how to
handle this condition, but some other piece of code will, and that is why we only catch those
errors we know how to handle.
Another appropriate time to use exceptions is when an unrecoverable error occurs deep inside
a program, such that it cannot be handled at that level. For example, if a low-level function
runs out of memory trying to allocate a temporary buffer, there is little that can be done. If
you do not catch this exception yourself, the application's top-level catch handler will catch
the error. However, your routine has the opportunity to log this error, perhaps to the console
or to a log file, and then generate a more specific error that describes what happened. When
you implement your top-level catch handler, reserve a memory buffer that can be released
when the user needs to save the state and restart the application. The information describing
the error can help the development team decide if this is indeed a memory error, or another
type of bug in the system.
If your development environment does not have the notion of debugging versus production
builds, you do not want to use assertions. An assertion will not only stop the system, it will
also display details about where the error occurred, as shown:
In debug builds, use assertions to enforce whatever restrictions you place on a function. If
the comments for a function describe certain requirements that must be true, assertions are
an excellent way to guarantee this. We can modify the getPixel() function we showed
earlier, as follows:
Page 155
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Keep in mind that the debugging version of the software differs in more ways from the
production version than just the addition of assertion statements. Often the debugging
software has different timing characteristics or other behavioral differences that can mask
bugs that may actually exist in the production version.
We offer you the following guidelines for using assertions in Figure 5.2
[ Team LiB ]
Page 156
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Unfortunately, compilers cannot perform all checks at compile time. Some must be deferred
until run time, such as casting references with dynamic_cast<> or exception specifications
(see page 142). The design of the application is also important. For example, virtual function
calls require one or more run-time lookups to determine which function to call.
Your designs will be more robust if you can shift as much burden as possible to your compiler.
You need to pay careful attention to any warnings that are issued because they can identify
design weaknesses or potential problems. Many developers consider all warnings as extraneous
messages. This practice, however, allows certain mistakes into the production code.
C OMPILER WARNINGS
Let's look at an example of some potentially dangerous code and the warnings it produces
using a variety of compilers:
In line 6:
int i = SIZE * N;
seems fine until you look at the values of SIZE and N. For 32-bit integers, SIZE*N does not
fit. To correct this problem you can use an unsigned int (appropriate in this example), or
you can use a larger signed quantity. This, however, will not fix the problem on many
embedded platforms where integers are only 16 bits. In line 10:
Page 157
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
if (l > ul)
there are problems when the value of l is negative. The compiler converts the signed quantity
to unsigned and then makes the comparison. For example, if we rewrite this example as:
long l = -1;
unsigned long ul = 0;
if (l > ul)
std::cout << l << " is greater than " << ul << std::endl;
you will see:
-1 is greater than 0
displayed on the console.
There are also two smaller issues in our example. In line 6, the variable i is set but never
referenced. Most often this occurs when other code that used the variable is removed during
the course of development. This condition can also indicate that the function is unfinished.
The second issue is at line 12; the file has no newline character at the end of it. Some
compilers will generate a warning if the last line of the file is an actual line of code.
So, what happens when this code is compiled? Let's take a look at the output of a few
different compilers.
GNU GC C
The GNU compiler, gcc, was tested on various platforms (Solaris, FreeBSD, and AIX) and
performance was identical on each one. With version 2.95.3 of gcc, no warnings are reported.
On version 3.0.3, gcc reports the following:
SGI IRIX
Page 158
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
All production code should compile cleanly, using the highest level of
detection. For gcc, use the -Wall command-line flag; for Microsoft
Visual C++, use /W4 (warning level 4).
Let's review each of the warning messages. We have a few comments about each type of
warning. For line 6:
int i = SIZE * N;
some compilers classified this as a warning. Other compilers, such as the native Sun compiler,
didn't list any warnings. Even though we are responsible for avoiding overflow errors, in our
opinion, all compilers should have flagged this problem as an error, so that it must be fixed
before the code will compile. For example, if i had been defined as a short or char, the odds
of a problem like this occurring are much greater.
if (l > ul)
some compilers generated a warning. If l must remain a signed quantity and ul an unsigned
quantity, then you need to cast one of the quantities so that they are of the same type.
Often this type of problem arises innocently because the signed variable is used only as a
counter, as shown in the following brief example:
The other warnings that were generated were obvious: adding a newline at the end of the file
and getting rid of the variable that was never referenced. Some compilers tend to be very
picky, with warnings that seem unimportant. But, even if they are unimportant on a relative
scale, they do clutter the output, making it more difficult to spot real problems. So, even if
Page 159
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
the warning seems trivial, such as a missing newline at the end of a file, you should make the
change so you will never have to see that warning again. Besides, if you follow our rule for
production code, all warnings have to be fixed before the code is considered for release.
C OMPILER ERRORS
Most errors found by the compiler are coding errors that must be fixed. The error messages
themselves, however, are sometimes non-specific messages like "syntax error." If the source
of the error is not obvious, you can start by following these simple steps.
1. Make sure your brackets are balanced. Indenting your code makes this easier to
diagnose.
3. Comment out offending lines and see if the error message goes away. Sometimes the
mistake is actually on a different line than that indicated by the error message.
In rare cases, some errors are caused by the compiler itself. Perhaps the best example of this
is how compilers handle templates. When templates were first introduced, they were used in
very well-defined ways. As compilers began to support features such as function templates,
inconsistencies started to appear. Let's look at a problem we encountered during prototyping
of the image framework:
int main()
{
apRGBTmpl<unsigned char> rgb1, rgb2;
apRGBTmpl<long> rgb3;
add2 (rgb1, rgb2, rgb3); // Fails to compile on win32 MSVC7
return 0;
}
The first definition of the template function add2() appears as if it is nothing more than a
wrapper around a trivial line of code. However, the use of function templates like this allows
us to handle the often ignored image processing issue of overflow. This definition of the
Page 160
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
We have also found that many compilers are quite lax regarding syntax for templates. If you
experiment with a particular compiler, you may find it accepts certain syntax that violates the
ISO C++ standard. Partial template specialization is one area where support is missing from
many compilers. This is an example of the issues you will uncover during prototyping.
Remember, you are not coding to the standard as much as you are coding to the conformance
of the compilers on your target platforms. If your development includes multiple platforms, this
means you are effectively designing for the least common denominator.
C OMPILE-TIME C ONSTRUC TS
The compiler can't always detect all errors at compile time. Sometimes, the compiler detects
errors at run time and throws an exception. Some constructs, such as exception
specifications (see Section 5.2 on page 135), require both compile-time and run-time checks.
If an exception is thrown and the specification is invalid, then std::unexpected() is called
and the application terminates (the proper behavior). You must define std::unexpected()
such that it can identify that the error condition occurred. Even if you are careful about
defining exception specifications, there is nothing to prevent another developer (or even you)
from writing software that violates this specification. And, you must do extensive testing to
make sure your specifications are correct. These complexities are just some of the reasons we
recommend you do not use this construct.
Another run-time issue that occurs is when casting is performed using dynamic_cast<>. This
Page 161
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
construct is useful for doing a downcast; that is, converting an object from its current type
to that of a derived object. Consider this simple example:
class apImage
{
public:
apImage () {}
virtual void f () {}
};
apImage imageInstance;
apImage& imageRef = imageInstance;
apColorImage colorInstance;
apColorImage& colorRef = colorInstance;
When you design your application, you need to think about how certain constructs affect
performance at run time. For example, the use of inheritance can help turn an incredibly
complex problem into a number of smaller, easier problems. Let's take a look at virtual
functions.
Page 162
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
When a compiler handles virtual functions, it creates virtual tables that are used at run time
to determine which function pointer gets used. This additional level of indirection incurs a
small run-time penalty each time a virtual function is called. If your design is such that virtual
functions are called frequently, then this overhead adds up to a measurable quantity.
Once the first virtual function is added to a class (and hence a virtual table is created), other
virtual functions incur only a very small size penalty. This is true because the virtual table only
needs to grow by the number of virtual functions added. Note that there is only one virtual
table for each type of object. However, as the number of virtual function calls increases, so
does the number of table lookups. For many applications, this overhead can be ignored. For
example, a drawing application where users manipulate objects by means of a graphical user
interface (GUI) is constrained by how often the user generates events, and is not likely to be
affected by virtual functions.
At the other end of the spectrum are real-time embedded systems. We learned a valuable
lesson from one of our early large-scale C++ efforts. Our design ignored the effects of virtual
function overhead, and we used virtual functions liberally. After all, processors were fast and
this was such a small effect, or so we thought. As a result, one part of the system was
written completely in C++, with a very rich framework. When the first benchmark test was
run, the product team was stunned. What used to take one millisecond in an older product
now took 50 milliseconds to run. It turns out that about 48 milliseconds of this time was
wasted in an overly complex design, thanks in part to too many virtual functions.
class apSimple
{
public:
apSimple () : sum_ (0) {}
void sum (int value) { sum_+=value;}
int value () const { return sum_;}
private:
int sum_;
};
apSimple is a non-virtual class that sums the value of all integers passed to it. We will use
apSimple as the baseline for our measurements as we break this into a more complex design.
class apVirtualBase
{
public:
apVirtualBase () : sum_ (0) {}
virtual void sum (int value) { sum_+=value;}
int value () const { return sum_;}
protected:
int sum_;
};
Our baseline is to measure how long it takes this snippet of code to run.
Page 163
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
apSimple simple;
Simple* sp = &simple;
for (i=0; i<1000000; i++)
sp->sum (i);
Our measurements were done using Microsoft Visual C++, and we used the
QueryPerformanceCounter() function to obtain access to the Windows high-resolution
counter. We used a pointer to call sum() so it would match our test.
apDerivedBase derivedbase;
apVirtualBase* vb = &derivedbase;
for (i=0; i<1000000; i++)
vb->sum (i);
This piece of code will return the same result as the previous one, except that each call to
sum() is done by way of the virtual table. A million calls to this function may seem excessive,
but look at a hypothetical image processing function to compute the sum of pixels in an
image.
int sum = 0;
for (int y=0; y<image->height(); y++)
for (int x=0; x<image->width(); x++)
sum += image->getPixel (x, y);
If image is a pointer to an apImage derived object, our calls to getPixel() will accumulate
virtual function call overhead, just like our example. For a 1024 by 1024 image, we are making
just over one million calls to have a meaningful benchmark.
Our test platform is an Intel Pentium 4 microprocessor-based machine, running at 2.0 GHz. Our
baseline loop took 1.1 milliseconds to execute, while our virtual function loop took 8.6
milliseconds. Seven milliseconds may not seem like much, but it can represent the difference
between your application running properly or not. If times like this are too small for you to
worry about, then just ignore this section. Otherwise, you need to understand the
ramifications of making any function virtual, especially when the function is involved in
time-critical code.
apDerivedBase derivedbase;
for (i=0; i<1000000; i++)
derivedbase.sum (i);
The difference from our previous loop is that we are calling the method directly in the derived
class, rather than by means of a pointer. In this case, there is no ambiguity about what
function should be called, and the compiler can avoid the virtual table. If we time this, we see
that this function takes the same amount of time as our baseline example. If we apply this
concept to our image processing example, we can add a new member function, sum(), to
compute the sum.
int apImage::sum ()
{
int sum = 0;
for (int y=0; y<image->height(); y++)
for (int x=0; x<image->width(); x++)
sum += getPixel (x, y);
return sum;
}
The getPixel() call is no longer using the virtual table and we completely eliminate this
overhead. When the user calls image->sum(), we incur a single virtual table lookup.
Page 164
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
template<class T>
class apImage
{
public:
apImage (int w, int h) : width_ (w), height_ (h), data_ (0)
{ data_ = new T [width() * height()];
memset (data_, 1, width() * height() * sizeof(T));
}
~apImage () { delete [] data_;}
int width () const { return width_;}
int height () const { return height_;}
T* getAddr (int x, int y) { return data_ + y*width()+x;}
T getPixel (int x, int y) { return *getAddr(x, y);}
int sum ();
private:
T* data_;
int width_, height_;
};
Now let's implement a specialization for the data type unsigned char, which is a very
common pixel type.
Page 165
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
return sum;
}
In this example, we do make assumptions about how memory is stored. Instead of using
getPixel() to fetch every pixel, we call getAddr() once to get a pointer to the first pixel in
the image. We then simply increment this pointer every time we want to access the next
pixel.
There is a large performance difference between these two versions of sum(). For a 1024 by
1024 image, our generic template function took 105 milliseconds to run. Our specialized
version for unsigned char took 2 milliseconds. This is an excellent use of specialization to
improve the performance of commonly used types.
We caution you to document very clearly where improvements have been made to enhance
performance. In a large image processing system, it is doubtful that every team member will
have a good understanding of all aspects of the system. What will happen when a team
member needs a different template type, and writes:
Add performance measurements to unit tests to make sure these optimized functions
execute as expected. If the measured performance does not fit within a desired
operating range (adjusted for processor speed), the test should fail.
During release testing, generate a list of all template arguments used by the
application. Compare this list against the existing documentation and prepare a list of
possible discrepancies. The development team should review this list to see if any new
specializations are needed.
[
Tea
m LiB
]
Page 166
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
When building applications, most developers don't think about internationalization issues unless
there are immediate business requirements that force them to do so. When the requirements
show up later, they force the developers to remediate the code -- a process that is often far
more complex and expensive than planning for internationalization from the start.
Almost all significant business software ends up internationalized sooner or later, and the
technical requirements posed by internationalization are becoming more complex. For example,
People's Republic of China (PRC) now requires that all software sold in China must support the
GB18030 character seta very large and complex character set.
Many people only think about the user-interface component of internationalization, which
includes translating such items as messages, menus, and labels. The translation process can
be fairly straightforward, regardless of the target market. Straightforward, however, is not the
same thing as trivial. You cannot simply replace the strings in the original language with
translated strings and expect a working result. The problems with such an approach include:
The translated strings no longer fit into the GUI (or dialog boxes), which were designed
for Latin characters running under the English or European versions of the operating
system.
In addition to translation issues, there are issues related to handling text inside your
application. Almost all applications read, write, parse, or otherwise process some text. Code
that works with text can require very significant adaptation to handle multiple languages. This
is especially true for those languages with large and complex character sets, such as Chinese,
Japanese, and Korean. See [Lunde99]. All kinds of things can and will go wrong when your
code encounters text in other character sets, including:
The database underlying the product, which worked fine for Latin characters, starts to
produce errors.
To avoid these and other problems, you need a little forward thinking to allow for handling
international text, even if it is not required for your initial release. In this section, we touch on
a few of the largest issues involved in making your code ready for internationalization. Getting
on the right track from the beginning is the key to making this process work. Toward that
goal, Figure 5.3 highlights some of the issues you should consider.
Page 167
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
5.4.1 Unicode
If your program processes any significant amount of text, the most important issue is to
decide how to represent text in your code - what character encoding to use. A character
encoding defines the mapping from a set of characters to binary values in your code. As soon
as you look outside of Western Europe, you will discover that there are many character
encodings other than ASCII and Latin-1, sometimes several for each language. For example,
the Japanese language appears in four different encodings in common applications: Shift-JIS,
EUC-JP, UTF-8, and ISO-2022-JP.
You can write your code to process text in one or more of the hundreds of defined encodings.
However, if you do this, you are likely to encounter defects and complexity that must be
debugged one language at a time. The alternative is to follow the example of Oracle, Microsoft
Windows, and Java, and use Unicode as your internal representation for text.
Microsoft Windows supports UTF-16 as a native data type in C, C++, C#, and Visual Basic.
Various UNIX systems and compilers support different Unicode encoding methods. You will
almost certainly have to support data sources in the many legacy encodings, but by
converting to Unicode for your internal processing, you can avoid the complexity of handling
all of these encodings in the bulk of your code.
The Unicode Standard has been adopted by such industry leaders as Microsoft, Apple, Oracle,
Sun, HP, IBM, Sybase, and many others. Unicode is required by such standards as Java,
JavaScript, XML, LDAP, CORBA 3.0, and WML. In addition, Unicode is the official way to
implement ISO/IEC 10646 and GB18030, which have important business ramifications.
You can add Unicode support to your application either directly or by using a third-party
library, such as Basis Technology's Rosette Core Library for Unicode (
http://www.basistech.com) or International Components for Unicode's (ICU) open source
software package (http://oss.software.ibm.com/icu/index.html). For detailed information
about Unicode and the Unicode Standard, visit the web site: http://www.unicode.org.
Page 168
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
strings in the scope of internationalization: the size of the buffers, their placement on the
GUI, and their use within the code, to name a few.
There are differing approaches to what strings should be localized (or translated). For
example, some people feel that all strings should be localized, including all error messages. We
take a simpler approach and consider the intended audience when determining whether or not
to localize a message. For example, the message File not found should be localized because
the user can take some corrective action when this message is received. However, a string
like Fatal Error #1004. Stack Trace: ... mostly contains information useful only to the
developer. In this case, we would split the string into two parts. The Fatal Error #1004
would be localized because this tells the user that something bad happened. However, any
other detailed information, such as a stack trace or internal dump of the system, can be in
the native language.
In this section, we design a simple resource manager to handle all strings used within the
application. The goal of our resource manager is to make it easy to replace strings with a
different list of strings, depending upon the desired language. We create a repository for all
displayed strings, with a mechanism for replacing that repository with an alternate version.
The design is flexible because few assumptions are made regarding where this list of strings is
stored. Our application refers to strings with a unique ID, which is the key to the design.
Exports all managed strings to a file, which is usually done during development after all
the strings have been defined. This file is then given to translators to produce a
localized version for another language.
Imports strings from a file. This is usually done when the application starts running to
load a set of translated strings.
Stores the string files in XML. This permits the file to be edited using an XML-aware
editor, or even a generic text editor, as long as it can display wchar_t characters.
It is important to note that our resource manager does not address the myriad of GUI issues
that arise (due to localization and the rendering issues specific to native operating systems).
We only briefly address the issue of string length. The CD-ROM contains the full source code
for the resource manager.
STRING REPOSITORY
apResourceMgr keeps all strings in a std::map object, which also does most of the work.
struct apStringData
{
std::wstring str; // Current string value
std::wstring notes; // Optional notes describing value
};
RETRIEVING A STRING
Page 169
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
return (*i).second.str;
}
ADDING A STRING
Inserting a string into our map is only a little more complicated, as shown here.
if (id == 0)
return id; // null string. These can be safely ignored.
apStringData data;
data.str = str;
data.notes = notes;
strings_[id] = data;
return id;
}
Only the first argument of addString() is required. The notes field is optional and is needed
only when you want to keep track of any requirements or translation notes. If id is not
specified, a function hash() is called to compute an id based on the string itself. It is usually
considered an error if the id is already found in our map. Some may consider this a bit harsh,
so the overlay argument can be set to true to allow duplicate strings to replace any existing
definition.
Our resource manager can be used, depending upon the style of the developer, in the
following ways:
Page 170
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
We have found it cumbersome to refer to strings by an ID value, because these IDs must be
unique to the application. However, for developers that are comfortable with this method of
localizing strings, our implementation supports it, as shown in the following example:
#include <string>
#include <iostream>
int main()
{
std::string astr = "Hello World";
std::wstring wstr = L"Hello World";
std::cout << astr << std::endl;
std::wcout << wstr << std::endl;
return 0;
}
The second way you can use apResourceMgr is to encapsulate all your strings inside another
object, apStringResource. You can choose either to define these objects in each source file,
or you can define them all in a single resource file. One nice feature of apStringResource is
that you do not have to worry about string ids. You access the string using the variable
name you created. Another advantage of using apResourceMgr is that your code contains a
default string to display. If no translation is available for a particular string, the default string
is shown.
EXAMPLE
Let's look at a simple example:
int main()
{
Page 171
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
class apStringResource
{
public:
static std::wstring sNullWString;
private:
unsigned int id_; // Id of this string
To keep this subject brief we will not go into every aspect of the XML tools we use. We wrote
a simple XML parser because we support a limited number of tags and do not require a
comprehensive package. There are many XML parsers available, including the open source
Expat library (see http://www.jclark.com/xml/expat.html). These are somewhat large
packages and we simply did not need all this functionality. However, if your application uses
XML for other purposes, we encourage you to write your own version of exportStrings()
and importStrings() to take advantage of the parser you already use.
Page 172
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
EXAMPLE
Our use of XML can best be seen from example:
The first line of the file, <?xml version="1.0" encoding="UTF-16" ?>, describes
the data to be XML using UTF-16 format. What is not shown is that two bytes
precede the first printed data. This data is called the BOM, or byte-order mark.
Because machines store data in either little-endian or big-endian order, the BOM
specifies which order is used in the file. This permits your localized string files to be
used on any machine, regardless of whether the endian order of the file matches
that of the machine.
Use the tag, <resources>...</resources>, once per file. This must surround all
other elements in the file.
<id>id</id>
<string>string</string>
<notes>notes</notes>
Our simple parser can also accept comments, but these are removed when the file
is read. A comment looks like this:
Page 173
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
apWideTools, to encapsulate endian detection and byte swapping. The XML functionality is
kept inside an object, apXMLTools. You can use this for other simple parsing needs, or you
can substitute it with a comprehensive XML parser.
API to retrieve messages from a file. Macros are used to replace the user string with a
version that will look up, and return, a translated string.
5.4.5 Locales
You can't discuss internationalization in the context of C++ without mentioning locales.
Although we don't recommend using locales for managing large-scale commercial
internationalization efforts, we provide a very brief overview here. For an extensive
discussion, see [Stroustrup00] or [Langer00].
The standard library includes a complicated package called locales. Although it is easy to
understand the intention of std::locale, its difficulty lies in its extensibility. std::locale
was intended to be used by the stream classes, although it is a general purpose object. You
can think of a locale as a collection of preferences, usually with regard to how something is
displayed.
For example, time and date formatting depends upon standards that vary around the world.
The date December 31st, 2002 would be represented as follows:
In Europe: 31/12/2002
In Japan: 2002/12/31
Fortunately, when an application runs, these preferences are typically specified by the user.
C++ allows facets to be reused by different locales, and you can take an existing locale and
change one or more facets. Delving deeper into locales and facets is beyond the scope of this
book. However, we do recommend the following:
Even if you do not use locales directly, the underlying stream package and many
run-time library functions do. When you write locale-type data to a buffer (for
example, strftime to format a time string), be generous about the size of any
temporary buffers you allocate. If you use a fixed size buffer and the function asks for
a maximum buffer size, you should use sizeof() instead of a hard-coded value.
If you display any string whose length might be affected by a locale, you should verify
Page 174
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Page 175
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
5.5 Summary
In this chapter, we explore multithreaded and multiprocess designs. We introduced the
concepts of locking objects and defined an object to manage the lock. We also contrast the
UNIX and Win32 implementations of threads and process, and provide practical techniques for
avoiding deadlocks.
We also discuss exception handling, and provide specific recommendations for top-level catch
handlers, as well as detailed guidelines for creating your own exception handling framework.
Smaller topics, such as catching specific types of exceptions like memory allocation errors,
and exception specifications are also covered. See Figure 5.1 for a checklist of rules for
handling exceptions. See Figure 5.2 for a checklist of guidelines for using assertions.
In Chapter 6, we use everything we have considered in the past chapters to finalize the
design and implementation strategy for the image framework. In addition, we demonstrate how
to integrate third-party libraries to further extend the framework's functionality and improve
the performance of some image processing functions.
[ Team LiB ]
Page 176
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Chapter 6. Implementation
Considerations
IN THIS CHAPTER
C++ Concepts
Memory Alignment
Exception-Safe Locking
Image Datatypes
Image Coordinates
Image Storage
In this chapter, we apply what we have learned through prototyping and examples to finalize
the design for the image framework. The components of the framework are shown in Figure
6.1.
Our design has had many iterations, using a variety of C++ techniques. We started by using
inheritance to create a framework that could handle numerous image types. It quickly became
apparent in our subsequent prototype that templates both improved and simplified the design
by eliminating most of the object hierarchy. However, we were still including image storage as
a component of the image class, instead of separating it as an image component. We also
explored using handles, but found they did nothing to improve the design. Once we prototyped
a solution that separated storage from the image class, we knew that the final design was
close at hand.
Page 177
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Each prototype was incomplete, but collectively they allowed us to test different design
principles. We used a single, trivial, image processing function (computing thumbnail images)
to observe the merits of each design. And, we wrote a unit test for each prototype to verify
the correctness and the behavior of memory allocation, and to verify how pixels are accessed.
Often, the final design grows out of one or more prototypes. Sometimes it is obvious that you
have hit on the right design, and other times it becomes an iterative process. From our
prototypes, we applied the following ideas:
Image storage should be separate from the image processing functions. The image
storage classes are independent of the image processing functions (but not vice
versa).
Templates should be used for a more efficient design, allowing us to: produce a single
version of code that works with any pixel type; optimize performance where needed by
using specialization; and adapt our image storage component to use other memory
allocators.
[ Team LiB ]
Page 178
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Image coordinates
Image storage
Pixel types
User-defined image coordinates are not discussed in this book. With user-defined coordinates,
the user can access images using coordinates that make sense for a particular application.
This can be something as simple as redefining the coordinate system, such that (0,0) is
located at the lower left corner of the image. Or, it can be as complicated as specifying
coordinates in terms of real units, like millimeters. We don't deny that this is a useful feature,
but it is also very application-specific. If you need to define your own coordinate system, you
can encapsulate this in your own code and transform it to our native coordinates.
When an image is created, all three values are specified, with the origin typically being (0,0).
To make working with coordinates easier, we create two generic objects to handle points and
rectangles.
POINT
A point is an (x,y) pair that specifies the integer coordinates of a pixel. Our apPoint object is
shown here.
class apPoint
{
public:
apPoint () : x_ (0), y_ (0) {}
apPoint (std::pair<int, int p)
: x_ (p.first), y_ (p.second) {}
apPoint (int x, int y) : x_ (x), y_ (y) {}
Page 179
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
REC TANGLE
A rectangle describes a point with a width and height. We use this to define the boundary of
an image. The width_ and height_ parameters are unsigned int, since they can only take
on positive values. A rectangle with an origin at (0,0), and width_ of 10, and height_ of 10,
describes a region with corners (0,0) and (9,9) because the coordinates are zero-based. A
null rectangle, a degenerate case where the rectangle is nothing more than a point, occurs
when width_ or height_ is zero.
class apRect
{
public:
apRect ();
apRect (apPoint ul, unsigned int width, unsigned int height);
apRect (apPoint ul, apPoint lr);
apRect (int x0, int y0, unsigned int width, unsigned int height);
Page 180
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
apPoint lr () const;
INTERSEC T()
The intersect() method computes the intersection of two rectangles, producing an output
rectangle, or a null rectangle if there is no intersection. This method handles a number of
conditions, including partial and complete overlap, as illustrated in Figure 6.2.
template <class T> const T& apMin (const T& a, const T& b)
{ return (a<b) ? a : b;}
template <class T> const T& apMax (const T& a, const T& b)
{ return (a>b) ? a : b;}
Page 181
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
WITHIN()
The within() method tests whether or not a point is inside the rectangle. It returns true if
the point is inside or on the border. The implementation of within() is shown here.
EXPAND()
The expand() method increases the size of the rectangle by adding a specific quantity to its
dimensions. This method is very useful when performing image processing operations that
create output images larger than the original image. Note that you can also pass in negative
values to shrink the rectangle. The implementation of expand() is shown here.
Handles and rep objects. The bottom line is that they do not fit in the design. We
still use reference counting by means of apAlloc<>, but having another layer of
abstraction is not necessary. Our final image storage object encapsulates an
apAlloc<> object along with other storage parameters. Because we aren't using
handles, these storage objects get copied as they are passed. Fortunately, the copy
constructor and assignment operators are very fast, so performance is not an issue
because the pixel data itself is reference counted. The complexity of the additional
layer of abstraction didn't provide enough of a benefit to make it into the final design.
Page 182
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
is not sufficient to align the first pixel in the image as apAlloc<> does. Most image
processing routines process one line at a time. By forcing the first pixel in each line to
have a certain alignment, many operations become more efficient. For generic
algorithms, this savings can be modest or small because the compiler may not be able
to take advantage of the alignment. However, specially tuned functions can be written
to take advantage of particular memory alignments. Many third-party libraries contain
carefully written assembly language routines that can yield impressive savings on
aligned data. Our final design has been extended to better address memory alignment.
Image shape. We refer to the graphical properties of the storage as image shape. For
example, almost all images used by image processing packages are rectangular; that is,
they describe pixels that are stored in a series of rows. Our prototypes and test
application described rectangular images. In our final design, we explicitly support
rectangular images so that we can optimize the storage of such images, but we also
allow the future implementation of non-rectangular images. For example, you might
have valid image information for a large, circular region. If we store this information as
a rectangle, many bytes are wasted because we have to allocate space for pixels that
do not contain any useful information. A more memory-efficient method for storing
non-rectangular pixel data is to use run-length encoding. With run-length encoding,
you store the pixel data along with the (x,y) coordinates and length of the row. This
allows you to store only those pixels that contain valid information. The disadvantage
of run-length encoding is the difficulty of writing image processing routines that
operate on one or more run-length encoded images.
Final Design
The final design partitions image storage into three pieces, as illustrated in Figure 6.3.
apImageStorageBase is the base class that describes the rectangular boundary of any
storage object. For rectangular images, it describes the valid coordinates for pixels in the
image. If you extend the framework to implement non-rectangular images, you would describe
the minimum enclosing rectangle surrounding the region. apRectImageStorage extends
apImageStorageBase to manage the storage for rectangular images. apRectImageStorage is
not a template class; instead, it allocates storage based on the number of bytes of storage
per pixel and a desired memory alignment for each row in the image. By making this a generic
definition, apRectImageStorage can handle all aspects of image storage. apImageStorage<T>
, however, is a template class that defines image storage for a particular data type. Most
apImageStorage<> methods act as wrapper functions by calling methods inside
apRectImageStorage and applying a cast. Let's look at these components in more detail in
the following sections.
Page 183
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
class apImageStorageBase
{
public:
apImageStorageBase ();
apImageStorageBase (const apRect& boundary);
apRectImageStorage is the most complicated object in our hierarchy. It handles all aspects
of memory management, including allocation, locking, and windowing. In this section, we
describe in detail how this object works. (The full source code is found on the CD-ROM.)
Reviewing the protected member data of apRectImageStorage shows us the details of the
implementation:
protected:
mutable apLock lock_; // Access control
apAlloc<Pel8> storage_; // Pixel storage
Pel8* begin_; // Pointer to first row
Pel8* end_; // Pointer past last row
eAlignment align_; // Alignment
unsigned int yoffset_; // Row offset to first row
unsigned int xoffset_; // Pixel offset in first row
unsigned int bytesPerPixel_; // Bytes per pixel
unsigned int rowSpacing_; // Number of bytes between rows
storage_ contains the actual pixel storage as an array of bytes. apAlloc<> allows a number
of objects to share the same storage, but the storage itself is fixed in memory. This allows us
to create image windows. An image window is an image that reuses the storage of another
image. In other words, we can have multiple apRectImageStorage objects that use identical
storage, but possibly only a portion of it. To improve the efficiency of accessing pixels in the
image, the object maintains begin_ and end_ to point to the first pixel used by the object and
just past the end, respectively. Derived objects use these pointers to construct iterator
objects, similar to how the standard C++ library uses them. bytesPerPixel_ and align_
store the pixel size and alignment information passed during object construction. Instead of
directly specifying the numeric alignment value, eAlignment provides a clean way to specify
alignment, as shown.
Page 184
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
eAlignment has entries using two different naming conventions, giving the user the flexibility
of choosing from two popular ones. rowSpacing_ contains the number of bytes from one row
to the next. This is often different than the width of the image because of alignment issues.
By adding rowSpacing_ to any pixel pointer, you can quickly advance to the same pixel in the
next row of the image.
xoffset_ and yoffset_ are necessary for image windows. Just because two images share
the same storage_ does not mean they access the same pixels. Image windowing lets an
image contain a rectangular portion of another image. xoffset_ and yoffset_ are the pixel
offsets from the first pixel in storage_ to the first pixel in the image. If there is no image
window, both of these offsets are zero.
The only remaining protected data member that we haven't described is lock_. lock_ handles
synchronization to the rest of the image storage variables, with the exception of storage_
(because it uses apAlloc<>, which has its own independent locking mechanism).
Each line in the image requires 8 bytes of storage, although only 6 bytes contain pixel data.
The first three bytes hold the storage for the first pixel in the line, and are followed by three
more bytes to hold the next pixel. In order to begin the next row with double-word alignment,
we must skip 2 bytes before storing the pixels for the next line. We dealt with memory
alignment when we introduced apAlloc<>. The arithmetic is the same, except we must apply
it to each line, as shown in the implementation of apRectImageStorage():
Page 185
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Using image windows is a powerful technique that lets you change which pixels an instance of
apRectImageStorage can access, as shown.
Page 186
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
lockState ();
unlockState ();
return true;
}
If the intersection is null, that is, there is no overlap between the rectangles, init() resets
the object to a null state. The remainder of the member variables are then updated to reflect
the intersection. The window() function only affects local variables, so we call lockState()
to lock access to member variables, because we do not also have to lock the underlying image
storage.
AP ROW ITERATOR<>
Before we introduce the actual storage objects, we need to introduce an iterator that can be
used to simplify image processing functions. Like iterators defined by the standard C++ library,
our apRowIterator<> object allows each row in the image to be accessed, as shown.
apRowIterator ()
{ cur_.p = 0; cur_.x = 0; cur_.y = 0; cur_.bytes = 0;}
Page 187
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
private:
current cur_;
};
Once you obtain an apRowIterator object from an apImageStorage<> object (presented in
the next section), you can use it to access each row in the image, as follows:
apRowIterator i;
for (i=image.begin(); i != image.end(); i++) {
// i->p points to the next pixel to process
...
}
Iterators don't really save us much typing, but they do hide the operation of fetching the
address of each line. If we did not have an iterator, we would write something like the
following, where T represents the pixel type:
AP PIXEL ITERATOR
We also create an iterator suitable for accessing every pixel in an image. apPixelIterator is
similar in design to apRowIterator, but it is implemented using the standard STL iterator
traits. See [Stroustrup00]. This makes the iterator usable by the generic STL algorithms, as
shown.
Page 188
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
struct current
{
T* p; // Current pointer to pixel
int x, y; // Current pixel coordinates
apPixelIterator ();
apPixelIterator (T* p, int bytes, int x, int y, int width);
private:
current cur_;
};
AP IMAGE STORAGE<>
template<class T>
class apImageStorage : public apRectImageStorage
{
public:
typedef apRowIterator<T> row_iterator;
typedef apPixelIterator<T> iterator;
apImageStorage () {}
apImageStorage (const apRect& boundary,
eAlignment align = eNoAlign)
: apRectImageStorage (boundary, sizeof (T), align) {}
Page 189
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
virtual ~apImageStorage () {}
Although the full comments are in the source code, we left a few critical ones in the code
snippet to indicate those member functions that synchronize access to the image data and
those that do not. Our decision about what functions should lock is based upon efficiency.
getPixel() and setPixel() lock both the object and the memory because these functions
are fairly inefficient to begin with. No locking is built into the other functions, and you are
responsible for determining the appropriate locking. Proper locking also requires us to catch
any exceptions that are thrown, as we do in our definition of getPixel():
template<class T>
const T& apImageStorage<T>::getPixel (int x, int y) const
{
static T pixel;
lock ();
try {
const Pel8* p = apRectImageStorage::getPixel (x, y);
memcpy (&pixel, p, sizeof (T));
}
catch (...) {
unlock ();
throw;
}
unlock ();
return pixel;
}
On page 188, we will see how to dramatically simplify getPixel() by using an exception-safe
Page 190
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
locking object.
EXAMPLE
Let's write a few different versions of a set() function to set all of the pixels in the
image to a fixed value. These versions demonstrate how to use row iterators, pixel
iterators, and generic algorithms from the STL, as follows:
template<class T>
void row_set (apImageStorage<T> image, T value)
{
typename apImageStorage<T>::row_iterator i;
unsigned int width = image.width ();
for (i=image.row_begin(); i != image.row_end(); i++) {
T* p = i->p;
for (unsigned int x=0; x<width; x++)
*p++ = value;
}
}
template<class T>
void pixel_set (apImageStorage<T> image, T value)
{
typename apImageStorage<T>::iterator i;
for (i=image.begin(); i != image.end(); i++)
*i = value;
}
template<class T>
void stl_set (apImageStorage<T> image, T value)
{
std::fill (image.begin(), image.end(), value);
}
There are more efficient ways to write these for basic data types, but these
versions have the advantage of working with any data type you might define. There
is no try/catch block defined because none is necessary. As long as we write a
loop using begin() and end() as shown, we will never access an invalid row.
Most functions that operate on apImageStorage<> objects will require some form of record
locking. This is true for functions that modify both the state of the object and the underlying
pixels. Writing a function that calls lock() and unlock() is not difficult, but you need to
consider how exceptions influence the design; otherwise, it is quite possible that when an
exception is thrown, the lock will not be cleared because the function does not terminate
properly. One solution is to add a try block to each routine to catch all errors, so that the
object can be unlocked before the exception is re-thrown. An easier approach is to construct
an object that uses the same RAII technique we describe on page 136, as shown.
Page 191
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
For example, getPixel() explicitly handles the locking and unlocking in its implementation.
This function can be greatly simplified with the use of apImageStorageLocker<>, as shown.
template<class T>
const T& apImageStorage<T>::getPixel (int x, int y) const
{
static T pixel;
return pixel;
}
As you can see, we create a temporary instance of apImageStorageLocker<> on the stack.
When getPixel() goes out of scope, either because of normal completion or during stack
unwinding of an exception, the lock is guaranteed to be released.
In the source code, we provide two generic functions: copy() and duplicate(). copy()
moves pixels between two images, while duplicate() generates an identical copy of an
apImageStorage<> object. Because we are dealing with template objects, our copy()
function copies image pixels from one data type to another, as shown.
The output storage must have the same dimensions as the input image. If not, a new
apImageStorage<T2> object is returned. This is a low-level copy function and we do
not want to worry about image boundaries that do not match. It would be better to
handle this at a higher level in the code.
If T1 and T2 are identical, memcpy() is used to duplicate pixels. This technique doesn't
work for complex data types, so an optional argument, fastCopy, has been added.
Page 192
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
if (src == dst)
return;
// Exception-safe locking
apImageStorageLocker<T1> srcLocker (src);
apImageStorageLocker<T1> srcLocker (dst);
if (src.boundary() != dst.boundary())
dst = apImageStorage<T2> (src.boundary(), src.alignment());
Support for basic data types such that they can be manipulated (i.e., added,
subtracted, and so on) in the standard ways
An RGB data type that allows a generic image processing routine to handle color pixels
A clamping (i.e., saturation) object that is used like other data types and eliminates
the undesirable pixel-wrapping behavior arising from overflow issues
Page 193
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
In our image framework, the pixel type is specified as a template parameter. In reality, there
are only a few common data types that most image processing applications need. Here are
the basic types used in image processing:
Pixels in color images are usually represented by RGB triplets. We showed a simple
implementation of an RGB triplet during the prototyping stage.
The following simple structure is not sufficient for our final design:
Page 194
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
operator T () const
// Conversion to monochrome
};
The complete implementation can be found in imageTypes.h on the CD-ROM. You will notice
that we added functions, such as operator apRGBTmpl<T2>, to make it easy to convert
between different RGB types.
Overflow is usually not an issue when an application uses mostly int and double data types.
However, when you use the smaller data types, like unsigned char, you have to be very
aware of overflows. What happens to the pixels that overflow the storage? What usually
happens is that the output will wrap, just like any mathematical operation on the computer.
This behavior has never seemed correct when dealing with image processing functions. After
all, if a value of 0 is black and 255 is white, 255+1 should be stored as 255 and not 0. This
clamping behavior is also called saturation.
EXAMPLE
8-bit images are still very popular. If you are using 8-bit images and you write
something like the following:
We think a better design for image processing is to use clamping as the default, while also
keeping the original wrapping behavior available if desired. Keep in mind that there is an
execution cost associated with clamping, and this may not always be tolerable. The clamping
operation is applied to every pixel that is processed, so the cost increases as the size of the
image increases.
For example, to detect and correct an overflow condition in a variable value larger than a
Pel8 looks like this:
Page 195
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
EXAMPLE
Let's look at an example to clarify the handling of overflow issues:
Pel32 l;
... some computation that sets l
Pel8 b = static_cast<Pel8>(l);
b = b > 255 ? : 255 : static_cast<Pel8>(b);
Clearly, the last line of this example does nothing, because b is already defined as
a Pel8. This demonstrates the problem, though, of trying to add clamping as a
separate step.
In order to get the behavior that you want, clamping must be designed into the image
processing routines. The hard way to solve this problem is to have two versions of every
routine: one that clamps the output data, and one that wraps.
The easier way is to use templates to define new pixel types that not only define the size of
each pixel, but also specify the clamping behavior. You want to be able to use an
apImage<Pel8> that defines an image using byte storage with the usual overflow behavior
(wrapping). You also want an apImage<apClampedPel8> that defines an image using byte
storage, but employs clamping. This solution requires three pieces:
Functions to convert and clamp a numeric quantity from one data type to another. In
addition to basic data types, our solution must also work for RGB and other complex
data types.
A new object, apClampedTmpl<>, that is similar to a basic data type, but has clamping
behavior.
Operators and functions that define the basic mathematical operations needed for
image processing functions.
Clamping Functions
To clamp a value at the limits of a data type, we must know the limits. With C, we used
#include <limits.h> to get this functionality. With C++, we can use #include <limit>.
The std::numeric_limits class gives us everything we need. We can easily determine the
minimum and maximum values for a data type by querying the static functions of this object,
as shown:
T minValue;
T maxValue;
Page 196
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
private:
apLimitInfo (T min, T max) : minValue (min), maxValue (max) {}
};
apLimitInfo<> gives us a common place to define limit information for any data type. The
definitions for a few of the data types are shown here.
template<> apLimitInfo<Pel8>
apLimitInfo<Pel8>::sType (std::numeric_limits<Pel8>::min(),
std::numeric_limits<Pel8>::max());
template<> apLimitInfo<apRGB>
apLimitInfo<apRGB>::sType (
apRGB(std::numeric_limits<Pel8>::min(),
std::numeric_limits<Pel8>::min(),
std::numeric_limits<Pel8>::min()),
apRGB(std::numeric_limits<Pel8>::max(),
std::numeric_limits<Pel8>::max(),
std::numeric_limits<Pel8>::max()));
These may look long, but they are just a machine-independent way of saying:
We can now construct a simple clamping function to test and clamp the output to its minimum
and maximum value.
Pel32 l = 256;
Pel8 b = apLimit<Pel8> (l); // b = 255
With this syntax, you explicitly define the type of clamping you desire. In this particular
example, the benefits are well worth the added bit of typing. The compiler will generate an
error if you neglect to specify the clamping data type.
Clamping Object
Page 197
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
apClampedTmpl () : val(0) {}
apClampedTmpl (T v) : val(v) {}
template<class T1>
apClampedTmpl (const apClampedTmpl<T1>& src)
{ val = apLimit<T> (src.val);}
One last step is to make apClampedTmpl<> look more like a data type by using typedefs, as
shown.
We also need to define a number of other global operators and functions that image
processing routines will need.
OPERATOR-
Page 198
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
We now add a few more arithmetic operations that our image processing functions require.
ADD2()
We implement two versions of image addition: one version that operates on a generic type,
and one version that employs clamping. These are as shown here.
SC ALE()
scale() does a simple scaling of the source argument by a floating parameter, as shown
here.
Page 199
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
It may seem like we have gone through great lengths for little benefit. After all, we have
mandated that image processing functions do this:
dst = s1 + s2;
The latter is more intuitive, but it is also more prone to error. If we are careful, we can
construct operator+ and operator= for the various data types to give the desired behavior.
The problem is that the compiler can do almost too good a job at finding a way to compile this
line by applying suitable conversions. Because we are dealing with basic data types, like
unsigned char, the compiler often has many ways to convert from one data type to another.
This makes it easy to write code that does not perform as you expect. If this happens, there
is a possibility that the error may not be caught until your testing phase.
[ Team LiB ]
Page 200
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
A common mistake when designing C++ classes is to make everything a member function of
the underlying class. As disparate functionality is added to an object, understanding what the
object does become more difficult. A simple class can grow to over 100 member functions with
little effort if you add new functionality at every opportunity. If these methods consist of 20
to 30 different pieces of functionality with similar, but different, interfaces, the object is still
of manageable size. The std::string class is a good example of this type of object. There
are a lot of member functions, but the functionality can be divided into one of about 25
families of functions. Our image class, on the other hand, can have a large number of different
routines, so instead most are implemented as functions.
apImageStorage<> contains most of the actual functionality exposed by apImage<>, but does
not handle some of the special conditions we expect to encounter. For example,
apImageStorage<> knows very little about image windows, other than how to create one.
The image processing functions that we write will use image windows to determine which
pixels to process. This will become especially important for image processing functions that
take more than one image as an argument.
There are two consumers of our image class: imaging applications and application-specific
frameworks:
For real applications, our API must be sufficiently broad so that images can be
manipulated. This includes image filtering, transformation, and arithmetic operations.
Once we discuss the final design of apImage<>, we will apply that design to enhance the
framework, so that it works with a number of image types that offer enhanced performance on
some platforms.
Page 201
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
In the final design, we kept the class T template parameter, but moved the class E
template parameter, which is the internal pixel type used during computations, to the global
image functions. Then we added an additional template parameter, class S, to make the
declaration, as follows:
class The pixel type. This parameter has the same meaning as in earlier classes.
T
class The underlying image storage object. The default parameter, apImageStorage<T>,
S which uses apAlloc<> to allocate memory, is applicable for most applications. If
you want to use another object to handle memory allocation, then specify it here.
This object also contains the iterators necessary to process the image on a row or
pixel basis.
Most applications can think of the image class as apImage<T>, rather than apImage<T,S>.
This is certainly desirable, because everything is more readable and it makes the class look
less onerous. When using apImage<> to write new image processing functions, you will still
need to deal with both parameters, but this is usually nothing more than a bookkeeping task,
and the compiler always reminds you if you made a mistake.
apImage () {}
apImage (const apRect& boundary,
apRectImageStorage::eAlignment align =
apRectImageStorage::eNoAlign)
: storage_ (boundary, align)
{}
apImage (S storage) : storage_ (storage) {}
virtual ~apImage () {}
Page 202
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
// iterators
row_iterator row_begin () { return storage_.row_begin();}
const row_iterator row_begin () const
{ return storage_.row_begin();}
row_iterator row_end () { return storage_.row_end();}
const row_iterator row_end () const
{ return storage_.row_end();}
iterator begin () { return storage_.begin();}
const iterator begin () const { return storage_.begin();}
iterator end () { return storage_.end();}
const iterator end () const { return storage_.end();}
Page 203
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
// Conditionals
bool isIdentical (const apImage& src) const;
// Checks for same boundary and identical image storage only
protected:
S storage_; // Image storage
};
Over half of this class is nothing more than a wrapper around the storage object. Although we
allow complete access to the storage object by means of the storage() method, we still try
to make direct access to the individual members as easy as possible. The remaining methods
belong to two categories: image operations and image storage operations.
IMAGE OPERATIONS
Most image operations alter the image with a constant value. The set(), add(), sub(),
mul(), and div() functions modify each pixel value with a constant value. scale() is similar
to mul(), except that a floating point scaling parameter is specified. These functions are very
intuitive to use:
apImage<Pel8> image;
image.set (0); // Set each pixel to 0
image.add (1); // Each pixel is now 1
image.mul (2); // Each pixel is now 2
image.scale (.5); // Each pixel is now 1
Image storage operations manipulate how an image is stored in memory. We spent a lot of
time in earlier sections describing how important image alignment is, especially when it comes
to performance on many platforms. Many image processing routines allocate and return new
images that may not have the desired alignment. Another need to adjust alignment arises
when window() is used to return a window of an image. The alignment of the image window
will depend on the size and location of the window itself. If you need to perform a number of
operations, and do not need to modify the parent image data, you should work with an aligned
image. align() returns an image that has the desired alignment for each row in the image. If
the image already has this alignment, the original image is returned; otherwise, a new image is
returned that contains the same pixel values as the original image.
Page 204
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
EXAMPLE
We now have enough functionality to write a simple application. Let's create an
image and use an image window to demonstrate a useful feature of windows.
int main()
{
apRect boundary (0, 0, 16, 16);
apImage<Pel8> image1 (boundary);
image1.set (0);
apImage<Pel8>::row_iterator i;
for (i=image1.row_begin(); i != image1.row_end(); i++) {
Pel8* p = i->p;
for (unsigned int x=0; x<image1.width(); x++)
std::cout << static_cast<int>(*p++);
std::cout << std::endl;
}
return 0;
}
This application sets each pixel in a 16x16 image to 0. A 4x4 image window, image2,
is created, and these values are set to 1. Finally, the pixels from the initial image
are displayed. The following display demonstrates that no pixels were copied when
the image window is created. image2 is pointing to the same pixels as those of
image1.
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000001111000000
0000001111000000
0000001111000000
0000001111000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
Page 205
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
0000000000000000
The static_cast<> reference in the example is just a simple cast, because we
want to display the image pixel as a number and not as a character. Most stream
packages will display a Pel8 (i.e., unsigned char) as a character.
ADD()
Let's look at the implementation of add(). It bears a close resemblance to the code that
displayed the image to the console in the previous example.
typename apImage<T,S>::row_iterator i;
unsigned int w = width ();
T* p;
for (i=row_begin(); i != row_end(); i++) {
p = i->p;
for (unsigned int x=0; x<w; x++)
*p++ += value;
}
}
There are a few differences between add() and the display code:
add() locks and unlocks the image to prevent other threads from accessing the image
data or changing the state of the image until the function is finished.
[ Team LiB ]
Page 206
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
For example, how do we go about copying an image? Is it possible to copy just a section of
the image? To answer these questions, we need to analyze what copying an image really
entails. We will take the lessons we learn from copying images and apply them to almost all of
our image processing functions. Our generic interpretation means that these functions become
more difficult to write, but they also become much more powerful to use.
Method 1: The source image is specified and the copy function creates the destination
image.
apImage<Pel8> image1;
...
apImage<Pel8> image2 = copy (image1);
Method 2: The source image and destination image are specified. The pixel types are
the same.
apImage<Pel8> image1;
...
apImage<Pel8> image2;
copy (image1, image2);
Method 3: The source image and destination image are specified. The pixel types are
different.
apImage<Pel8> image1;
...
apImage<Pel32> image2;
copy (image1, image2);
EXAMPLE
This example shows how not to copy an image:
apImage<Pel8> image1;
...
apImage<Pel8> image2 = image1;
Page 207
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
image2 and image1 share the same underlying pixel data. While this is an efficient
way to share memory between images, it is not what you want to do, especially
when you want to modify a copy of the image and keep the original image intact.
You might be tempted to implement a different version of copy() for each method. The
semantics of method 1 differs from methods 2 and 3, but if we can design a solution for
method 3, we can use it to solve method 1 as well. We have already seen that in method 3,
the source and destination images do not have to be the same pixel type. This is a very
desirable feature because certain image conversions are very common operations. For
example, look at the conversion from a color image to a monochrome image:
apImage<apRGB> image1;
...
apImage<Pel8> image2;
copy (image1, image2);
There is a more complicated case we need to consider. What happens if the destination image
is not null? In other words, what should the behavior of copy() be if both the source and
destination images are valid? Most imaging software packages will probably discard whatever
is stored in the destination image and replace it with a copy of the source image. Doing so
makes the implementation of copy() easier, but it does not address what the user is asking
for. Our interpretation of the problem is quite different:
If no destination image is specified, create an image with the same boundary and
alignment as the source image.
You might wonder if all this is really necessary for a copy() function. The answer is yes,
because we want to offer this type of functionality for any image processing function that
looks like:
Computing the overlap between two images is easy. Mathematically, we want to compute the
intersection between the source image boundary and the destination image boundary. The
result is an apRect that specifies which pixels should be processed. apRect has an
intersect() method that will compute the overlap for us:
Page 208
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
copy()
operator+=
operator-=
duplicate()
operator+= and operator-= might not look like single source operations, but these operators
take a single image and add or subtract it to the destination image.
We provide a general class, apFunction_s1d1, that you can use to easily add your own single
source image processing functions. (Note that we have chosen to make this class name
descriptive, rather than concise, because it is used internally by developers. We reuse this
naming convention when we provide a similar class, named apFunction_s1s2d1, for two
source image processing functions.)
In general, we cannot assume that the same intersection region is applied to both the source
and destination images, so we keep these regions separate. We use the apIntersectRects
structure, as shown:
struct apIntersectRects
{
apRect src; // Intersection region for source image(s)
apRect dst; // Intersection region for dest image(s)
};
Page 209
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
protected:
Function function_; // Our process function, if any
apImage<T1,S1> roi1_; // roi of src1 image
apImage<T2,S2> roi2_; // roi of dst1 image
When T1 pixels are manipulated by an image processing function to compute T2 pixels, any
temporary storage will use R as the pixel type. There is no default argument for R because this
value is highly application-dependent. If you remember, it was our third prototype (separating
the image class and image storage) that demonstrated the need for R. R is the first parameter
because it must be explicitly defined.
apFunction_s1d1 can be used in two different ways, depending on how you want to specify
the actual image processing operations. You can either override process() to define your
processing function, or you can pass a function pointer to the constructor. We recommend
Page 210
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
that you pass a pointer because it means that there will be no changes to apFunction_s1d1,
and no need to derive objects from it. It also encourages you to write stand-alone image
processing operations that potentially have other uses in your application. You pass a function
pointer to the constructor, as shown:
EXEC UTE()
The run() method is the main entry point of apFunction_s1d1, but it only calls the virtual
function, execute(). The execute() method constructs the intersection and performs the
image processing operation. execute() is only overridden if the standard rules for computing
the image windows changes. We will soon see how image processing operations, such as
convolution, require a new definition for execute(). The definition for execute() is shown
here.
// Exception-safe locking.
apImageLocker<T1,S1> srcLocking (src1);
Page 211
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
roi2_.window (overlap.dst);
Returns a null destination image if the source image is null. We can take advantage of
the sNull definition available in every apImage object.
Creates the destination image if none was specified. This is performed by the virtual
function createDestination(). The default definition creates an image of the same
size and alignment as the source image.
Computes the intersection between the two images, creates an image window for each
one, and stores the image windows in roi1_ and roi2_. We use the term roi, meaning
region of interest, which aptly describes what these images represent. roi1_ will be
identical to src1 if no destination is specified, or if the destination was the same size
or larger than the source image. roi1_ and roi2_ are stored as member functions to
keep the object as generic as possible. We thought about passing the computed
images as parameters to process(), but derived classes might require other arguments
as well so we decided against it.
Calls process() to perform the image processing operation, which occurs inside a try
block to catch any exceptions that might be thrown. The catch block does no special
processing, other than to rethrow the error.
INTERSEC T()
The intersection() method does nothing but call a global intersect() function. We added
numerous intersect() functions to the global name space to encourage developers to use
them for other purposes. The intersect() function is shown here.
overlap.src = srcOverlap;
overlap.dst = srcOverlap;
return overlap;
}
PROC ESS()
We provide the process() function to allow derived classes to define their own processing
behavior, if necessary. We create a placeholder variable so that the compiler will call
function_ with the appropriate arguments, as shown:
Page 212
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
C OPY()
Let's get back to our copy() example and look at how we handle its implementation. We start
by designing the actual image processing operation, making sure that its function prototype
matches the Function definition in apFunction_s1d1. We show two implementations of our
copy function. Note that neither function requires the template parameter, R, so this
parameter is ignored.
ap_copy() defines the generic copy function and uses assignment to copy pixels from
the source image to the destination image, as shown here.
template<class R, class T1, class T2, class S1, class S2>
void ap_copy (const R&, const apImage<T1,S1>& src1,
apImage<T2,S2>& dst1)
{
typename apImage<T1,S1>::row_iterator i1;
typename apImage<T2,S2>::row_iterator i2;
unsigned int w = src1.width ();
const T1* p1;
T2* p2;
for (i1=src1.row_begin(), i2=dst1.row_begin(); i1 !=src1.row_end();
i1++, i2++) {
p1 = i1->p;
p2 = i2->p;
for (unsigned int x=0; x<w; x++)
*p2++ = static_cast<T2>(*p1++);
}
}
ap_copyfast() makes the assumption that memcpy() can be used to duplicate pixels,
as long as source and destination images share the same data type. ap_copyfast() is
slightly more complicated, because it uses typeid() to determine if the source and
destination image share the same pixel type. To properly use typeid(), make sure
that any compiler flags that enable Run-Time Type Information (RTTI) are set. The
ap_copyfast() function is shown here.
template<class R, class T1, class T2, class S1, class S2>
void ap_copyfast (const R&, const apImage<T1,S1>& src1,
apImage<T2,S2>& dst1)
{
typename apImage<T1,S1>::row_iterator i1 = src1.row_begin();
typename apImage<T2,S2>::row_iterator i2 = dst1.row_begin();
unsigned int w = src1.width();
unsigned int bytes = w * src1.bytesPerPixel ();
const T1* p1;
T2* p2;
Page 213
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
if (typeid(T1) == typeid(T2)) {
// We're copying like datatypes so use memcpy for speed
// This assumes T1 and T2 are POD (plain old data) types
for (; i1 != src1.row_end(); i1++, i2++) {
p1 = i1->p;
p2 = i2->p;
memcpy (p2, p1, bytes);
}
}
else {
// We have to do a pixel by pixel copy
for (; i1 != src1.row_end(); i1++, i2++) {
p1 = i1->p;
p2 = i2->p;
for (unsigned int x=0; x<w; x++)
*p2++ = static_cast<T2>(*p1++);
}
}
}
The assumption that memcpy() can be used to duplicate pixels is usually, but not always,
valid. For example, what if you had an image of std::string objects? It may sound absurd,
but it demonstrates that blindly copying memory is not always appropriate.
Our final version of copy(), written using the generic ap_copy(), is shown here.
In the case where the source image is specified and the copy should create the destination
image, we can create an overloaded version of copy() to take advantage of ap_copyfast(),
as shown.
Page 214
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
return dst;
}
While this function may be easier to write, it also slows greatly during execution. Using our 2.0
GHz Intel Pentium 4 processor-based machine, the performance for allocating and copying a
1024x1024 8-bit monochrome image is as follows: copy() takes 4 milliseconds, while
copy_stl() takes 16 milliseconds. Both functions produce the identical image as output.
intersect()
add()
operator+
sub()
operator-
Let's look at a few of these operations. We can use apFunction_s1d1 as a template to add a
new object that computes the intersection of three images (the two source images and the
destination image, if any). There are two images that supply source pixels to the image
processing function. If a valid destination image is specified, its boundary information helps
determine which source pixels to use in the image processing routine.
Our new object, apFunction_s1s2d1, takes on a slightly more complicated form, because
there are now two additional template parameters to refer to the additional image used by
these operations. This brings the total number of parameters to seven, but for most uses only
four are needed. The apFunction_s1s2d1 object is shown here.
INTERSEC T()
Let's look at the intersect() method, which is shown here. Note that we have removed a
few virtual functions, compared to apFunction_s1d1, because we do not expect derived
classes to be necessary.
Page 215
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
overlap.src = srcOverlap;
overlap.dst = srcOverlap;
return overlap;
}
Now let's look at the add() operation. As we demonstrated with copy(), add() uses the
apFunction_s1s2d1 class to add the contents of two images and store the result in a third.
ap_add2() is the function that performs this operation. And like copy(), we can ignore the
intermediate storage specifier (R in this case).
The user-callable functions are now easy to write. The only assumption that we make is with
operator+, where the destination image is given the same pixel type and alignment as the
first image specified. The implementation of add() and operator+ is shown here.
Page 216
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
apImage<T1,S1> dst;
add (src1, src2, dst);
return dst;
}
Noise in an image may come from such phenomena as photographic grain. Noise often appears
as random pixels interspersed throughout the image that have a very different pixel value than
those pixels immediately surrounding them (or the neighboring pixels).
There are many different algorithms for smoothing noise in an image. Noise in an image
generally has a higher spatial frequency because of its seeming randomness. We use a simple
low-pass spatial filter to reduce the effects of noise through an averaging operation. Each
pixel is sequentially examined and, in the 3x3 kernel we use, the pixel value is determined by
averaging the pixel value with its eight surrounding neighbors.
Given a point (x,y) we average nine pixels in a 3x3 neighborhood surrounding this point. This
3x3 kernel is shown here.
Page 217
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
This kernel is sequentially centered over each pixel. The value of each pixel and its
surrounding neighbors are multiplied and then summed. Finally, the average value is computed
using the following formula:
D(x,y) = (S(x-1,y-1)+S(x,y-1)+S(x+1,y-1)+
S(x-1,y) +S(x,y) +S(x+1,y)+
S(x-1,y+1)+S(x,y+1)+S(x+1,y+1)) / 9;
Figure 6.7 shows how this kernel is used to reduce the noise on a single pixel in an image.
This function is easy to write, until you consider the boundary conditions. Consider a source
image with an origin of (0,0). In Figure 6.7, the origin has a pixel value of 215. To compute the
destination point in the filtered image at (0,0), our equation shows that we need information
from pixels that do not exist (for example, S(-1,-1)). We cannot compute a pixel when the
data does not exist.
This boundary condition can be handled in a number of different ways. One very common
solution is to set all boundary pixels to 0 (black). We recommend a different, more generalized
solution that has several advantages. By effectively optimizing the problem to determine
which pixels are needed to compute the destination (or filtered) image, our solution allows
developers to ignore complicated boundary solutions.
Here's how it works. We compute the intersection of the source and destination image, taking
the size of the kernel into account. Our intersection() function computes which pixels to
process. In our example, the kernel size is 3 (using the 3x3 kernel above). The
intersection() function assumes the kernel size is odd, making it possible to center the
kernel over the pixels to process. The function is as follows:
Page 218
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
overlap.src = srcRegion;
overlap.dst = dstRegion;
return overlap;
}
As you can see, this function is very different from the simple intersection operations we have
written so far. Let's apply this function using an example with our 3x3 kernel. Assume that
both the source and destination images have an origin at (0,0) and are 640x480 pixels in size
as shown in Figure 6.8.
1. Determine if the kernel size is too small, and therefore no intersection exists. This is a
degenerate case. In our example, the kernel size of 3 is fine.
2. Determine which pixels the destination image needs in order to compute an output
value for every pixel in the image. To do this, we increase the size of the destination
region to show the pixels that are needed from the source image to fill the destination.
Page 219
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
For our 3x3 kernel, this amounts to expanding the size of the destination region by one
pixel in all directions, as shown in Figure 6.9.
This is also called a dilation operation. The destination region has an origin at (0,0) and
is 640x480 pixels in size. The expanded region has an origin at (-1,-1) and is 642x482
pixels in size.
3. Intersect this expanded destination region with the source region to find out exactly
which pixels are available in the source image. This produces an intersection region at (
0,0) of size 640x480 pixels, as shown in Figure 6.10.
5. Compute the actual destination pixels that will be manipulated. We determine this by
reducing the size of the source region by the one pixel in all dimensions as shown in
Figure 6.11.
This is also called an erosion operation. Eroding this region shows what pixels in the
destination region can be computed, with an origin at (1,1) and 638x478 pixels in size.
This says what we already know: if the source and destination images are the same size,
there is a one pixel border surrounding the destination image that cannot be computed. Under
common operating conditions, these calculations result in a long, but simple, result. It has
much more utility when you need to process a region of interest of a larger image. With larger
images, the destination image can often be filled with valid results, since the source image
Page 220
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Our neighborhood processing is similar to the one source image, one destination image case
we discussed earlier. Our image processing class, apFunction_s1d1Convolve, is derived from
apFunction_s1d1 to take advantage of our modular design. Besides taking additional
parameters, this object overrides the member functions that compute the intersection and
creates a destination if none was specified.
We can write a general purpose convolution routine by writing our processing function to take
an array of kernel values and a divisor. For example, the following kernel is what our image
framework uses to compute a low-pass averaging filter:
Page 221
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
As you can guess, convolution is a fairly slow process, at least when compared with simple
point processing routines. This function is somewhat dense and needs some further
explanation.
There are four loops in this function. The outer loops step pixel by pixel in the
destination image. The inner two loops perform the neighborhood operation on the
source image, by multiplying a kernel value by the source pixel value and accumulating
this term in sum.
When you call a convolution function, you must explicitly specify the datatype of R.
Once sum is computed, it is scaled by the divisor, which is 9 in our example, to create
the output pixel value. Some convolution kernels have a divisor of 1, and we can
achieve much higher performance by making this a special case. For example, we saw
a 10% performance improvement for a 1024x1024 image when we added this
optimization.
apLimit<> is used to prevent pixel overflows. Unlike many of our image processing
functions, where the user can select special data types to prevent overflow (by use
apClampedTmpl<>), convolution always enforces this constraint.
Kernel values are expressed as a char. This is sufficient for most convolution kernels.
However, some large kernels, especially Gaussian filters, may have values that do not
fit. If this is the case, you will need your own convolve() function that defines the
kernel as a larger quantity.
Fortunately, all of these details are hidden. To perform convolution, you can simply call the
convolve() function and explicitly specify R. Its definition is shown here.
apImage<Pel8> src
...
apImage<Pel8> dst;
char kernel[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1 };
convolve<Pel32> (src, kernel, 3, 9, dst);
If you call convolve() without specifying a value for R (i.e., as in convolve<Pel32>), the
compiler will generate an error to remind you to add one.
The edge of an object is indicated by a change in pixel value. Typically, there are two
parameters associated with edges: strength and angle. The strength of an edge is the
amount of change in pixel value when crossing the edge. Figure 6.12 illustrates strength by
the length of the arrows. The angle is the angle of the line as drawn perpendicular to the
Page 222
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
There are many methods for sharpening edges. A very effective and simple image processing
technique is to ignore the angle and use the strength to sharpen the edges. You can
accomplish edge sharpening by using a Laplacian mask (or kernel) in a convolution operation
on the image. The Laplacian kernel generates peaks where edges are found. Our framework
provides the following Laplacian kernel:
If we sum of all the values in the kernel, we see that they sum to zero. This means that when
this kernel is run over a constant, or slowly varying image, the output will be zero or close to
zero. However, when the kernel is run over a region where strong edges exist (the center pixel
tends to be brighter or darker than surrounding pixels), the output can be very large. Figure
6.13 illustrates the application of an edge sharpening filter.
We can write a function that is similar to ap_convolve_generic, but uses this specific
Laplacian kernel, as shown.
Page 223
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
sum -= *pk++;
sum -= *pk++;
sum -= *pk++;
pk += pitch;
sum -= *pk++;
sum += (*pk++) * 8;
sum -= *pk++;
pk += pitch;
sum -= *pk++;
sum -= *pk++;
sum -= *pk++;
It unrolls the two inner loops that are inside ap_convolve_generic and explicitly
computes the summation of the kernel using the source pixels.
It uses the variable pitch to specify the number of pixels to skip after we process one
line of input pixels to get to the start of the next line. Precomputing this value allows
us to quickly skip from one line to the next.
While this function efficiently processes monochrome data types, it is slower for color images.
Page 224
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
To address this issue, we can take advantage of template specialization and we can define a
special version of ap_convolve_3x3laplacian that works with apRGB (an 8-bit RGB image).
To do this, we not only unroll the two inner loops, but we also explicitly compute the RGB
values. This function is not difficult to write and it produces a dramatic increase in speed, as
shown here.
template<>
void ap_convolve_3x3laplacian (const apRGBPel32s&,
const apImage<apRGB>& src1,
const char* /*kernel*/,
unsigned int /*size*/,
int /*divisor*/,
apImage<apRGB>& dst1)
{
apImage<apRGB>::row_iterator i1 = src1.row_begin();
apImage<apRGB>::row_iterator i2 = dst1.row_begin();
sum.red -= pk->red;
sum.green -= pk->green;
sum.blue -= pk->blue;
pk++;
sum.red -= pk->red;
sum.green -= pk->green;
sum.blue -= pk->blue;
pk++;
sum.red -= pk->red;
sum.green -= pk->green;
sum.blue -= pk->blue;
pk++;
pk += pitch;
sum.red -= pk->red;
sum.green -= pk->green;
sum.blue -= pk->blue;
pk++;
sum.red += 8*pk->red;
sum.green += 8*pk->green;
sum.blue += 8*pk->blue;
pk++;
Page 225
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
sum.red -= pk->red;
sum.green -= pk->green;
sum.blue -= pk->blue;
pk++;
pk += pitch;
sum.red -= pk->red;
sum.green -= pk->green;
sum.blue -= pk->blue;
pk++;
sum.red -= pk->red;
sum.green -= pk->green;
sum.blue -= pk->blue;
pk++;
sum.red -= pk->red;
sum.green -= pk->green;
sum.blue -= pk->blue;
pk++;
There is one more small change to our template specialization for ap_convolve_3x3laplacian
. As we discussed in class Versus typename on page 25, we cannot use the keyword
typename in our specialization without generating an error. The line from our generic template
definition:
apImage<apRGB>::row_iterator i1 = src1.row_begin();
To use the Laplacian filter, you can simply call the laplacian3x3() function and, as with
convolve(), explicitly specify the R template parameter. The definition of laplacian3x3() is
shown here.
Page 226
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
As you can see from the results, this specialization was clearly advantageous. We have
removed numerous loops in our RGB specialization, which explains the performance gain. As
you would expect, calling any of the three functions produces the identical image as output.
If we sum of all the values in the kernel, we see that they sum to one. This means that when
this kernel is run over a constant, or slowly varying image, the output will be very close to the
original pixel values. In areas where edges are found (i.e., the pixel values vary), the output
values are magnified. Figure 6.14 illustrates the application of a high-pass edge sharpening
filter.
You can use a convolution operation with a Gaussian kernel to smooth the edges in your
image. This technique usually produces a superior result to the low-pass kernel we presented
on page 219. Our framework provides the following Gaussian kernel:
Page 227
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Like our general convolution kernel, the Gaussian kernel uses summing and averaging to assign
new values to pixels in the filtered image. The effect is that the strong edge differences are
reduced, giving the filtered image a softer or blurred appearance. This is useful for reducing
contrast or smoothing the image to eliminate such undesired effects as noise and textures.
Figure 6.15 illustrates the application of a Gaussian edge smoothing filter.
Page 228
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
if (src1.isNull())
return dst;
typename apImage<T1,S1>::row_iterator s;
typename apImage<T1,S1>::row_iterator d;
typename apImage<T1,S1>::row_iterator s1;
unsigned int w = dst.width ();
const T1* sp;
T1* dp;
R sum;
return dst;
}
[ Team LiB ]
Page 229
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Modern software takes advantage of existing libraries to speed development and minimize the
maintenance issues. It is now considered good design practice to design applications with
interfaces that leverage existing code. We use the word delegates to refer to third-party
software packages to which we delegate responsibility.
There are many image storage formats, including JPEG, GIF, PNG, and TIFF. They all have their
advantages and disadvantages, so supporting a single format is of limited use. We will design
a simple interface so new file formats can be added with little difficulty. This design can be
used by most image formats, although it may not take advantage of all the features of an
individual format. Figure 6.17 provides an overview of the file delegate strategy.
We create a base class, apImageIOBase, that defines the services we want and then we
derive one class from apImageIOBase for every file format we want to support.
apImageIOBase defines three essential methods, info(), read(), and write(), that check
the file format and handle the actual reading and writing of each file format, respectively, as
shown.
class apImageIOBase
{
public:
virtual apDelegateInfo info (const std::string& filename) = 0;
Page 230
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
INFO()
info() determines whether a file is of the specified format and, if known, can provide the
size. info() returns a structure, apDelegateInfo, which is shown here.
struct apDelegateInfo
{
bool isDelegate; // true if delegate knows this format
apRect boundary; // Image boundary (if known)
unsigned int bytesPerPixel; // Bytes per pixel (if known)
unsigned int planes; // Number of planes (if known)
std::string format; // Additional image format information
};
The optional field, format, is used to hold other information about a storage format. This is
particularly important when an object, like apDelegateInfo, is shared among many objects.
This prevents apDelegateInfo from containing a large number of fields, most of which are
particular to a single image format. None of the image formats we support use the format
member, but it is still good practice to include it for future use.
READ()
The read() function reads an image into the specified apImage<> object. This is an excellent
example of using templates inside a non-template object. The user can specify an image of
any arbitrary type, and read() returns an image of that type. Most applications would use
info() to determine the image type before using read() to read the image data from a file.
A very nice feature of read() is that it lets you pass it the name of a file containing a color
image, and you receive a monochrome image in return. The color image is read, but converted
to the monochrome image and returned.
WRITE()
write() takes an apImage<> object and saves it in a particular image format. Optional
parameters can be passed in an apDelegateParams structure:
struct apDelegateParams
{
float quality; // Quality parameter
std::string params; // Other parameters
};
As with apDelegateInfo, we only place common functions directly in the structure. We added
the quality parameter, because it is needed when JPEG images are stored, but other storage
formats might use this parameter. We made quality a float to make it as useful as possible
(although this is an integer value for JPEG compression). params is intended to hold
Page 231
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
format-specific parameters and prevents the structure from getting too large.
You may have wondered why the read() and write() methods are not virtual. The answer
is that it is illegal. A member template function cannot be defined this way. Our definitions for
read() and write(), however, will call virtual functions. The complete definition for
apImageIOBase looks like this.
class apImageIOBase
{
public:
virtual apDelegateInfo info (const std::string& filename) = 0;
Page 232
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
function, make sure that the image file format you are using supports such a resolution.
When you look at the read() method, you can see how it figures out whether to call
readRGB() or readPel8(). By using typeid(), we are able to convert from a non-virtual
member template to a regular virtual function. When you have the ability to enable or disable
Run-Time Type Identification (RTTI), as with Microsoft compilers, you will need to enable RTTI
so that this function works properly. readRGB() is called if the pixel type is not a Pel8 or
apRGB, since a color image is more generic than a monochrome image.
To implement a new file delegate, you have to implement info(), write(), readRGB(), and
readPel8(), although often readPel8() can simply call readRGB().
Note that it is possible to extend this interface to include files that are stored in memory, but
that is beyond the scope of this book.
It is very nice to have a separate object to save or restore our apImage<> objects in each file
format. However, it is better if we can maintain a current list of available file formats. Using
our standard gOnly() technique, we write a repository object, apImageIODelegateList, to
keep track of which file delegates are loaded, as shown here.
class apImageIODelegateList
{
public:
static apImageIODelegateList& gOnly ();
private:
typedef std::map<std::string, apImageIOBase*> map;
typedef map::iterator iterator;
apImageIODelegateList ();
};
With this object, we can see whether a particular file format can be read or written.
getDelegate() returns a pointer to a file delegate object if the specified file type name
exists. The type name is simply the standard file suffix used by a file format (i.e., jpg for a
JPEG file).
You can extend apImageIODelegateList by adding read() and write() methods to handle
all known file formats. These methods would find an appropriate delegate for the specified file,
and then would call the delegate's corresponding read() or write() method to handle the
file.
One of the most common file formats is Joint Photographic Expert's Group (JPEG). JPEG can
store both monochrome and color images at various levels of compression. This format is
considered lossy, meaning that if you save an image as a JPEG file, and then read it back, the
image will be close but not identical to the original. For images intended for human viewing,
this is usually acceptable, especially if you limit the amount of compression. However, for
Page 233
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
many applications, including medical imaging and machine vision, a lossy compression method
should be avoided.
C and C++ API's are freely available and can be built on many platforms. Like most
frameworks, we are using the Independent JPEG Group implementation (http://www.ijg.org).
This is a pretty complicated API with many options. If this library does not already exist on
your system and pre-compiled binaries are not available, you will need to do the following:
1. Download the JPEG library from the Web site or the CD-ROM included with this book.
3. Read the building and installation instructions that come with the distribution.
4. Build and install. For UNIX systems, this is as easy as typing make as the first
command, followed by make install as the second.
The JPEG library is C-based and uses callback functions when errors or warnings occur. For
our purposes, we define callback functions that generate C++ exceptions that we later catch
in our file delegate object, as shown:
apJPEG is our file delegate object that creates an interface between our apImageIOBase
interface and the JPEG library. Its definition is shown here.
Page 234
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
apJPEG ();
~apJPEG ();
};
The implementation of these functions requires knowledge of the JPEG API. A rough outline of
the implementation is as follows:
FILE *infile;
if ((infile = fopen(filename.c_str(), "rb")) == NULL) {
return rgbImage;
}
We only need a single instance of any file delegate class, and gOnly() takes care of this for
us. Instead of using apJPEG directly, we access this object by means of the
apImageIODelegateList class. This gives us a way to automatically update our list of
available file delegates, because having to manually update this list whenever a file delegate is
added or subtracted is prone to error. Our solution is to take advantage of static initialization
by defining a static function that adds the delegate to apImageIODelegateList:
class apJPEG_installer
{
public:
static apJPEG_installer installer;
private:
apJPEG_installer ();
};
In our source file, we add:
apJPEG_installer apJPEG_installer::installer;
apJPEG_installer::apJPEG_installer ()
{
apImageIODelegateList::gOnly().setDelegate ("jpg",
Page 235
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
&apJPEG::gOnly());
}
During static initialization, apJPEG_installer::installer is initialized by calling the
apJPEG_installer constructor, which adds the JPEG file delegate to the list of supported file
types. apImageIODelegateList::gOnly() and apJPEG::gOnly() ensure that the Singleton
objects are constructed.
Get in the habit of not referencing apJPEG directly. The preferred method is:
apImageIOBase* delegate;
delegate = apImageIODelegateList::gOnly().getDelegate ("jpg");
if (delegate) {
...
}
This lookup converts the file type, .jpg in this case, to the object that handles this file
format. This approach is clearly advantageous when you need to manage many file formats.
The method readRGB() does more than it appears. This function returns an apImage<apRGB>
image, but it must be capable of reading the information contained in the file. The JPEG format
can store an image in multiple ways, two of which are supported by our class. Using the
nomenclature of the JPEG standard, the two color spaces that we support are a 24-bit color
image (same as apImage<apRGB>), and an 8-bit monochrome image (same as apImage<Pel8>
). readRGB() handles both cases and converts grayscale data to colors if necessary.
readPel8() could look very similar to readRGB(), in that it converts color data as it is
received to monochrome. However, we do not have to worry about performance as much with
save and restore operations, so we can take a huge shortcut:
The Tag Image File Format (TIFF) is another popular image format. It includes many internal
formats for storing both color and monochrome images. It can store images in both lossy and
loss-less fashion. The apTIFF object works like apJPEG, in that it handles the most common
cases of reading and writing a TIFF image. We handle the warning or error callback issue in
the same way by constructing an exception object, apTIFFException. Like the JPEG library,
the C source code is freely available for this format from http://www.libtiff.org or on the
CD-ROM included with this book.
Page 236
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
There are many third-party image processing packages available. The primary reasons to use
third-party libraries is to broaden functionality and improve performance.
INTEL IPP
In our examples, we use the Intel Integrated Performance Primitives (IPP) library, which
contains a large number of highly optimized, low-level functions that run on most Intel
microprocessors. Once you take a look at the results of running a Laplacian filter with a 3x3
kernel on a 1024x1024 image, you'll understand why we have chosen this library.
Table 6.2 shows the performance results you can expect using our framework's wrapper
functions to call the corresponding Intel IPP functions. Note that we performed these tests on
our Intel Pentium 4 processor-based test platform running at 2.0 GHz.
Before we deal with the specifics of IPP, let's discuss the broader design issue of how to
interface any external library with our framework. To explore this issue, let's use our generic
convolve() function and see how it changes, given different design strategies. Our original
definition for convolve() is:
If you want to fully integrate our framework with a third-party library, such that they operate
as a single piece of code, we would need to modify our generic convolve() function as
follows:
Page 237
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
if (!thirdPartyFramework()) {
apFunction_s1d1Convolve<R,T1,T2,S1,S2> processor
(ap_convolve_generic);
processor.run (src1, kernel, size, divisor, dst1);
}
else {
thirdPartyFramework()->convolve (...);
}
}
We modify convolve() to query thirdpartyFramework(), which returns a pointer to an
external library, or 0 if none is available.
Let's consider the issues with this strategy. Our example shows that the external convolve()
function is always called if an external convolve() function is available. What happens if the
external function is less efficient than our built-in version? Our function definition should really
include some kind of logic to determine which particular image processing function should be
called.
In addition, our function assumes that as long as the third-party library exists, it must also
support convolution. We should make sure to not only query the existence of the library, but
also to verify that it supports convolution.
These changes are not unique to convolve(); rather, we would need to make similar changes
to all image processing functions that we would like to use with our third-party library.
And given the extensiveness of the changes, it is unlikely that the third-party library will
exactly support our definition of an image. In our example, thirdpartyFramework() must
return an object, which takes apImage<> objects as parameters and converts them, as
needed, to an image format that is compatible with the image format used by the third-party
library.
It is a very expensive proposition to make two distinct pieces of code act together as one,
requiring numerous changes throughout the framework. This makes the solution prone to
errors and difficult to maintain or extend.
We attempted to create a tightly integrated design that was very similar to how we handled
file delegates. Although we don't highlight all the details in the book, we created a mapping
object that would track which processing functions were available. We also looked at
modifying our interface functions, like laplacian3x3<>, to detect whether an alternative
version was available. This scheme quickly became unworkable because of the number of
changes this approach forced us to make to the entire framework. Since many applications
should be able to use our framework directly, without the need for any image delegates, we
decided to abandon this strategy.
Because of these issues, we decided that the integration of a third-pary library should happen
at a higher level. Let's explore a loosely coupled solution that does just that.
Let's look at a solution that provides a very loose coupling of our framework and a third-party
library. We will leave both our framework and the third-party library unchanged. Instead, we
will create interface functions and objects that convert our apImage<> references to a form
compatible with the third-party library. In this solution, our original convolve() function also
remains completely unchanged, as do all of the other image processing functions. To fully
explore this solution, we use concrete examples with Intel IPP as the third-party library.
It turns out that the image definitions and memory alignment capabilities of the two
frameworks are very compatible. In the IPP, an image is an array of image pixels, rather than a
Page 238
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
separate, well-defined structure. In apImage<>, our image storage also requires an array of
image pixels. The memory alignment capabilities of apImage<> are also supported in the IPP.
IPP routines pass the pitch (or step, as the IPP documentation calls it) between rows. This is
the number of bytes needed to move from one pixel in the image to the pixel just below it on
the following row. While the IPP does not have specific memory alignment requirements, some
alignments will result in higher performance because of the architecture of the Intel
processors. Unless you must squeeze out every bit of performance, you can safely ignore the
alignment details and simply let the libraries handle the alignment issues.
If you really must be concerned about every bit of performance, here are some issues to
consider. Neighborhood processing routines, such as convolution, require preprocessing to
determine which pixels in the operation are used. (See our discussion regarding intersections
on page 213 for more information.) It is not possible to guarantee that the regions of interest
(ROIs) being processed will be aligned for optimal performance. But this does not mean that
the obvious solution of creating a temporary image will result in the performance gains of an
aligned image. Using an aligned image adds four processing steps: creating temporary
image(s), copying the pixels from the input images to the temporary images, copying the
result to the destination image, and, finally, deleting the temporary image(s).
Interfacing apImage<> to other image processing libraries will not always be this easy. If you
are fortunate, as we are in this example, the complexity will be limited to converting between
the apImage<> data structure and the third-party library. However, if the third-party library
uses an incompatible storage format, the underlying image data must be converted.
Regardless of the complexity, you should always implement an interface object to encapsulate
the details.
TRAITS
Our interface object, apIPP<>, converts an apImage<> object into a form usable by the Intel
library. Converting images is a simple operation in our case. IPP, however, is a C-interface
library that contains hundreds of functions. Therefore, it is very useful to encapsulate some of
these related datatypes into traits classes. Using traits, you can define one or more
properties (or traits) for a datatype.
Let's start by reviewing our IPPTraits<> object, which maps an apImage<> pixel type to one
used by the IPP, as shown.
Page 239
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
For example, apImage<apRGB> defines an image of RGB pixels. However, the IPP considers this
to be an image of bytes, taking three consecutive bytes as a color pixel. While the memory
layouts are identical, the compiler will generate an error if we try to pass an apRGB pointer to
an IPP function. So, we define a specialization that uses the native pixel types (Ipp8u for an
8-bit unsigned integer and Ipp32s for a 32-bit signed integer) for the IPP.
AP IPP<>
apIpp<> encapsulates our apImage<> objects and converts them into a form compatible with
the IPP as shown here.
enum eType {
eIPP_Unknown = 0, // Unknown or uninitialized type
eIPP_8u_C1 = 1, // 8-bit monochrome
eIPP_32s_C1 = 2, // 32-bit (signed) monochrome
eIPP_8u_C3 = 3, // 8-bit RGB
eIPP_32s_C3 = 4, // 32-bit (signed) RGB
};
ipptype* base ()
{ return reinterpret_cast<ipptype*>(align_.base());}
const ipptype* base () const
{ return reinterpret_cast<ipptype*>(align_.base());}
// Base address of image data, in the proper pointer type
protected:
void createIPPImage ();
Page 240
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
C REATE IPPIMAGE()
createIPPImage() is called by the constructor and allows IPP functions to use the
apImage<> pixel data. The implementation does little more than use Run-Time Type
Identification (RTTI) to map the pixel type to the IPP datatype name, as shown.
SYNC IMAGE()
After any image processing step that changes align_, the syncImage() method should be
called to make sure that image_ accurately reflects the results of the operation. If align_
and image_ point to the same storage, nothing needs to be done. The definition of
syncImage() is shown here.
Page 241
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
void apIPP<T,S>::syncImage ()
{
if (align_.base() != image_.base())
copy (align_, image_);
}
Now that we have apIPP<> to interface our apImage<> object with the IPP data structures,
we can turn our attention to writing an interface to some of its image processing functions.
We create a generic object, apIPPFilter<>, that defines an interface that can be used with
most IPP filtering functions. Most of the IPP functions are similar to the following prototype:
IPPFilter<> defines the call operator, operator(), to perform a specific image filtering
operation. The definition of IPPFilter<> is shown here.
Page 242
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
}
else if (src.isIdentical (dst)) {
// In-place operation
dest = duplicate (source, apRectImageStorage::e16ByteAlign);
method = eInPlace;
}
else if (source.boundary() != dest.boundary()) {
// Restrict output to the intersection of the images
dest.window (src.boundary());
source.window (dest.boundary());
method = eWindow;
}
apIPP<T> s (roi1);
apIPP<T> d (roi2);
d.syncImage ();
// Post-processing
switch (method) {
case eInPlace:
copy (dest, source);
dst = dest;
break;
default:
break;
}
return status;
}
protected:
enum eMethod { eRegular, eInPlace, eCreatedDest, eWindow};
Page 243
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
fixedFilterFunction func_;
};
IPPFilter<> is fairly straightforward. The typedef ipptype is identical to the one we find in
the traits class, IPPTraits<>, and is defined to make it easier to refer to the IPP datatype.
Note that you must use typename to equate ipptype with the corresponding type in
IPPTraits<>.
The typedef fixedFilterFunction defines what the IPP filter functions will look like, and
matches the prototype() function shown earlier on page 242.
You pass the desired IPP filter function to IPPFilter<> in its constructor, so that it can be
used by operator().
You may notice that we are only using a single template parameter, T, when referring to
apImage<>, and we are relying on using the default values for the other parameter. If your
images require non-default versions of the other parameter, you can modify this object very
easily.
operator() allows some flexibility in how the destination argument is specified, as follows:
If an output image is specified, the returned image will be the intersection of the
source and destination regions.
If the destination image is identical to the source image, the operation is performed in
place. Internally, this creates a temporary image before the image processing is
performed.
To keep track of what kind of processing is needed, the eMethod enumeration defines all of
the possible states we may encounter.
4. Synchronize our result with the destination image, in case a copy of the image was
made for alignment reasons.
apIPP<T> s (roi1);
apIPP<T> d (roi2);
IppStatus status = func_ (s.base(), s.pitch(), d.base(),
d.pitch(), s.imageSize(), S);
d.syncImage ();
You can now derive objects from IPPFilter<> to define objects for specific Intel IPP image
processing functions. For example, we derived an object that defines 3x3 and 5x5 Laplacian
Page 244
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
IPPLaplacian3x3 (fixedFilterFunction f)
: IPPFilter<T,ippMskSize3x3> (f) {}
};
IPPLaplacian3x3<Pel8> IPPLaplacian3x3<Pel8>::filter
(ippiFilterLaplace_8u_C1R);
IPPLaplacian3x3<apRGB> IPPLaplacian3x3<apRGB>::filter
(ippiFilterLaplace_8u_C3R);
IPPLaplacian5x5 (fixedFilterFunction f)
: IPPFilter<T, ippMskSize5x5> (f) {}
};
IPPLaplacian5x5<Pel8> IPPLaplacian5x5<Pel8>::filter
(ippiFilterLaplace_8u_C1R);
IPPLaplacian5x5<apRGB> IPPLaplacian5x5<apRGB>::filter
(ippiFilterLaplace_8u_C3R);
In the example above, you can see how our framework interfaces with the Intel IPP function,
ippiFilterLaplace_8u_C3R(), to compute the Laplacian image of an 8-bit RGB image.
We can see how easy it is to use our image delegate by looking at a comparison of techniques
for computing the Laplacian image of an 8-bit RGB image. To compute the Laplacian image
using our framework, we write:
apImage<apRGB> src;
apImage<apRGB> dst;
laplacian3x3<apRGBPel32s> (src, dst);
When we rewrite this example using our image delegate, the code looks similar; however, the
template parameter now specifies the pixel type instead of the internal pixel type required by
our framework. The rewritten example that computes the Laplacian image of an 8-bit RGB
image is as follows:
apImage<apRGB> src;
apImage<apRGB> dst;
IPPLaplacian3x3<apRGB>::filter (src, dst);
If we call the function, laplacian3x3<>, we call our built-in routine to compute the Laplacian
Page 245
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
An additional advantage in this particular example, is that both versions return the identical
image results, because the Intel IPP library has the same boundary behavior as our framework.
You cannot always count on this behavior from a third-party library, however, and it is best to
leave it up to the application to choose the appropriate function.
Our loosely coupled strategy gives us the freedom to call functionality either in our image
framework or from image delegates, depending on the application requirements. Many
applications need to call both kinds of functions, depending upon such issues as the
availability of image delegate run-time libraries, performance requirements, or accuracy
requirements.
You can extend our loosely coupled design to create a framework, using inheritance, that
enables the libraries to work efficiently together. Your application's framework may look similar
to the following:
class Image
{
public:
virtual void create (unsigned int width, unsigned int height);
protected:
apImage<Pel8> image_;
};
We have left out most details, but the idea is to create a wrapper object that manages a
specific type of apImage<> (in this case, it is an 8-bit monochrome image). You can place
whatever processing support you need in this object, or you can create a number of separate
functions to add this functionality. These functions are all virtual functions, allowing derived
classes to override their behavior as needed.
To handle the specifics of a third-party framework, such as the IPP, you create a derived
object like IPP_Image, as shown:
protected:
apIPP<Pel8> ipp_;
};
The derived object, IPP_Image in this example, can be very selective in what functions it
overrides, and in what contraints it chooses to enforce. Your application will use Image when
functionality from apImage<> is desired, and it will use IPP_Image when functionality from
both apImage<> and the image delegate (IPP in this case) is needed.
During our design phase, we spent a long time analyzing our image framework to determine
whether or not to build hooks for third-party image processing packages. We explored a fully
Page 246
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
integrated design that was very similar to how we handled file delegates. There were many
issues that arose from our design efforts, and we highlighted a few of them in the section
Strategy 1: A Fully Integrated Solution on page 238. This design proved difficult to manage,
and required extensive changes that rendered the use of image delegates more like a built-in
feature instead of its intended purpose as an extension.
Based on the issues that arose, we decided that the fully integrated design was unsuitable.
We explored a second strategy in the section Strategy 2: A Loosely Coupled Solution on page
239, which provided a loosely coupled connection between our framework and a third-party
library (in this case, the Intel IPP). We found this to be a successful strategy that not only
allowed us to build a general purpose framework, but also gave us the ability to provide
additional tools, through a third-party libary, that are necessary for building robust
applications.
[ Team LiB ]
Page 247
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
6.5 Summary
In this chapter, we finalized the design and implementation of the image class, the global
image functions, and the third-party interfaces (also called delegates). We showed many
examples where methods could also be implemented to leverage generic STL algorithms, in
addition to using those supplied with the image framework. We also made extensive use of
templates to define the constructs needed by our final image class, including clamping data
types to prevent overflows. This required an exception-safe locking mechanism, which we fully
discussed.
When we described global image processing functions, we provided a brief overview of the
algorithms, and then showed concrete implementations to support the algorithms. We also
provided visual feedback of the original image and the subsequently filtered image, letting you
decide the usefulness of any particular operation.
Finally, we discussed the appropriate design for integrating third-party software libraries with
the image framework. We showed a file delegate for supporting other file types, such as JPEG
or TIFF, and we showed an image delegate for supporting third-party image processing
libraries (Intel IPP, in our example). In addition to providing coding examples, this section
compared alternative techniques of using third-party software packages, and contrasted their
effects on implementation and future expandability.
In Chapter 7, we proceed to create a unit test framework to ensure the accuracy of the
global imaging functions you are now able to add. We also discuss specific techniques,
showing coding examples of each, for improving the performance of your code.
[ Team LiB ]
Page 248
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
C++ Concepts
Performance Tuning
Macros
GUI Considerations
No software development effort is complete until you do some testing and performance tuning.
This chapter provides guidelines for unit tests and creates a framework for automatically
running them. In addition, we provide thirteen specific techniques that you can apply to your
code to optimize performance.
[ Team LiB ]
Page 249
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Any piece of code checked into the main code base must have a corresponding unit
test.
Code cannot be checked into the main code base until it passes its unit tests.
Unit tests should be kept current, being updated as the code is updated.
Unit tests should be part of a framework that runs regularly (every night, every week,
and so on) and reports meaningful results.
int main()
{
... test some stuff
return 0;
}
A simple framework can lend organization to your unit test strategy. We include a unit test
framework with the following features:
A few simple macros are included that make it easy to verify certain conditions and
report any that are not correct.
1. Write one or more unit test functions using the functionality provided:
UTFUNC() Creates a unit test function of the specified name and adds
the function name to the list of functions to run. UTFUNC()
actually creates a small object, but this detail is hidden.
setDescription() Specifies what the unit test does. This string is displayed
when the test is run.
Page 250
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
2. Run the unit test functions. The execution time is computed and any exceptions are
caught. If a unit test has no VERIFY() statements, the result is set to eUnknown. If an
exception is thrown, the unit test result is set to eFailure. Possible results for unit
tests are as follows:
eNotRun The default state if the unit test has not been run.
3. Use main() to call apUnitTest::gOnly().run(), which runs all of the unit tests in
the framework object. The results are then written to the console (or other stream).
main() also returns the cumulative state of the unit test framework object as 0 if
there are any failures or unknowns, or as 1 if there are only successes.
EXAMPLE
We have written unit tests for almost every component we present in this book. All
unit tests are included on the CD-ROM. Here, we use one of the apBString unit
tests as an example:
UTFUNC(Pel8)
{
setDescription ("Pel8 tests");
Pel8 b;
Pel16 w;
Pel32 l;
Pel32s ls;
float f;
double d;
std::string s;
apBString bstr;
bstr << (Pel8) 123;
VERIFY (b == 123);
VERIFY (w == 123);
VERIFY (l == 123);
VERIFY (ls== 123);
VERIFY (f == 123);
VERIFY (d == 123);
VERIFY (s.compare ("123") == 0);
Page 251
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
bstr >> b;
VERIFY (bstr.eof());
}
This function tests that a byte can be written to the binary string and then read
back in multiple formats, to verify that the conversions were made correctly.
You can use the provided framework to write your own unit tests. Our framework encourages
you to write a number of small, isolated tests instead of a large complex test. We strongly
recommend that you test as much as possible in your unit tests, as we demonstrate in the
following portion of the apRect unit test:
UTFUNC(rect)
{
setDescription ("Rect");
UTFUNC(defaultctor)
{
setDescription ("default ctor");
apRect rect;
VERIFY (rect.x0() == 0);
VERIFY (rect.y0() == 0);
VERIFY (rect.width() == 0);
VERIFY (rect.height() == 0);
...
}
Your unit test file contains one or more UTFUNC() functions as well as a main() function. If
you want to include any custom pre- or post-processing, you can do so, as follows:
int main()
{
// Add any pre-processing here
bool state = apUnitTest::gOnly().run ();
// Add any post-processing here
apUnitTest::gOnly().dumpResults (std::cout);
return state;
}
apUnitTest is a Singleton object that contains a list of all unit tests to run. The results for
each unit test are stored internally and can be displayed when dumpResults() is called. Unit
test functions should not generate any output on their own, unless that is the point of the
test. Any extra input/output can skew the execution time measurements.
Page 252
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Figure 7.1 illustrates the overall design of the unit test framework.
There is a base class, apUnitTestFunction, from which unit tests are derived using the
UTFUNC() macro. There is a unit test framework object, apUnitTest, that maintains a list of
all the unit tests, runs them, and displays the results. Each of these components is described
in this section.
Each unit test is derived from the apUnitTestFunction base class using the UTFUNC() macro.
The complete apUnitTestFunction base class is shown below.
class apUnitTestFunction
{
public:
apUnitTestFunction (const std::string& name);
Page 253
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
void apUnitTestFunction::run ()
{
std::string error;
apElapsedTime time;
try {
test ();
}
catch (const std::exception& ex) {
// We caught an STL exception
error = std::string("Exception '") + ex.what() + "' caught";
addMessage (error);
result_ = eFailure;
}
catch (...) {
// We caught an unknown exception
error = "Unknown exception caught";
addMessage (error);
result_ = eFailure;
}
elapsed_ = time.sec ();
apUnitTest Object
Our unit test framework object, apUnitTestObject, maintains a list of unit tests, runs all of
the unit tests in order, and displays the results of those tests. Its definition is shown here.
class apUnitTest
{
public:
static apUnitTest& gOnly ();
Page 254
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
private:
apUnitTest ();
static apUnitTest* sOnly_; // Points to our only instance
bool apUnitTest::run ()
{
bool state = true;
EXAMPLE
In this example, we look at the output for the apBString unit test. (Note that we
include the complete source code for the apBString unit test on the CD-ROM.)
Running the unit test produces the following output:
[View full width]
0 sec
Test 2: Success : Pel8 : Pel8 tests : 0 sec
Test 3: Success : Pel16 : Pel16 tests : 0 sec
Test 4: Success : Pel32 : Pel32 tests : 0 sec
Test 5: Success : Pel32s : Pel32s tests : 0 sec
Test 6: Success : float : float tests : 0 sec
Test 7: Success : double : double tests : 0 sec
Test 8: Success : string : string tests : 0 sec
Test 9: Success : eof : eof tests : 0 sec
Test 10: Success : data : data tests : 0 sec
Test 11: Success : bstr : bstr tests : 0 sec
Test 12: Success : dump : dump tests : 0 sec
Test 13: Success : point : point tests : 0 sec
Test 14: Success : rect : rect tests : 0 sec
The execution times are all reported as 0 because each test is very simple. This unit test
framework is portable across many platforms and the results are similar on each platform.
We can simulate a failure by adding a simple unit test function to our framework, as shown:
Page 255
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
UTFUNC (failing)
{
setDescription ("Always will fail");
VERIFY (1 == 2);
}
The output would include these additional lines:
Test 15: ***** Failure ***** : failing : Always will fail : 0 sec
Messages:
1 == 2
The unit test framework uses two macros: UTFUNC() and VERIFY(). In general, we tend to
avoid macros; however, they are very useful in our unit test framework. Figure 7.2 provides a
quick overview of the syntax used in macros.
Note that parameters used in macros are not checked for syntax; rather, they are treated as
plain text. Parameters can contain anything, even unbalanced braces. This can result in very
obscure error messages that are difficult to resolve.
The UTFUNC() macro creates a unit test function of the specified name by deriving an object
from the apUnitTestFunction base class. UTFUNC() is defined as follows:
#define UTFUNC(utx) \
class UT##utx : public apUnitTestFunction \
{ \
UT##utx (); \
static UT##utx sInstance; \
void test (); \
}; \
UT##utx UT##utx::sInstance; \
UT##utx::UT##utx () : apUnitTestFunction(#utx) \
{ \
apUnitTest::gOnly().addTest(#utx,this); \
} \
void UT##utx::test ()
Page 256
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
For example, the preprocessor expands the UTFUNC(rect) macro into the following code:
The VERIFY() macro is much simpler than UTFUNC(). It verifies that a specified condition is
true. Its definition is as follows:
setDescription() is a method that lets you include more descriptive information about the
test. It is very useful if you have a number of tests and wish to clarify what they do.
Page 257
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
The unit test framework that we have included is only a beginning of a complete solution. We
recommend using this framework as a basis to construct a fully automated unit test framework
that runs unit tests at regular intervals, such as each night. An automated framework would
do the following:
Obtain the most recent sources for your application. This involves obtaining a copy of
the sources from whatever configuration management package you use.
Execute all unit tests, capture the results, and record which unit tests fail.
Our experience is that at least half of all unit test failures are not actually failures in the
objects being tested. Failures tend to occur when an object has been modified, but the unit
test lags behind.
When updating your code, update the unit test at the same time.
[ Team LiB ]
Page 258
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Let's look at it another way. If you want to train to become a sprinter, you know that you
need to run very fast for a relatively short period of time. If you were to write a list of goals
for yourself, would you include a statement that says you should "run as fast as possible"? Of
course you wouldn't. You would probably say that you need to be able to run a certain
distance in a certain amount of time. And based on this goal, you can set up a training
program to meet it.
Writing software is no different. You need to have plausible goals and then you can design a
plan to reach them. Your goals can be difficult, but not impossible, to achieve. Sometimes a
goal may seem impossible, but further investigation reveals it is actually possible, though
extremely challenging. Having a goal that is well defined is absolutely essential for getting the
performance you need from your application. See [Bulka99] for useful information on writing
efficient C++ code.
For example, let us look at how a customer interacts with a graphical user interface (GUI). We
see that the overhead of the framework and the way you write your code often has little
effect on performance. The customer communicates with the software by making various
requests in the form of mouse or other pointer manipulation or keyboard input. For complicated
interfaces, these requests occur at no more than one request per second. The steps that the
software takes to process such a request can be listed as a series of events:
If the customer generates events at the rate of one request per second, then this sequence
of events, including updating the user interface, must happen in no more than half that time.
Where did this number, .5 seconds, come from? It is simply a guess based upon our perception
of how customers operate such systems.
Page 259
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Now, without worrying about specific operating systems or GUI implementations, we can make
some assumptions about how long it takes to handle each of the above steps. Receiving and
invoking an event handler is a fast operation, even when written in a general purpose C++
framework. This step comprises a number of table lookups, as well as one or more virtual
function calls to locate the owner of an event. It certainly does not consume a lot of time.
Processing the event is strictly an application-specific task, as is updating the user interface.
The amount of overhead that can be tolerated in this example is fairly large. The customer will
have a very hard time distinguishing between one millisecond and 50 milliseconds.
As a contrasting example, let's look at a real-time system that has hard performance
requirements. As opposed to the previous example, where there is a person waiting to see
results updated on the screen, these results are needed in a certain period of time, or else
the information is useless. The framework's overhead, as well as how the processing is
written, is important. However, it is not so obvious how much overhead is acceptable.
We have found that dealing with percentages makes it easier to gauge how overhead can be
tolerated. If your processing function does not spend 98 to 99 percent of its time doing actual
work, you should examine your design more closely. For example, for very fast processing
cycles, say five milliseconds, the framework overhead should be kept to less than 100
microseconds. For slower real-time systems that require about 50 milliseconds to execute,
overhead should be less than one millisecond. The design of each of these systems will be
very different.
To measure overhead for an image processing system, or other system that performs a
repeated calculation on a number of samples, it is customary to compute the overhead on a
per row or per pixel basis. Let us assume that we must perform one or more image processing
steps on a 512x512 pixel image. If we want to keep the total overhead to one millisecond, the
per-row overhead can be no more than two microseconds. Two microseconds is quite a bit of
time for modern processors, and this permits numerous pointer manipulations or iterator
updates. We are not so lucky if we have a goal of only two-tenths of a microsecond per row.
In this case, you should consider optimizing your code from the onset of the design.
If you find this calculation too simplistic for your taste, you can write some simple prototypes
to measure the actual performance on your platform and decide if any code optimization is
needed. Since many image processing functions are easy to write, you can get a sense for
how much time the operation takes. Our unit test framework is a convenient starting point,
since the framework computes and reports the execution time of each unit test function. To
get started, you would need to write at least two functions. The first function would contain
the complete operation you want to test. The second function would contain just the
overhead components. The execution time of the first test function tells us how long the
image processing takes, while the second tells us if the overhead itself is significant. If our
unit test framework was more complicated than it is, we would also need a third function to
measure the overhead of the unit test framework itself. But, since the framework is so simple,
its overhead is negligible.
Page 260
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Measure performance with release builds, not debugging builds. Debug builds usually
have more overhead from the optimizations that are employed, the maintaining of
debugging symbols, and the code intended only for debugging. The difference in
execution time between release and debug builds can be enormous. For compilers, such
as Microsoft Developer Studio, switching between either type of build is easy. For UNIX
makefiles, make sure that debugging symbols are excluded and that optimizations are
enabled.
Compute only those items that you need, especially in functions that take a long time
to execute or are called frequently by your application. It is very common to write a
function with the desire to be complete. If you find that certain functions are returning
quantities that are never used, you should investigate whether these quantities are
needed at all.
For example, suppose you have a function that computes the mean and standard
deviation of a number of samples. If you always find yourself throwing out the standard
deviation value, then you have to ask yourself why you are computing it. There is no
reason why you cannot have two similar functions: one that computes just the mean
of a sample set, and another that computes the mean and standard deviation.
Compute or fetch items only once. It is better to explicitly use a local variable in your
function than to assume your compiler can divine which quantities are invariant. For
example:
Page 261
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
for (unsigned int y=0; y<image.height(); y++)
for (unsigned int x=0; x<image.width(); x++)
// ...
In this example, the height() member is called once for every row in the image.
width() is called for every pixel in the image. In this particular example, height() is a
simple inline method, but this is not always the case. If you are concerned about
performance, you can rewrite this loop as:
Use integers instead of floating point when possible. This used to be much more
important than it is now, but many microprocessors have more integer registers than
floating point ones, and they can be used in many more ways. On embedded platforms,
you might also consider using the smallest integer format possible.
For example, if you need to sum all the pixels in an image row, you might be able to
use an unsigned short instead of an unsigned int. In our own development
projects, we usually consider the size of most variables and use one of a sufficient
size, but no larger. As an aside, we ignore this rule for counter variables and just use
an int or unsigned int.
Know and understand the objects you are using in time-critical code. For example, you
don't want a copy constructor or assignment operator to take the bulk of the
processing time. If objects are anything other than very simple, try using references (
const if possible) or pointers to avoid copies. This is not a big concern with our
apImage<> object because it takes advantage of reference counting.
Remove all debugging and log generation code from time critical code, once you have it
debugged. Otherwise, make sure you use #if to compile this code only for debug
builds.
Deliver release versions of your application internally as early as possible. Internal users
can provide valuable feedback to you regarding performance of which you may not be
aware. There's nothing like a real user banging on the system to bring performance
issues to light.
Avoid excessive operations with heavyweight objects, like the standard library string
class. Many operations, like searching a large string for a substring, should be called as
few times as possible. For example, if you are parsing through a large string, keep track
of the last index where you found a match, so that you only need to search from that
point in the string.
Page 262
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Minimize locks in time-critical sections of code. The process of locking is fast only if no
one currently owns the lock. A section of code can potentially wait forever before it
acquires the lock.
Develop an efficient algorithm that works correctly before you start fine tuning your
code. Use unit tests to ensure that the tuning hasn't broken anything. There are many
ways to solve the same problem. You may find that optimizing a poorly designed,
well-implemented algorithm will never be as efficient as a well-designed, poorly
implemented algorithm.
Use a profiler to find out where the biggest performance problems are before doing a
lot of small optimizations. Often 90 percent of the execution time for an operation
takes place in only 10 percent of the code. Make sure you are spending your time
optimizing the right section of code.
SLOW
The easiest way to write this, and probably the slowest, is:
UTFUNC(slow)
{
setDescription ("Slow version");
Page 263
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
ORIGINAL
UTFUNC(original)
{
setDescription ("Original version, sum all pixels");
apImage<Pel8>::row_iterator i;
for (i=byteImage.row_begin(); i != byteImage.row_end(); i++) {
const Pel8* p = i->p;
for (unsigned int x=0; x<byteImage.width(); x++) {
sum += *p++;
}
}
}
OVERHEAD
Now, we rewrite the original function, so that it has the same setup, but performs no image
processing, in order to measure overhead. Note that we perform one trivial operation per row,
to make sure that the compiler doesn't optimize the loop for us.
UTFUNC(overhead)
{
setDescription ("Overhead version, sum all pixels");
apImage<Pel8>::row_iterator i;
for (i=byteImage.row_begin(); i != byteImage.row_end(); i++) {
const Pel8* p = i->p;
dummy += *p++; // Prevents compiler optimization of loop
Page 264
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
}
}
VERIFY (true); // So this test will pass
}
The execution time of original includes the execution time of the unit test framework, as
well as the actual image processing operation. The execution time of overhead also includes
the unit test framework overhead, but only includes the overhead portion of the image
processing operation, and not the actual time required for the operation. If the overhead of
the unit test framework is significant, then we need a third function that measures the
overhead of the unit test framework. Because our framework has very low overhead, we can
skip this third test.
ORIGINAL_WIDTH
Let's further optimize the function by removing width() from the loop and placing it into a
local variable, as shown.
UTFUNC(original_width)
{
setDescription ("Original version with width()");
apImage<Pel8>::row_iterator i;
for (i=byteImage.row_begin(); i!=byteImage.row_end(); i++) {
Pel8* p = i->p;
for (unsigned int x=0; x<w; x++) {
sum += *p++;
}
}
}
VERIFY (true); // So this test will pass
}
Let's do one more optimization.
ORIGINAL_UNROLLED
We can do some basic loop unrolling by expanding the inner loop, so that multiple pixels are
handled in each iteration, as shown.
UTFUNC(original_unrolled)
{
setDescription ("Original version with unrolling optimization");
Page 265
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
apImage<Pel8>::row_iterator i;
for (i=byteImage.row_begin(); i!=byteImage.row_end(); i++) {
Pel8* p = i->p;
for (unsigned int x=0; x<w; x++) {
sum += *p++;
sum += *p++;
sum += *p++;
sum += *p++;
sum += *p++;
sum += *p++;
sum += *p++;
sum += *p++;
}
}
}
VERIFY (true); // So this test will pass
}
As written, this version works for images whose width are multiples of eight pixels. Extending
this to work with an arbitrary pixel value would not be difficult.
Execution Results
We ran these tests using Microsoft Visual Studio 7.0 on an Intel Pentium 4 processor-based
machine, running at 2.0 GHz. Table 7.1 shows the times for a single iteration. The actual times
are lower than this, because we shut off optimization to prevent the compiler from optimizing
the loops.
Let's look at some of the execution times for our existing image processing functions. Table
7.2 shows the execution times for a Laplacian filter running on a 1024x1024 8-bit monochrome
image.
convolve() 89 milliseconds
Page 266
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
laplacian3x3() 51 milliseconds
You really need to measure the slowest code path in your code to get the worst case
time. Our filter example is very simple, yet it has several possible code paths. For
example, you should let the filter function determine the size and allocate the
destination image. Likewise, if you are using a clamped pixel type, you should test your
filter so that every pixel in the image causes an overflow.
Multithreaded and multiprocess designs "steal" processor time from the code you are
trying to measure. If these threads or processes are not part of the actual application,
you should test your function in isolation. If you are utilizing other threads or
processes in your design, you won't be able to get actual worst-case values until you
simulate these conditions in your unit test. You will also need to compute the
execution time of each iteration and store the worst case value.
Interrupt service routines should also be considered if you are working with an
embedded system that has more than simple interrupt routines. Like we mentioned
above, you will need to measure the worst case execution time by simulating the
actual conditions.
The timing routines defined in time.h provide standard, but very coarse, measurements of
elapsed time. Most platforms include higher resolution facilities to measure time, and if you are
designing for a single platform, we encourage you to take advantage of them. For example,
under Microsoft Windows, you can take advantage of timers that provide sub-microsecond
resolution. Instead of using this functionality directly (QueryPerformanceCounter), you
should create a generic wrapper object and reuse the interface on other platforms, as shown.
class apHiResElapsedTime
{
public:
apHiResElapsedTime ();
// Record the current time
Page 267
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
private:
LONGLONG starting_; // Starting time
};
void apHiResElapsedTime::reset ()
{
LARGE_INTEGER t;
QueryPerformanceCounter (&t);
starting_ = t.QuadPart;
}
[ Team LiB ]
Page 268
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
7.3 Summary
In this chapter, we provide a practical strategy for integrating unit tests into your software
development cycle. We design a complete unit test framework, providing the details on why
certain C++ constructs were used in the implementation. We provide guidelines for extending
the framework to work in your software development environment. We also include a short
primer on macros, as the implementation of the unit test framework relies on two macros.
We also focus on performance issues, both in the context of soft performance constraints (of
typical applications) and hard performance constraints (of real-time systems). We detail
specific techniques, providing a checklist on page 261, that you should consider in any domain
to improve the run-time performance.
In Chapter 8, we explore those topics that we felt warranted more detailed discussion than
we could provide at the time they were introduced in the book. These topics include: copy on
write, caching, explicit keyword usage, const, and pass by reference. We also include a
section on extending the image framework with your own processing functions. Because we
thought it might be of interest, we've highlighted some routines that work particularly well for
enhancing your digital photographs.
[ Team LiB ]
Page 269
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
C++ Concepts
Copy on Write
Caching
Const Usage
Morphology
Image Transformations
Unsharp Masking
In this chapter, we explore topics that we touched on earlier, but strongly warrant further
discussion. In addition to going into detail about specific C++ constructs, we discuss ways
that our image processing framework can be extended.
[ Team LiB ]
Page 270
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
buffer2.duplicate ();
buffer2[0] = 0;
This can become very time intensive if we have to manually duplicate memory every time we
plan on changing the contents of a buffer. This is where copy-on-write semantics can help.
We can extend our apAlloc<> class to add this capability, but we will also be faced with
some limitations. This topic is discussed fully in More Effective C++. See [Meyers96].
You can rewrite the example to make sure that the const and non-const versions are called
Page 271
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
at the appropriate times. We use a const apAlloc<> object whenever we only read the
contents of the object, as follows:
In More Effective C++ [Meyers96], Meyers discusses a technique of using proxy classes to
decide how operator[] is being used (either as an lvalue or rvalue). This technique requires
that we return a class object that, based on operator overloading, determines which form is
being called. While this solves the problem, it does add some additional complexity.
Leaving out copy-on-write semantics from our apAlloc<> class was done on purpose.
Providing the compiler with enough information to always do the right thing makes the
apAlloc<> class much more complicated. The last thing we want to do is write code that may
become difficult to maintain because of its complexity. We believe it is much better to specify
what apAlloc<> does and what it does not do.
A Practical Solution
If you really must have copy-on-write in apAlloc<>, here is an alternative way to do it. It
does require an additional step, but fortunately, the compiler will always complain if you should
forget. The const methods to access data are no problem:
T* dataRW ();
The RW suffix indicates that the pointer is for reading and writing. By moving the decision of
what kind of pointer is needed from the compiler to the developer, we remove the confusion
about what is happening, since we no longer offer two versions of a data() function.
operator[] also looks different because it takes an apIndex object as an argument, rather
than a simple numerical index as shown:
class apIndex
{
public:
explicit apIndex (unsigned int index) : index_ (index) {}
operator unsigned int () const { return index_;}
private:
unsigned int index_;
};
apIndex is really just a proxy for an unsigned int. It is critical that we use the explicit
Page 272
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
keyword in the constructor, to prevent the compiler from implicitly using this constructor to
convert an integer into an apIndex object. If this were to ever happen, the non-const
version of operator[] might incorrectly be called.
Our sample code is now slightly different, with references to buffer[0] being replaced by
buffer[apIndex(0)], as shown:
buffer[0] = 5;
the compiler would issue an error, because a non-const reference can only be returned when
an apIndex object is used. Using the apIndex type as an array index lets the compiler know
that you intend to modify the array. This solution, although not perfect, lets us minimize the
complexity of our apAlloc<> object.
EXAMPLE
Let's take an example of an image processing function and look at how caching
makes it more efficient. The following function computes a histogram of all pixels in
the image. The histogram always has 256 entries and represents the number of
times each pixel value occurs. Our function converts the image to an 8-bit
monochrome image before the histogram is computed. The histogram() function is
as follows:
template<class T>
unsigned int* histogram (const apImage<T> image)
{
static unsigned int counts[256];
for (unsigned int index=0; index<256; index++)
counts[index] = 0;
Page 273
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
apImage<Pel8> mono;
copy (image, mono);
apImage<Pel8>::row_iterator i;
for (i=mono.row_begin(); i!=mono.row_end(); i++) {
Pel8* p = i->p;
for (unsigned int x=0; x<mono.width(); x++)
counts[*p++]++;
}
return counts;
}
This simple function is deficient in a few ways (for example, it duplicates the image
even if it already is of type Pel8), but this is not relevant for our discussion. Let's
look at how we might use such a function:
int main ()
{
apRect boundary (0, 0, 1024, 1024);
apImage<apRGB> src (boundary);
src.set (apRGB(1,2,3));
return 0;
}
On the surface, histogram() computes an array of values given an image. The fact
that it computes a temporary image is hidden. However, computing this temporary
image can consume quite a bit of time. The way we have defined histogram(), it
always computes an apImage<Pel8> image. One obvious performance enhancement
is to test if the image is already an image of type Pel8. If so, there is no need to
copy this image. For other image types, however, a copy is always necessary.
Our need to cache intermediate copies of images can be generalized into a generic framework.
It should have the following features:
If the object is not available, compute this quantity, then save it in the cache.
After we discuss the general framework, we apply it to an image processing example, where
we attach a cache object to each instance of an image to hold the list of available images.
C AC HING OBJEC T
Page 274
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
apCache keeps track of one or more cached quantities, called apCacheEntryBase* objects.
Each of these cached quantities is maintained as an object derived from apCacheEntryBase,
as shown.
class apCache
{
public:
enum eDeletion { eAll, eStateChange, eDataChange};
// Each entry can decide when it will get deleted from the cache
apCache ();
~apCache ();
protected:
Cache cache_; // List of cached items
};
return (i->second);
}
It is equally important to decide when information should be deleted from the cache. For
example, if a function changes the original image that was used to compute a derived
quantity, the cached image is no longer valid and must be recomputed. apCache has a limited
understanding of what is contained in its cache. The clear() method is included to delete
items from the cache, as shown.
Page 275
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
{
// Delete some or all of our objects. This loop is different
// because we can't delete the iterator we're on.
// See Meyers, Effective STL, Item 9
Cache::iterator i;
for (i=cache_.begin(); i!=cache_.end();) {
apCacheEntryBase* entry = i->second;
if (deletion == eAll || entry->deletion() == eAll ||
entry->deletion() == deletion) {
delete (i->second);
cache_.erase (i++);
}
else
++i;
}
}
You decide what items are deleted by using the state of the cached object and the value of
deletion, which can be any of the following:
eStateChange - The state of the object has changed. If this cache contained images,
it means that the pixel data has not changed, but other features, such as its origin
point, have changed.
eDataChange - The object's data has changed. If this cache contained images, it
means that the actual pixels of the parent image have been altered.
Note that, as the comment indicates, the loop must be written this particular way, since it is
possible to delete the current item we are examining in cache_.
Now we can look at the objects that actually get cached. We have seen that apCache keeps
track of zero or more apCacheEntryBase* objects. The main purpose of apCacheEntryBase is
to act as the base class for any cache object, as shown here.
class apCacheEntryBase
{
public:
apCacheEntryBase (apCache::eDeletion deletion);
virtual ~apCacheEntryBase ();
protected:
apCache::eDeletion deletion_;
};
apCache maintains pointers to objects derived from apCacheEntryBase in cache_. We use
pointers to avoid copies, since these cached objects might contain very large and difficult to
copy features. apCache::clear() calls delete on these pointers in order to free them.
Page 276
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Templates once again allow us to be very flexible about what is contained in a cache entry,
as shown here.
template<class T>
class apCacheEntry : public apCacheEntryBase
{
public:
apCacheEntry (T& object,
apCache::eDeletion deletion = apCache::eAll)
: apCacheEntryBase (deletion), object_ (object) {}
virtual ~apCacheEntry () {}
protected:
T object_;
};
As you can see, one copy of the cached item, object, is made during construction when it
gets stored in object_. The deletion criteria can also be specified, with the default behavior
that any changes to the parent object cause the entry to be deleted. Cached elements are
returned as base class objects. They must be cast in order to retrieve the specific information
contained inside. This is not a problem, since the function that is trying to access this value is
also capable of storing it, so the function must understand the details.
As you might expect, there are a number of important issues that you must understand when
writing code that caches derived objects. Let's look at example to clarify things.
EXAMPLE
Let's rewrite the histogram() function we used in our previous example. Instead of
caching an apImage<> object, we will store an apImageStorage<> object because
it contains all the necessary information, as shown.
template<class T>
unsigned int* histogram (const apImage<T> image)
{
static unsigned int counts[256];
for (unsigned int index=0; index<256; index++)
counts[index] = 0;
apImage<Pel8> mono;
Page 277
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
(cached);
mono = apImage<Pel8>(entry->object());
}
apImage<Pel8>::row_iterator i;
for (i=mono.row_begin(); i!=mono.row_end(); i++) {
Pel8* p = i->p;
for (unsigned int x=0; x<mono.width(); x++)
counts[*p++]++;
}
return counts;
}
This technique makes the histogram() function more complicated, but also more
efficient. cacheKey is the string name we use to see if an image is already in the
cache. We use apCache::find() to see if the item is already in the cache.
If the item is not in the cache (i.e., cached is zero), we compute the monochrome
image and save it in the cache. The template parameter for our apCacheEntry is
apImageStorage<Pel8>, as shown.
If the item was in the cache, we make mono point to the storage object we obtain
from the cache, as shown.
[View full width]
cached);
mono = apImage<Pel8>(entry->object());
The rest of the histogram() function remains the same. While caching is clearly a
useful performance and efficiency enhancement, it is also obvious that it makes the
design more complex. These trade-offs have to be considered when approaching
the design of your application.
To add caching to our image object, apImage<>, we need to make every instance of
apImage<> contain an apCache instance. We must also modify every image processing
function that uses an intermediate image, to first check if the image is already available in the
cache before computing it.
And, we still have to worry about making sure the cache is cleaned up when the image is
changed. Let's take a look at how we might add caching to our image object.
We start by adding a number of new methods to apImage<> to manage the apCache object,
cache_, as shown:
Page 278
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
If your application computes the same types of images many times, then you should consider
adding caching at the application level of your design. In this case, it would be a mistake to
add this functionality within the image object. Making apImage<> cache every image it
computes would mean that many of the cached images would never be used. The point of
caching is to reduce the number of times we compute temporary images. Caching unused
images would defeat the whole purpose.
If you are still not convinced, consider objects with very long lifetimes. These objects are
used for many operations and persist in the system for a long time. If these objects cache
quantities that are only used once, the application can find itself exhausting memory, without
the addition of yet another component to make sure that cached entries do not stay around
too long.
In this section, we have highlighted only some of the changes required to add caching to the
image framework by means of the apImage<> object. Instead of adding caching to apImage<>,
you should consider caching certain images in your application, such as:
Monochrome images derived from color images or vice-versa. Since many image
processing operations are written to only work on one type of image, caching a copy
of it is a good idea if you expect to perform numerous computations on it.
Images with a specified image alignment. We have said many times that some
algorithms run faster when their storage is aligned in memory. If you take the time to
compute an image with a specific alignment, you should consider caching a copy in
your code to use it again. Our Intel IPP interface is a perfect example. If you pass an
improperly aligned image to an IPPFilter<T> object, a copy of this object is made. If
possible, you should consider caching if you expect to call many apIPP methods using
the same image.
[
Tea
m LiB
]
Page 279
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
explicit keyword
const
The compiler is using a defined conversion operator to convert the object from one
type to another.
At times, it is undesirable for the compiler to implicitly convert an object from one type to
another. If the compiler finds a way to perform an implicit conversion, it will do so, regardless
of whether or not the conversion is correct. After all, the compiler does not understand the
specific meaning of a constructor, only that it can be used to perform a conversion. Let's look
at an example where using explicit can address these issues.
EXAMPLE
Let's create a testString object. This object is nothing but a wrapper around
std::string with a single operation, as shown.
class testString
{
public:
testString ();
testString (const std::string& s) : string_ (s) {}
testString (const char* s) : string_ (s) {}
testString (int size, const char c=' ') : string_ (size, c) {}
Page 280
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
With this usefulness, comes some pitfalls. For example, you can also write:
testString d = a + 5;
While the intention of this line is completely unclear, the compiler will successfully
compile it. This line of code produces the same results as:
Another place where explicit can remove confusion is during template instantiation. When
you define a template object, you create a generic template that can be used for any data
type. It is not uncommon to create a template object with the intention of using only a few
specific data types. A problem can occur when a new data type is used for the first time.
You can use a technique, like we do, of letting the compiler tell you what functionality is
missing that prevents a template instantiation from working. The compiler will try to do simple
conversions automatically, and it is possible that it will successfully create an instantiation by
applying the incorrect conversions.
If you do have a single argument constructor, or a constructor that the compiler will treat as
if it had a single argument, you should review whether or not the compiler is making the
appropriate conversion. To ensure that it does, use the explicit keyword. If the compiler
finds that you are attempting this conversion, and this is your intention, explicitly call the
constructor rather than removing the explicit keyword.
Page 281
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
software, we go back to the unit tests and change the code, such that any object that is not
altered by a method is defined as const. We define a method as being const if it looks like a
const object to the client (i.e., the code using this object). The compiler enforces that there
can be no changes to the const object.
EXAMPLE
Let's look at an example where we need to be able to change the value of a
variable within a const function.
class apTest
{
public:
apTest () : value_ (0), counter_ (0) {}
void set (int v) { value_ = v;}
int get () const { counter_++; return value_;}
...
private:
int value_;
int counter_;
};
apTest is a simple repository for an integer, int, accessed by means of get() and set()
methods. However, the object also keeps track of the number of times that get() is called.
From the standpoint of the code that uses apTest, the get() method is constant.
However, in this example there is an internal variable, used for debugging or perhaps caching,
that is changing. Because of this, the compiler will not allow you to define this function as
const. This is where the mutable keyword is used. The mutable keyword defines a variable
that can be changed inside a const function. This is exactly what we are trying to do. All we
have to do is define counter_ as:
Guidelines
Use const references for any function arguments that do not change. The const
portion will tell the compiler to make sure the value is not altered, and the reference
will prevent the copy constructor from getting called when the function is called. An
Page 282
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
example is:
void set (const std::string& string);
void set (const apImage<T>& image);
Examine all functions to see if they can be made const. It is easy to miss them. By
labeling a function as const, the compiler becomes responsible for making sure this
condition is true. There are some occasions where you will have to make a few
changes in order to use const, but it is worth it. For example, suppose you have the
following members in a class:
std::vector<int> v;
int sum () const
{
int s = 0;
std::vector<int>::const_iterator i;
for (i=v.begin(); i!=v.end(); i++)
s += *i;
return s;
}
Since the method is const, you must use a const_iterator instead of just an
iterator. If you forget, the compiler will remind you. However, in some cases, such as
when templates are used, the error messages can be difficult to decipher. See
[Meyers01].
Return internal values from a const method as const. For example (from apImage<>):
const S& storage () const;
const T* base () const;
Add a second method that returns a non-const version when a const method must be
accessed by a non-const member. For example (from apImage<>):
const T* rowAddress (unsigned int y) const;
T* rowAddress (unsigned int y);
This is one of the most common uses for references. By making the return value const, you
can safely return object variables without copying them. Copies should be avoided whenever
possible, even if that object uses reference counting, like our image storage class does.
Returning a non-const reference to an internal variable gives full access, without restriction,
to that variable. By design, returning a non-const reference is done in the assignment
operator (operator=), prefix increment (operator++), and prefix decrement (operator--), in
addition to other places in the language. You should be very careful when using this feature,
as it is easy to misuse the reference that is returned. If you see this construct in your code,
you should redesign it, as follows.
Page 283
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
If you are returning a reference because an external function needs to access it, consider
using friendship instead. Obviously, you cannot return a non-const reference in a const
function. In general, if you are returning a non-const reference, it is most likely a bug in your
code unless you have deliberately and carefully applied this construct. If you do use non-
const references, be sure to comment it in your code so that others are clear about your
intentions.
This is also a very common use for references. Arguments passed by const reference avoid
the copy issue. It also helps the function prototype become self-documenting. For example,
the duplicate() functionality in our image framework looks like this:
Passing by Reference
Passing by reference is a perfectly acceptable practice. Usually when you see an argument
passed by reference (not const reference), you can assume it is used as a return value.
When you have more than one output parameter, you can either return one parameter
normally with the rest returned by setting reference arguments, or you can return a structure.
We use both types, depending upon the application. If you only have two pieces of
information to return, returning a std::pair<> is a great way of handling this.
We frequently use references to assist the compiler during template instantiation. By using
references to pass the destination information, the proper template instantiation can occur,
as shown in the following example. We chose to use a slightly different definition, but we
could have written our add2() functions as:
The other use of references is for returning multiple values. For example, you might have a
function like:
struct results
{
bool status;
std::string value;
// other results
};
Page 284
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
The Boost Graph library has an interesting construct, called tie(), that allows you to treat
the values of a std::pair<> as two separate pieces of data. See [Siek02]. Using this
construct, you can write:
[
Tea
m LiB
]
Page 285
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Neighborhood Operations
A neighborhood operation is one that uses a relatively small set of pixels in the source image
to generate a pixel in the destination image. The Laplacian and Gaussian filters we presented
in Chapter 6 are examples of neighborhood operators. We presented a flexible filter framework
that other neighborhood routines can use with little or no modification. The framework dealt
with all the special boundary conditions and took care of the intersection operations to
determine exactly which pixels needed to be processed.
Our image processing filters, such as Laplacian and Gaussian, use kernels that are always
square with an odd width and height. The kernel is always centered over the corresponding
pixels in the image, removing any ambiguity over which pixels are used. There are other
neighborhood routines, however, where this restriction does not work.
To handle these cases, we have to generalize our definition in two ways. First, we need to
add an origin point to the kernel to specify how the neighborhood operation is computed.
Figure 8.1 shows a 2x2 kernel and its origin.
Second, we have to relax the restriction of the kernel being square, so that any rectangular
kernel can be used. Figure 8.2 shows a 1x3 rectangular kernel and its origin.
Page 286
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
In addition to image convolution, there are a number of other image processing operations
that use neighborhood operators, such as morphology. Note that dilation and erosion, which
are indicated in Figure 8.3, are subsequently discussed within the context of morphology.
Morphology
Morphology operations were initially defined for binary images (i.e., each pixel is either on or
off), and performed simple OR and AND operations between pixels in the source image to
decide the state of each destination pixel.
Page 287
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Grayscale morphology extends the original definition to work with non-binary pixel data. These
operators typically use a min or max operator to compare pixels in the source image.
The erosion operator (or min operator) tends to shrink the objects in an image by removing
pixels along the boundary between the object and its background. The complimentary
operator is the dilation operator (or max operator), which tends to expand the size of an
object.
Let's write a simple morphology operator with the following assumption: we will use a cross
structuring element so that we can extend our existing convolution operation to perform
morphology. Even though the structuring element is a cross, the intersection operations are
identical to those used in our 3x3 kernel. Since we are using an existing operation, there are
many parameters that we can ignore. The structuring element is hard-coded into our filter
function, as shown here.
Page 288
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
To perform an erosion using a cross structuring element, you can simply call the
erodeCross() function. Its definition is shown here.
char* kernel = 0;
unsigned int size = 3;
int divisor = 1;
processor.run (src1, kernel, size, divisor, dst1);
}
Like with many image processing operations, you should experiment to determine what
settings work best in your application.
Sobel
Another popular convolution filter is the Sobel operator. This is similar to Laplacian, but it finds
the edges in the image by applying two kernels to each pixel. If you only take the magnitude
of these two results, you have an image similar to that produced by the Laplacian filter.
However, you can also use this information to determine the angle of any edge in the image.
This angle information is then mapped to a binary value and written like any image pixel. Even
though this angle is very rough, it works surprisingly well when the image has little noise in it.
Figure 8.6 demonstrates the effects of using the Sobel operator that we provide.
The Sobel filter differs from the Laplacian filter, in that it tends to produce two peaks at every
edge in the image. This is the result of using the following two convolution kernels during
processing:
Page 289
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Although you can compute a separate image with each kernel and then compute the output
image, it is much more efficient to do this in a single step. Given the values x and y, you can
compute the output pixel values using the following equations:
We can use our convolution framework to design this filter, as long as we return only a single
image from each call. Since the Sobel angle image is of limited use, we implement it as a
separate function. Here is the generic Sobel magnitude function:
pel = *pk++;
sumx -= pel;
sumy += pel;
sumy += 2 * (*pk++);
pel = *pk++;
sumx += pel;
sumy += pel;
pk += pitch;
sumx -= 2 * (*pk++);
pk++; // Skip this pixel
Page 290
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
sumx += 2 * (*pk++);
pk += pitch;
pel = *pk++;
sumx -= pel;
sumy -= pel;
sumy -= 2 * (*pk++);
pel = *pk++;
sumx += pel;
sumy -= pel;
sumx = static_cast<R>(sqrt(static_cast<double>(sumx*sumx +
sumy*sumy)));
*p2++ = apLimit<T2> (sumx); // Prevent wrapping
}
}
}
When you write a function like this, you will often find that there are optimizations you can
make. We took advantage of zero kernel elements to eliminate the unnecessary arithmetic: six
of the 18 total kernel elements are zero, so ignoring them saves about one-third of the
processing time.
sumx = static_cast<R>(sqrt(static_cast<double>(sumx*sumx +
sumy*sumy)));
We use a double to compute the magnitude, which is then cast to the appropriate type.
However, this does not work properly for RGB data types because the compiler will implicitly
convert the RGB quantity to a grayscale value. As with the morphology filter, we have written
an RGB specialization to correctly handle this case. It is included on the CD-ROM.
We can also write more specializations to further optimize our Sobel filter. For example, the
magnitude calculation can hurt performance on embedded platforms in particular because of
the floating point operations that are used. For a pixel type of Pel8, for example, you could
add a lookup table to convert the X and Y values to a magnitude with a single lookup. This
table does consume memory (64 kilobytes if an exhaustive table is stored), but can be well
worth it to make this calculation run faster. You can also use approximations, depending on
the purpose of the filtered images. For example, the magnitude calculation can be
approximated using absolute value, as shown:
Page 291
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Another image processing category are those operations that transform images from one type
to another. In Chapter 6, we showed how copy() can be used to change an image from one
type into another. We used this function to convert between monochrome (apImage<Pel8>)
and color images (apImage<apRGB>). When we talk about image space transforms, we are
referring to the meaning of a pixel, rather than the data type of a pixel.
A color-space operator to convert an RGB (Red, Green, Blue) image to an HSI (Hue,
Saturation, Intensity) image
Mapping Pixels
Lookup tables are a very common technique for mapping pixels from one value to another. For
pixel types with a small number of values, such as Pel8, an array is used to contain the
mapping between source and destination. For larger images, such as apRGB, this is not
24
practical because there are 2 possible values. A generic lookup table function is very easy to
write, partly because only a single image is involved. The image is modified in-place and does
not require any temporary image storage as shown.
Even if you could pass these arguments, the function would not work properly, because the
line:
*p++ = lut[*p];
will try and use *p (an apRGB value) as an index to the lookup table. And yes, this does
compile, because there is a conversion operator defined in apRGBTmpl<T> to convert a color
pixel into a monochrome pixel (i.e., an integer value), as shown:
operator T () const
{ return (red + green + blue) / 3;}
Most unit tests will not catch this mistake unless it tests template functions for a variety of
pixel types. We recommend that whenever you write template functions, do any of the
following:
Write specializations only for the pixel types you want to support.
Write specializations that fail for the pixel types you want to exclude. For example:
template<> void convertLUT (apImage<apRGB>&, apRGB*)
{
throw apUnsupportedException("convertLUT");
Page 292
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
}
Document the function to indicate which template arguments are known to work.
Many applications that handle color images require image space conversion, which is the
conversion of an RGB image to another color space image and vice versa. In our framework,
we have used apRGBTmpl<> to represent an RGB pixel. There are many color spaces that use
a triplet (i.e., three values) to represent the value of a color pixel. The term tristimulus value
is another way of describing this triplet.
When we defined apRGBTmpl<>, we considered making this name more generic. We decided
against it because in most cases we are dealing with an RGB pixel. To define our new color
space data type, we can reuse apRGBTmpl<> by defining a new typedef, as shown:
Figure 8.8 shows the conversion of an RGB image to its corresponding hue, saturation, and
intensity images.
We have split the problem into two pieces: first, we write a function, RGB2HSI(), to convert
an RGB pixel into an HSI pixel; next, we write another function, also called RGB2HSI(), to
convert an entire RGB image to an HSI image. Both are shown here.
Page 293
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
template<class T>
apRGBTmpl<T> RGB2HSI (const apRGBTmpl<T>& pel)
{
static double twoPi = asin(1.) * 4.;
apRGBTmpl<T> hsi;
double t;
T r = pel.red;
T g = pel.green;
T b = pel.blue;
// Hue
t = acos(.5 * ((r-g)+(r-b)) / sqrt((r-g)*(r-g) + (r-b)*(g-b)));
double sum = r+g+b;
if (b > g) t = twoPi - t; // 2*pi - t; Gives us 4 quadrant answer
hsi.red = static_cast<T>(t *
apLimitInfo<T>::sType.maxValue / twoPi);
// Saturation
t = 1. - 3. * min(r, min(g, b)) / sum;
hsi.green = static_cast<T>(t * apLimitInfo<T>::sType.maxValue);
// Intensity
hsi.blue = (r + g + b) / 3;
return hsi;
}
template<class T>
void RGB2HSI (apImage<apRGBTmpl<T> >& image)
{
typename apImage<apRGBTmpl<T> >::row_iterator i;
Image Geometry
Page 294
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Operations that deal with the geometry of an image, such as resizing or rotation, are used
extensively in the image processing world. Like most algorithms, there are good ways and bad
ways to solve these problems. Earlier, we used a thumbnail() function as a running example
through the book because it is both easy to understand and implement. While thumbnail() is
a resizing function and it has some usefulness, it does not go far enough to address the
general problem.
In this section, we present a more robust image resizing operation. Most of our discussion is
also applicable to image rotation, another important operation for image processing.
Image Resizing
The right way to solve this problem is to think in terms of the destination image. For every
pixel in the destination image, you want to find the appropriate pixels from the source image
that contribute to it. When we write resize(), we will work with the geometry of the
destination and compute a value for every pixel.
The wrong way to solve this problem is to think in terms of the source image. Depending on
how the resizing is performed, a single source pixel can be used to compute multiple
destination pixels. If you try and step through each source pixel, you must find all the
destination pixels that are affected by it. The result will be a jagged looking image that may
contain output pixels with no value.
Our implementation for resize() is slightly different than our other image processing
operations, in that resize() allocates the destination image given the desired size. This
decision is based upon how we expect resize() to be used in applications. We can ignore the
origin point of the source image until we are finished, since this value is left unchanged.
For any pixel in the destination image, D(x,y), we can find the corresponding pixel in the
source image, S(x',y'), using the equations shown in Figure 8.9.
Notice that the desired source pixel usually has fractional pixel coordinates. The pixel at these
Page 295
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
coordinates does not exist in our discrete representation of the image, but we can perform an
interpolation to find out its value.
Figure 8.10 shows an image reduced by 75 percent using nearest neighbor interpolation.
We recommend bilinear interpolation to determine the pixel value because the results are
better. Bilinear interpolation refers to the fact that we use four pixels (two pixels in the x
direction and two in the y direction) to determine the value of any arbitrary pixel. In contrast,
nearest neighbor interpolation only chooses the nearest point, rather than computing the
weighted average of the four pixels. Bilinear interpolation is fairly fast and produces good
results, as shown in Figure 8.11.
Page 296
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Figure 8.13 shows a side-by-side comparison of the nearest neighbor technique and the
bilinear interpolation technique. Notice how the edges are much smoother with the bilinear
interpolation technique applied to the same image.
With this information we can now write our resize() function to work with most datatypes.
(Note that it does not support our apRGB datatype; you could support this datatype by
writing a specialized version of this function.) We start by defining a function,
Page 297
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
fetchPixel_BLI(), to fetch a pixel from an image using floating point coordinates. The BLI
suffix stands for bilinear interpolation. This is a useful stand-alone function that can also be
reused in other applications. The fetchPixel_BLI() function is as follows:
We can also write the nearest-neighbor equivalent function, fetchPixel_NN(), which fetches
the nearest integer pixel in the image, as follows:
Page 298
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
if (height == 0)
height = src.height() * width / src.width();
typename apImage<T,S>::row_iterator i;
T* p;
for (i=dst.row_begin(); i!=dst.row_end(); i++) {
sx = src.x0();
p = i->p;
for (unsigned int x=0; x<width; x++) {
*p++ = fetchPixel_BLI (src, sx, sy);
sx += xscale;
}
sy += yscale;
}
return dst;
}
Image Rotation
Image rotation is similar to image resizing because it uses the same approach, in that pixels in
the destination image are populated with pixels from the source image. The major difference is
in how we compute which source pixels are needed. Image rotation is only one aspect of a
more general purpose transformation called an affine transform. An affine transform handles
image rotation, scaling, translation, and shear (non-uniform scaling). This may sound
complicated, but the affine transform is really just a matter of applying one or more linear
transformations to the pixel coordinates.
For example, to rotate pixels in an image by an angle, , around the origin of the image, you
would apply the following computations:
We have not included the implementation for an affine transform. To write this function, you
must pay close attention to the boundary conditions. For example, if you take a source image
and rotate it 45 degrees, there will be large areas in the destination image that do not have
valid pixels. If you devise your algorithm correctly, you can ignore most of these regions and
only compute those pixels that contribute to the output image.
Page 299
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
digital photographs to make them appear crisper and in focus. We refer to this method as
unsharp masking because it works by subtracting a smoothed (or unsharp) version of the
image from the original.
If you have never used this method to sharpen images, you might be surprised by how well it
works. You can take an image from a digital camera, for example, and once you see the
processed photograph, the original may begin to look fuzzy to you. You can also use unsharp
masking as a way to restore the sharpness of the edges in scanned photographs.Figure 8.14
shows the effects of unsharp masking.
You can construct an unsharp mask using one or more basic filtering components that we
already have developed. Other filters, such as the convolution filters, may make an image
more useful for further analysis by removing noise, but they can't create a more visually
appealing image, as an unsharp masking filter can.
There are a number of ways to filter an image using unsharp masking. One simple technique
uses a Laplacian filter to find the high frequency edges, and then adds a portion of this value
to the original image. Although this is a very simple implementation, it works erratically on
many images.
We provide the class unsharp masking implementation, which involves running a low pass filter
on the image and subtracting a percentage of it from the original. The steps we use to
implement unsharp masking are as follows:
1. Run a low-pass filter on the image. As a rule of thumb, the larger the kernel, the better
the results. We use a 5x5 Gaussian filter to blur the image, as shown in Figure 8.15.
Page 300
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Note that you could also the 3x3 Gaussian filter we presented in Chapter 6, but it
produces greater errors near high frequency edges.
2. Compute the output image, D, by combining the original image, S, and the Gaussian
filter image, G, as follows: D = k S + (1-k)G
If k=0, the output image is identical to the Gaussian image. If k=1, the output image is
identical to the source image. If k=2, the output image is twice the original image,
minus the Gaussian image. When k>1 we achieve the intent of the unsharp mask,
because we subtract a portion of the Gaussian filter from the original.
When k=2, we have a filter that usually produces reasonable results. We can rewrite our steps
to produce this filter using a single convolution kernel, which is computed as shown in Figure
8.16.
There is a one small problem with using this single kernel. The convolution routines we
developed in Chapter 6 use a char to store the kernel values. The value, 299, does not fit.
We could add a new version of convolve() to take larger kernels; however, an easier
approach is to solve the problem in steps, thus avoiding the limitation. Our solution is shown
below.
Page 301
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
unsigned int x, y;
R sum;
const T1* ps;
const T1* pg;
T1* pd;
R pels, pelg;
double gstrength = 1. - strength;
return dst;
}
[ Team LiB ]
Page 302
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
8.4 Summary
In this chapter, we provided in depth discussions of topics we felt warranted such treatment.
These topics included copy on write, caching, explicit keyword usage, const, and pass by
reference. We also spent some time showing you exactly how to take the image framework
provided in the book and extend it with image processing functions to meet the needs of our
application. And, for those of you who have an interest in imaging, we highlight some routines
that work particularly well for enhancing your digital photographs.
Throughout the book, we've taken a simple test application and evolved it into a
commercial-quality image framework by prototyping and later applying C++ constructs
designed to meet the requirements. By following this path, we've been able to have detailed
discussions on techniques, such as memory management, handle class idiom, reference
counting, and template specialization, as they apply directly to solving real world problems.
Some of the techniques we expected would end up as part of the final design, like the handle
class idiom, proved to have no benefit; whereas other techniques, like template specialization,
proved to have significant benefit. The evolution of the framework gave us a concrete code
base in which to compare and contrast the usefulness of various C++ constructs. We would
not have been able to accomplish this, with the same level of detail, through an abstract or
theoretical discussion.
We hope that, in addition to the C++ discussions, you might also find the software provided
with the book useful in your software development efforts. We've touched on a number of the
utilities that are provided, including the image framework, a unit test framework, a resource
manager for localization, and other third-party libraries and development tools. You will be able
to find software updates at http://www.appliedcpp.com. Appendix B provides a detailed
description of the contents of the CD-ROM.
[ Team LiB ]
Page 303
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
[ Team LiB ]
Page 304
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
A.1 Software
This book's companion Web site:
http://www.appliedcpp.com
DebugView freeware:
http://www.sysinternals.com
Standard C++ I/O Streams and Locales, by Angelika Langer & Klaus Kreft:
http://home.camelot.de/langer/Articles/Internationalization/I18N.htm
[ Team LiB ]
Page 305
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
A.2 Standards
POSIX, IEEE Std 1003.1-2001 specification:
http://www.opengroup.org/onlinepubs/007904975/toc.htm
Unicode Standard:
http://www.unicode.org/standard/standard.html
XML Standard:
http://www.xml.org
[ Team LiB ]
Page 306
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
[ Team LiB ]
Page 307
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
B.1 Contents
The attached CD-ROM contains the following high-level directories:
Delegates
This directory contains the libraries and/or the source code to build the libraries for the
three delegates that were used in Chapter 6: the Intel Integrated Performance
Primitives (IPP) image delegate, the JPEG file delegate, and the TIFF file delegate.
framework
This directory contains all of the source code, unit tests, and project and makefiles
necessary to build and use the framework.
Prototypes
This directory contains the complete source code and unit tests for each of the three
prototypes used in Chapter 3. In addition, this directory contains the complete test
application that was designed and built in Chapter 2.
Utilities
This directory contains two useful utilities: the Sysinternals DebugView utility and the
Intel C++ Compiler 7.0 for Windows.
[ Team LiB ]
Page 308
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
B.2 Framework
The CD-ROM includes all of the source code for the image framework developed throughout
this book. In addition, we provide the source code for the supporting unit tests, makefiles,
and project files. The file hierarchy is shown here.
framework
Header files
include
Licensing Information
Copyright © 2003 Philip Romanik, Amy Muntz
Permission to use, copy, modify, distribute, and sell this software and its documentation for
any purpose is hereby granted without fee, provided that (i) theabove copyright notices and
this permission notice appear in all copies of the software and related documentation, and (ii)
the names of Philip Romanik and Amy Muntz may not be used in any advertising or publicity
relating to the software without the specific, prior written permission of Philip Romanik and
Amy Muntz.
Use of this software and/or its documentation will be deemed to be acceptance of these
terms.
THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, EXPRESS,
IMPLIED, OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY WARRANTYOF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
IN NO EVENT SHALL PHILIP ROMANIK OR AMY MUNTZ BE LIABLE FOR ANY SPECIAL,
INCIDENTAL, INDIRECT, OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES
Page 309
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Page 310
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
B.3 Prototypes
The CD includes all of the source code and supporting unit tests for the prototypes developed
in Chapter 3 and for the test application built and designed in Chapter 2. The file hierarchy is
shown here.
Prototypes
Licensing Information
Copyright © 2003 Philip Romanik, Amy Muntz
Permission to use, copy, modify, distribute, and sell this software and its documentation for
any purpose is hereby granted without fee, provided that (i) the above copyright notices and
this permission notice appear in all copies of the software and related documentation, and (ii)
the names of Philip Romanik and Amy Muntz may not be used in any advertising or publicity
relating to the software without the specific, prior written permission of Philip Romanik and
Amy Muntz.
Use of this software and/or its documentation will be deemed to be acceptance of these
terms.
THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, EXPRESS,
IMPLIED, OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
IN NO EVENT SHALL PHILIP ROMANIK OR AMY MUNTZ BE LIABLE FOR ANY SPECIAL,
INCIDENTAL, INDIRECT, OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA, OR PROFITS, WHETHER OR NOT
ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
[ Team LiB ]
Page 311
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
B.4 Utilities
The CD-ROM includes two utilities: the Sysinternals DebugView utility and the Intel C++
Compiler 7.0 for Windows. The file hierarchy is shown here.
Utilities
Description
From the Sysinternals Web site: DebugView is an application that lets you monitor debug
output on your local system, or any computer on the network that you can reach using
TCP/IP. It is capable of displaying both kernel-mode and Win32 debug output, so you don't
need a debugger to catch the debug output that your applications or device drivers generate,
nor do you need to modify your applications or drivers to use non-standard debug output
APIs.
DebugView works on Windows 95, 98, Me, NT 4, 2000, XP, and .NET Server.
Under Windows 95, 98, and Me, DebugView will capture output from the following sources:
Win32 OutputDebugString
Win16 OutputDebugString
Kernel-mode Out_Debug_String
Kernel-mode _Debug_Printf_Service
Under Windows NT, 2000, XP, and .NET Server, DebugView will capture:
Win32 OutputDebugString
Kernel-mode DbgPrint
DebugView also extracts kernel-mode debug output generated before a crash from Window
NT/2000/XP crash dump files if DebugView was capturing at the time of the crash.
Licensing Information
From the Sysinternals Web site: There is no charge to use this software at home or at work.
Page 312
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Sysinternals commercial licenses are priced according to the complexity of the licensed code
and its role in the target application. If you are interested in licensing Sysinternals tools or
source code for redistribution or for inclusion with or as part of a software product, please
contact [email protected].
Description
The Intel C++ compiler makes it easy to get outstanding performance from all Intel 32 bit
processors, including the Pentium 4 and Intel Xeon processors, and the 64-bit Intel Itanium
processor. The compiler provides optimization technology, threaded application support,
features to take advantage of Hyper-Threading technology, and compatibility with leading
tools and standards to produce optimal performance for your applications.
Profile-Guided Optimization
Data prefetching
Run-time support for Intel processor generations: processor dispatch [IA32 only]
OpenMP support
Page 313
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Licensing Information
You can obtain the required license key by going to: http://www.appliedcpp.com/intel.html
You can upgrade to a full license version of the Intel C++ Compiler 7.0 for Windows for a
nominal fee at the same URL.
[ Team LiB ]
Page 314
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
B.5 Delegates
The CD-ROM includes either the library or the source code to build the library for the image
and file delegates that are fully discussed in Chapter 6. The file hierarchy is shown here.
Delegates
Description
Intel Integrated Performance Primitives (IPP) is a cross-platform software library that provides
a range of library functions for multimedia, audio codecs, video codecs (for example, H.263,
MPEG-4), image processing (JPEG), signal processing, speech compression (i.e., G.723, GSM
ARM*) plus computer vision as well as math support routines for such processing capabilities.
Specific features include vector and image manipulation, image conversion, filtering,
windowing, thresholding, and transforms, plus arithmetic, statistical, and morphological
operations. A variety of data types and layouts are supported for each function. IPP minimizes
data structures to give the developer the greatest flexibility for building optimized
applications, higher level software components, and library functions.
The Intel IPP is a low-level layer that abstracts multimedia functionality fromthe processor.
This allows transparent use of recent Intel architecture enhancements such as MMX
technology, Streaming SIMD Extensions, Streaming SIMD Extensions 2, as well as Intel Itanium
architecture and Intel XScale technology instructions.
Intel IPP is optimized for the broad range of Intel microprocessors: Intel Pentium 4 processor,
the Intel Itanium architecture, Intel Xeon processors, Intel SA-1110 and Intel PCA application
processors based on the Intel XScale microarchitecture.
Licensing Information
You can get the required license key by going to: http://www.apppliedcpp.com/intel.html
You can upgrade to a full-license version of Intel IPP for a nominal fee at thesame URL.
B.5.2 JPEG
The CD-ROM includes the files necessary to build the Independent JPEG Group's (
http://www.ijg.org) free JPEG software. This is the file delegate software discussed in Chapter
6. This software is free for use in both noncommercial and commercial applications. Please
Page 315
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Description
From the Independent JPEG Group's Web site: This package contains C software to implement
JPEG image compression and decompression. JPEG is a standardized compression method for
full-color and gray-scale images. JPEG is intended for "real-world" scenes; cartoons and other
non-realistic images are not its strong suit. JPEG is lossy, meaning that the output image is
not identical to the input image. The user can trade off output image quality against
compressed file size by adjusting a compression parameter.
The distributed programs provide conversion between JPEG "JFIF" format and image files in
PBMPLUS PPM/PGM, GIF, BMP, and Targa file formats. The core compression and
decompression library can easily be reused in other programs, such as image viewers. The
package is highly portable C code; and has been tested on many machines ranging from PCs
to Crays.
Licensing Information
From the Independent JPEG Group's Web site: We are releasing this software for both
noncommercial and commercial use. Companies are welcome to use it as the basis for
JPEG-related products. We do not ask a royalty, although we do ask for an acknowledgment in
product literature (see the README file in the distribution for details). We hope to make this
software industrial-quality although, as with anything that's free, we offer no warranty and
accept no liability. For more information about licensing terms, contact
[email protected].
B.5.3 TIFF
The CD-ROM includes the files necessary to build TIFF Software's free TIFF software (
http://www.libtiff.org). This is the file delegate software discussed in Chapter 6. This
software is free for use in both noncommercial and commercial applications. Please review the
licensing information includedhere.
Description
From TIFF Software's Web site: This software provides support for the Tag Image File Format
(TIFF), a widely used format for storing image data. The latest version of the TIFF
specification is available on-line in several different formats, as are a number of Technical
Notes (TTN's).
Included in this software distribution is a library,libtiff, for reading and writing TIFF, a small
collection of tools for doing simple manipulations of TIFF images on UNIX systems, and
documentation on the library and tools. A small assortment of TIFF-related software for UNIX
that has been contributed by others is also included.
The library, along with associated tool programs, should handle most of your needs for reading
and writing TIFF images on 32- and 64-bit machines. This software can also be used on older
16-bit systems though it may require some effort and you may need to leave out some of the
compression support.
The software was originally authored and maintained by Sam Leffler. While he keeps a fatherly
eye on the mailing list, he is no longer responsible for day to day maintenance.
License Information
Page 316
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
Permission to use, copy, modify, distribute, and sell this software and its documentation for
any purpose is hereby granted without fee, provided that (i) the above copyright notices and
this permission notice appear in all copies of the software and related documentation, and (ii)
the names of Sam Leffler and Silicon Graphics may not be used in any advertising or publicity
relating to the software without the specific, prior written permission of Sam Leffler and
Silicon Graphics.
THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, EXPRESS,
IMPLIED, OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
[ Team LiB ]
Page 317
ABC Amber CHM Converter Trial version, http://www.processtext.com/abcchm.html
[ Team LiB ]
Bibliography
[Bulka99] Bulka, D.; and D. Mayhew. Efficient C++. Reading, MA: Addison-Wesley, 1999.
ISBN 0201379503.
[Coplien92] Coplien, J. Advanced C++ Programming Styles and Idioms. Reading, MA:
Addison-Wesley, 1992. ISBN 0201548550.
[Gamma95] Gamma, E.; Helm, R.; Johnson, R.; and J.M. Vlissedes. Design Patterns. Reading,
MA: Addison-Wesley, 1995. ISBN 0201633612.
[Gonzalez02] Gonzalez, R.C.; and R.E. Woods. Digital Image Processing, Second Edition.
Boston: Addison-Wesley, 2002. ISBN 0201180758.
[Lakos96] Lakos, J. Large Scale C++ Software Design. Reading, MA: Addison-Wesley, 1996.
ISBN 0201633620.
[Langer00] Langer, A.; and K. Kreft. Standard C++ IO Streams and Locales. Boston:
Addison-Wesley, 2000. ISBN 0201183951.
[Lunde99] Lunde, K. CJKV Information Processing. Sebastopol, CA: O'Reilly, 1999. ISBN
1565922247.
[Meyers96] Meyers, S. More Effective C++. Reading, MA: Addison-Wesley, 1996. ISBN
020163371X.
[Meyers98] Meyers, S. Effective C++, Second Edition. Reading, MA: Addison-Wesley, 1998.
ISBN 0201924889.
[Nichols97] Nichols, B.; Buttlar, D.; and J. Proulx Farrell. Pthreads Programming. Sebastopol,
CA: O'Reilly, 1997. ISBN 1565921151.
[Pratt01] Pratt, W.K. Digital Image Processing, Third Edition. Indianapolis, IN: Wiley, 2001.
ISBN 0471018880.
[Siek02] Siek, J.G.; L. Lie-Quan; and A. Lumsdaine. The Boost Graph Library User Guide and
Reference Manual. Boston: Addison-Wesley, 2002. ISBN 0201729148.
[ Team LiB ]
Page 318