Josuttis Nicolai M Objectoriented Programming in C
Josuttis Nicolai M Objectoriented Programming in C
Nicolai M. Josuttis
Object-Oriented
Programming in C++
Object-Oriented
Programming in C++
Nicolai M. Josuttis
Solutions in Time
W)
JOHN WILEY & SONS, LTD
Copyright © 2001 by Pearson Education Deutschland GmbH. All rights reserved. First published in the German
language under the title Objektorientiertes Programmieren in C++ by Addison-Wesley, an
imprint of Pearson Education GmbH, Miinchen.
Translation Copyright © 2003 John Wiley & Sons Ltd, The Atrium, Southern Gate, Chichester, West Sussex
PO19 8SQ, England
Telephone (+44) 1243 779777
Email (for orders and customer service enquiries): cs-books@ wiley.co.uk
Visit our Home Page on www.wileyeurope.com or www.wiley.com
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, scanning or otherwise, except under
the terms of the Copyright, Designs and Patents Act 1988 or under the terms of a licence issued by the Copyright
Licensing Agency Ltd, 90 Tottenham Court Road, London WIT 4LP, UK, without the permission in writing of
the Publisher, with the exception of any material supplied specifically for the purpose of being entered and
executed on a computer system, for exclusive use by the purchaser of the publication. Requests to the Publisher
should be addressed to the Permissions Department, John Wiley & Sons Ltd, The Atrium, Southern Gate,
Chichester, West Sussex PO19 8SQ, England, or emailed to [email protected], or faxed to (+44) 1243
770571.
Neither the authors nor John Wiley & Sons, Ltd accept any responsibility or liability for loss or damage
occasioned to any person or property through using the material, instructions, methods or ideas contained herein,
or acting or refraining from acting as a result of such use. The authors and publisher expressly disclaim all implied
warranties, including merchantability or fitness for any particular purpose. There will be no duty on the authors
or publisher to correct any errors or defects in the software.
Designations used by companies to distinguish their products are often claimed as trademarks. In all instances
where John Wiley & Sons, Ltd is aware of a claim, the product names appear in capital or all capital letters.
Readers, however, should contact the appropriate companies for more complete information regarding
trademarks and registration.
This publication is designed to provide accurate and authoritative information in regard to the subject matter
covered. It is sold on the understanding that the Publisher is not engaged in rendering professional services. If
professional advice or other expert assistance is required, the services of acompetent professional should be
sought.
John Wiley & Sons Inc., 111 River Street, Hoboken, NJ 07030, USA
Jossey-Bass, 989 Market Street, San Francisco, CA 94103-1741, USA
Wiley-VCH Verlag GmbH, Boschstr. 12, D-69469 Weinheim, Germany
John Wiley & Sons Australia Ltd, 33 Park Road, Milton, Queensland 4064, Australia
John Wiley & Sons (Asia) Pte Ltd, 2 Clementi Loop #02-01, Jin Xing Distripark, Singapore 129809
John Wiley & Sons Canada Ltd, 22 Worcester Road, Etobicoke, Ontario, Canada
M9W IL]
This book is printed on acid-free paper responsibly manufactured from sustainable forestry
in which at least two trees are planted for each one used for paper production.
Contents
Preface
2-6... Friends and Othe Ty pes@ieo aq... 25a ee. ER. eee
217,
4.0.18 ~Automatic Type Conversionsaa ane ae eee ene. le. 4. 217,
4.6.29 The exp Preats hey word nee eens fonieid OA...
.. 21S
40.329 Friend Bunciionsoat) anecer a ie epee ae. GLE... . 220
4.0 hee CONY CISION UNCUONS erent. ok see ee. 22)
4.6.5 Problems with Automatic Type Conversions ............... 229
46.0) |Other Uses. of thest mi end. Keyword aaa a ee. oe 25k
46: 9 iriendiversus Object-Oriented Programming west. 2. 231
A632 “SUMINGNyyg Bee cpcghiet het He or Mes alee ey Lud P os 2 232
4./ . xceptionHandling:for<Classes) atiaim cae pail
eetet. eat? . FE:. 234
4.7.1 Motivation for Exception Handling in the Fraction Class ....... 234
S29 Exception Handling forthe Fraction @lassyseawe . eeeet
Ae. Die)
Ai7.3.2. Exception Glasséswiat.-al cial ent tae temo... 243
4c:Ag Remrowingl xceptions cker ulin were eee Fe! 244
Ad o@ « Exceptionsin DestiiCtots ¢ yeaa tia Sea toe Wie Ee. S 245
47.07 |Exceptions in Interface Declarations | >< seugoens
Geese 4 Oe 245
Avias Uierarchies of exception, Classes aint tx ieee ae. oe ceo 246
47.86. Desion OL EXceptiomClassts ai Reais eee nan Peck Me 250
aco Throwing Standard EXceptionss # eae ai) eee).
Pe, be 252
4710 Exception Satetyii aa. wit ett 3A ere ts. BeBe 252
Aa7 EAP SUPIIALV ARS oy a gd an a eee ne ag” GC 9 eee 235
7 Templates 427
TL gmaWINS LET Plates Ter RW ot yok ckaxe Gina shghd EE kM el Bee 428
ie del eemel CLENINO
LSS yagi tema cece 2 ap2) hee <un OM MO AR wlslemtbaien Sees 428
TZ eC UnCHOnM Templates mer ee che es ne ee ae a RU I 429
oop moi PUNCHION, LCMIPIALES© Aon fae oc sos. s ee Gets es at 429
xii Contents
10 Summary 569
101g Hierarchy.of CAs Operators 20.2 a es es ee ee ee, 569
10.24 Glass-Specific Properties.of Operations vas wie 2 2 572
10.3.5 Rules for Automatic; lype.Conversion. awe.
ee se es a3
10:47 Useful Programming Guidelines and Conventions ~~... ..-.....- 4: 574
Bibliography ME
Glossary 581
Index 587
Preface
It took a long, long time to produce this book, but now it is done. I have used my experience with
C++ and object-oriented programming, gained over several years of project and training work,
as well as my experience as a member of the C++ standardization committee, to write a tutorial
for C++ programmers consistent with the style in which C++ and its standard library should be
used.
The result is a book for all beginners who want to learn and understand how to program in
C++ as well as those programmers who want to get the overall picture and take advantage of the
standardized C++ language and its standard library.
The first part (Chapters 2 and 3) introduces and clarifies how to use and combine classes
to create a C++ program. Thus, it teaches from the application programmers point of view.
The second part (Chapters 4 to 6) introduces all aspects for the design and implementation of
classes and class hierarchies. It follows an extensive chapter about templates, which clarifies
their advantages, also in the framework of object-oriented programming. To round the book off,
there is a detailed introduction to certain aspects of the standard library (I/O, containers, etc.) and
to additional special language features that are important for day-to-day use.
C++ is far too rich a language to explain everything in one book. (In fact, my other C++
books, which cover all aspects of the C++ standard library and templates respectively, are both
over 500 pages.) However, this book contains a carefully planned introduction to programming,
using the features that C++ currently offers.
I hope that all readers have as much fun in reading this book as I have had in writing it.
Thanks
The history of this book goes back to 1994 when the first edition was published in German. After
the standardization of C++, a second German edition was published, which made use of all the
features and advantages of the C++ standard and especially its library. Now, this English edition
is a revised second edition, with some updates resulting from feedback and more experience.
Because of this long history there is a long list of people who have given tremendous support to
get this work done.
Firstly I should give special thanks to the staff at BREDEX (where I was working while
writing the first edition). In particular, Achim Brede, Ulrich Obst, Achim Loérke, Bernhard Witte,
and other employees have helped me a lot, devoting much of their time and effort.
2 Preface
Secondly, I’d like to thank all people who reviewed this book in its various editions over the
years. They did an incredible job to give this book the quality it has now. Many thanks for the
reviews of the German editions to Johannes Hampel, Peter Heilscher, Olaf JanBen, Jan Langer,
Bernd Mohr, Olaf Petzold, Michael Pitz, André Pénitz, Daniel Rakel, Kurt Watzka, Andreas
Wetjen, and Sascha Zapf. Many thanks for the reviews of the English edition to Jon Kalb, David
Reynolds, Angelika Langer, Mark Radford, and Detlef Vollmann.
Thirdly, I’d like to thank all people of the publishing companies that were involved. I'd like
to thank Susanne Spitzer, Judith Muhr, Margrit Seifert, and Friederike Daenecke from Addison-
Wesley, Germany for their cooperation and their patience. And I’d also like to thank Gaynor
Redvers-Mutton and Robert Hambrook from Wiley for giving me the opportunity to get this
book published in English and helping to ensure that it speaks both C++ and English.
There are many more people who have given me feedback and valuable advice over the
years. They have sent me e-mails or discussed various aspects of C++ at conferences or over the
Internet. Thanks also go to all of them, although I can’t list their names because this list would
be far too long and I would probably forget some.
Finally, I naturally thank my family and friends, who, as always, have supported this project
with a great deal of patience and understanding. Many thanks to Ulli, Lucas, Anica, and Frederic.
Nicolai Josuttis
Hondelage, September 2002
About this Book
This chapter explains why this book was written and the concepts that inspired it. A content
overview is also provided.
1.2 Prerequisites
The principal prerequisite for understanding this book is a degree of familiarity with the concepts
of higher-level programming languages. It is assumed that the reader already knows terms such
as loop, procedure or function, statement, variable, etc.
Knowledge of the concepts of object-oriented programming is not necessary. These concepts
are introduced when we discuss language features that provide the basis for these concepts in
C++. Some general knowledge of object-oriented concepts would be an advantage, however, as
the book inevitably looks at these concepts mainly from the viewpoint of C++.
Readers who know C or Java will already be familiar with some of the fundamental language
features. However, there are differences in details, and these will be explicitly pointed out where
necessary.
The key elements of C++ are classes. These are types that provide a certain interface to store and
retrieve data and/or to call operations. Their interface hides the details of a particular solution
from the caller. For this reason, C++ can be viewed from different perspectives:
e On one hand, there is the perspective of the application programmer of a class. He consumes
the class in various ways.
e On the other hand, there is the perspective of the system programmer of a class. He produces
the classes that allows the application programmer to concentrate on the general task, while
using the provided classes to solve particular problems of the task.
From the point of view of the system programmer, all details of the language must be known and
mastered to provide an elegant, effective and high-quality interface. From the point of view of
the applications programmer, knowledge of the interface and how to use it should be sufficient.
The book is organized according to these two points of view. For teaching purposes, the
perspective moves from that of the application programmer (who consumes classes provided by
the C++ standard library) to that of the system programmer (who implements classes by himself).
In practice, these two points of view cannot be completely separated. This is mainly because,
when solving problems, modules (such as classes) and interfaces are always used to provide
higher modules and interfaces. However, the book starts with the general concepts and the ap-
plication view, so that existing classes and interfaces can be used. The flexibility underlying the
classes and interfaces, which make C++ so valuable as a programming language, is explained
step by step afterwards.
1.3 Organization of the Book 5
Language Version
It was a general goal of this book to concentrate on the general language C++ and not any special
language extension such as ‘Visual C++’ or any language derivative such as “managed C++’.
C++ is taught according to the current ANSI/ISO standard (see [Standard98}), so that portability
over all (standard-conforming) platforms is guaranteed.
The structure of the book is based on numerous training courses I gave over several years. In
particular, the following topics are dealt with in the following chapters:
Chapter 2: Introduction: C++ and Object-Oriented Programming
Gives an introduction to the topic and introduces the basic terms required to understand C++.
Chapter 7: Templates
Introduces aspects of generic programs using templates.
Chapter 8: The Standard I/O Library in Detail
Describes in detail the input and output aspect of the standard library. This includes an
introduction to file access.
Chapter 9: Other Language Features and Details
Focuses on the remaining language features and standard components of C++.
Chapter 10: Summary
Contains a summarizing conclusion of C++.
The appendix contains
— abibliography,
— aglossary,
— an index.
6 1: About this Book
This unexpected question is aimed at those who prefer to study facts by way of examples, rather
than reading a book. (I am one of them.) I suggest that these readers work through the examples
one after another. In addition, each section in which a new language feature is introduced has a
short summary that can be used to decide whether the chapter should be read in detail.
1.6 Feedback
Something can always be improved upon. I would therefore welcome any suggestions and criti-
cisms.
The best way to reach me is by e-mail:
[email protected]
Be sure to check the book’s Web site for the currently known errata before submitting reports.
2
Introduction: C++ and
Object-Oriented Programming
This chapter provides a general overview of C++ and introduces the basic terminology necessary
to understand it. Following an introduction to the history and design aspects of C++, the most
important language features are introduced in a brief overview, before going into them in more
detail in the following chapters.
Structures
A fundamental progress in the history of programming languages was the ability to combine
several items of data together, and therefore to bundle various properties in one type. These kinds
of structures or records enable one variable to contain all data belonging to the circumstances
represented by the variables.
Structures therefore provide one way of abstraction in programs, namely the combination (or
composition) of different parts or elements (or so-called members). A structure for the type Car
consists of members such as engine, bodywork and four wheels.
Objects
The object lies at the center of object-oriented programming. An object is something that is
viewed, used and plays a role. If one programs in an object-oriented way, one tries to discover
and then implement the objects that play a role in the problem domain of the program. The
internal structure and behavior of an object therefore do not have priority. It is important that an
object such as a car plays a role.
Depending on the problem, various aspects of an object are relevant. A car can be assem-
bled from members such as engine and bodywork, or can be described using properties such
as mileage and speed. These attributes indicate the object. Accordingly, a person can also be
regarded as an object, of which various attributes are of interest. Depending on the problem
definition, these can be first name, address, personal number, hair color, and/or weight.
An object does not necessarily have to be something concrete or tangible. It could be totally
abstract and can also describe a process. A football match could be regarded as an object. The
attributes of this object could be the players, the score, and the time elapsed.
Structures enable objects to be managed within programs. Ultimately, they enable an object to
be broken down into individual attributes, and to get managed using fundamental types supported
by the programming language.
10 2: Introduction: C++ and Object-Oriented Programming
In structures or records, the individual properties of objects can be stored in the members. For
objects, it is not only of interest how they are organized, but also what you can do with them.
That is, the operations that form the interface of an object are also important.
The term abstract data type (or ADT) comes into effect here: an abstract data type describes
not only the attributes of an object, but also its behaviors (the operations). This could also include
a description of the states the object could have.
In the example of the football match, an object is not only described via the players, the score,
and the time that has elapsed. There are also the operations that can be carried out with the object
(substitution of a player, scoring a goal, blowing the final whistle) and the constraints (the game
starts with the score 0:0 and lasts 90 minutes’).
The ability to program these semantics is insufficiently supported in traditional non-object-
oriented programming languages. There is support for the fact that an object is composed from
attributes. There is, however, no support for the fact that operations and constraints are a part of
it. This has to be programmed separately as an addition.
Classes
For the production of abstract data types, the term class has been introduced in object-oriented
programming. A class is the implementation of an abstract data type. As opposed to a structure,
it not only describes the attributes (data) of an object, but also its operations (behavior).
For example, a Car class defines that a car consists of an engine, bodywork and four wheels
and that one can get into a car, drive it and get out of it.
A class can therefore be regarded as a realization of the term ‘abstract data type’. However,
in doing so, the exact difference is controversial. Sometimes the terms are used as synonyms;
sometimes a distinction is made between them, as, in contrast to an abstract data type, properties
of a class are derivable by other classes. In the C++ world in particular, the term abstract data
type is also used as a separation from the term fundamental data type (or FDT), and therefore
means the same as a (user-defined) class.
Instances
A class describes an object. It is also actually a type in the sense of programming. Several vari-
ables can be created from this type. In object-oriented programming, these are called instances.
Instances are therefore the realization of the objects described in a class. These instances
consist of data or attributes described in the class, and can be manipulated with the operations
defined within.
The terms object and instance are frequently used synonymously (particularly in C++). If a
variable of the Car type is declared, a Car object (an instance of the class Car) is created. In
' Of course, I talk about the European ‘football’ game, here, which you might know as being called ‘soccer.’
2.2 C++ as an Object-Oriented Programming Language sl
some languages, a distinction is made between object and instance, because a class can also be
regarded as an object. Object is then the common term for class and instance.
Methods
In object-oriented terminology, the operations defined for objects are called methods. Every
operation called for an object is interpreted as a message to the object, which uses a particular
method to process it.
An object-oriented program is ultimately produced via messages to objects, which can then
produce more messages for other objects. If the operation ‘drive’ is called for a car, the message
‘drive’ is sent to the car, which is then processed using the appropriate method.
This approach focuses on the object rather than the operation. During object-oriented design,
one thinks about the objects available in the problem area. Their construction and behavior are
described depending on their roles. The concrete program is then ‘only’ a simple sequence of
operations provided by these objects.
With the implementation of object-oriented concepts in C++, we should always remember that
C++ is not a purely object-oriented language, but a language hybrid. This is also reflected in
the naming: methods are also described as functions (any procedure or subroutine is called a
function in C++), and instances are merely described as concrete objects. The terminology babel,
prevalent in the object-oriented world, is perfect in C++ (I will discuss this in more detail in
Section 2.4 on page 20).
In C++, a class is a structure that can also contain functions (methods) as members. This is
demonstrated in the following example of the Car class.
Both the construction and the behavior, including initialization and destruction, are parts of the
class structure:
e Internally, a car has the following attributes:
— yearOfManufacture defines the year of manufacture of the car. The type used for this
int is available for integer values in C++.
— mileage defines the current mileage of the car. The type used for this float is available
for floating-point values in C++.
— licensePlate defines the actual license plate of the car. The type used for this string
is available for strings (character sequences) in C++.
e The following operations are defined:
- getYear0fManufacture() is used to query the year of manufacture and return it as an
integer value.
— drive() is used to cover a distance submitted as a floating-point value m with the car.
— getMileage() is used to query the current mileage of the car and return it as a floating-
point value.
e There are also special functions that can be used to influence the behavior of a car during
creation and destruction:
— Car() defines what will happen automatically during the creation of a car. For example,
the year of manufacture, mileage and license plate will be initialized appropriately.
- ~Car() defines what will happen during the destruction of a car. For example, it can be
defined here that specially created internal memory space is automatically released.
This kind of definition has several advantages:
e The operations (functions/methods) do not fill (or ‘pollute’) the global namespace. The only
global type is the Car type.
e Operations on a car object do not require the Car object as a parameter. The object invokes the
operation (rather than the other way around); thus an appropriate car is always automatically
available within the operation.
e Neither initialization nor clean-up operations have to be called explicitly. As a result, objects
always have a useful initial state and the user of a class (its application programmer) does
not have to (and cannot forget to) call a clean-up function when a car leaves its scope.
A corresponding application program in C++ looks as follows:
void cartest()
if
Carer // create car c and initialize it
public:
int getYear0fManufacture() ; // query year of manufacture
void drive(float m); // drive m miles
float getMileage() ; // query mileage
? Sometimes the term information hiding is used instead of data encapsulation. However, this is incorrect,
at least as far as C++ is concerned, because the internal data is only prevented from being accessed from
the outside, and is not actually hidden.
> Without access keywords, none of the members of a class can be accessed from outside (private is the
default). In this respect, the applications program introduced only compiles with this version of class Car.
14 2: Introduction: C++ and Object-Oriented Programming
The concept of data encapsulation is ultimately based on the idea that the internal organization
and behavior of an object is not relevant to the application programmer of the object. It is only
important that the object behaves correctly.
In the Car example, the way in which the year of manufacture and the mileage are managed
are of no interest to the application programmer. It is decisive that a car can be created, can be
driven, and that certain attributes can be requested. Figure 2.1 shows the car interface graphically.
The figure uses the Unified Modeling Language (UML) notation, which is the de facto standard
for object-oriented modeling and programming.
getYearOfManufacture(): int
drive (m: float)
getMileage(): float
For the application programmer, only the WHAT is of interest: the public interface establishes
what can be done with an object. The HOW is only of interest to the implementer of the class.
From the application programmer’s point of view, this is just a ‘black box’. As long as the public
interface is stable, implementation can be changed in any way without the application code being
affected. An implementation of a class can therefore be optimized later in order to increase the
performance without the need to update all uses of a class.
2.2 C++ as an Object-Oriented Programming Language 15
2.2.4 Inheritance
If the behavior of objects is described, there are often properties that different objects have in
common. In natural language use, common terms are used for this*. A blue car manufactured
in 1984 with a diesel engine simply becomes a car, as long as its details are not relevant. It can
even be regarded simply as a vehicle, if the fact that it is a car is irrelevant.
For example, this kind of generalization occurs with a car transporter. It is unimportant which
cars are transported; only size and weight may be relevant. Nevertheless, various types of car
may be transported and their differences may very well be relevant again once they have been
unloaded.
There is no equivalent data model in conventional programming for this means of abstraction
of generalization and specialization. In object-oriented programming, the concepts of inheritance
and polymorphism are used for this purpose.
Different classes can be connected to each other hierarchically. A derived class is always
a specialization or concretion of its base class. The reverse of this is that the base class is the
generalization of the derived class. This means that all properties (attributes and operations) of
the base class are inherited by the derived class, usually supplemented with additional properties.
Figure 2.2 shows an appropriate example (again we use the UML notation). The Vehicle
class describes the properties of a vehicle as a base class. For example, these include the at-
tribute yearOfManufacture or the operation getYearOfManufacture(). For all classes de-
rived from this, such as Bike, Car or Truck, these properties are also valid. The Vehicle class
is therefore the common term under which the common properties come.
The different properties of special vehicles are then implemented in the respective derived
classes. Vehicles with speedometers (VehicleWithSpeedo), for example, also have the addi-
tional attribute mileage, as well as the drive() and getMileage() operations. Trucks also
have the additional load() and unload () operations. In this way, a class hierarchy arises where
common terms become more and more concrete. The fundamental characteristic of inheritance
is the is-a relationship. A truck is a ‘vehicle with a speedometer’; a ‘vehicle with a speedometer’
is a vehicle.
What are the advantages of inheritance? First, it is used for consistency and to reduce code.
Common properties of various classes only need to be implemented once and only need to be
changed in one place if necessary. The other advantage is that the means of abstraction of the
common functionality is supported. This will be clarified in Section 2.2.5 on page 16, with an
introduction to polymorphism.
In practice, it may also be necessary to change inherited properties. This is possible but has
its limitations. The inheritance is, of course, meaningless if no property still applies. In this case,
a separate class should be implemented.
4 Generic term is probably a better phrase for common term. However, as generic has a special meaning in
C++, I decided to use common term here.
16 2: Introduction: C++ and Object-Oriented Programming
| ventele |
yearOfManufacture: int
getYearOfManufacture(): int
VehicleWithSpeedo
getMileage(): float
Bus
2.2.5 Polymorphism
Concerning the advantages that arise from inheritance, only consistency and code reduction have
been mentioned so far. However, inheritance is also an important prerequisite in facilitating
polymorphism. Work with common terms is only useful if objects can be accessed using
this
common term.
Let us assume that a collection of cars is to be viewed for a garage. These could be completely
different cars, different brands, different models, different types. It is therefore a ‘heterogen
ous
collection’ of cars. If the engine is to be changed in different cars, the same thing
is done in
principle, but, depending on the car, it may look very different. The request used
in everyday
language ‘change the engine of the cars’ will therefore lead to various different actions.
In order to reflect this, a separate version of the engine change can be implemen
ted for every
Class that describes a kind of car. The specialty of polymorphism is that even for such
a heteroge-
nous collection of different cars, the general changeEngine() operation can
be called, which
automatically results in the respective function being called according to the actual
type of car.
2.2 C++ as an Object-Oriented Programming Language 17
Ultimately, polymorphism means the ability for an operation to be interpreted only by the object
itself. This might happen at run time, because during compilation it is not known what kind of
car is used and therefore what operation has to be called.
There are different ways of implementing polymorphism>. In C++, inheritance is normally
used for the implementation of polymorphism°. If similar classes implement the same operation
differently, these can be declared in a common base class first, without them necessarily having
to be implemented there. If a heterogeneous collection of objects of different classes is used, this
collection is declared with the type of the common base class. If an operation is then called for
an element in the collection, this results in code that at run time finds out what class the object
actually belongs to to call the appropriate function.
An applications program can declare an appropriate heterogeneous collection of cars, assign
various objects and call the changeEngine() function for all objects. In each case, the correct
function is called automatically, according to the class of the actual car at run time:
CarCollection cars; // collection of Cars
VW Vv; // Object of the class VW (Volkswagen)
Ford nie // Object of the class Ford
“ll
cars[i] .changeEngine() ;
}
It should be taken into account that it doesn’t matter what derived classes exist for Car. There is
no code for a selection of a fixed set of classes. If one decides to implement a new car class as a
derived class from Car, cars can also contain its objects without having to be recompiled.
> The prototype of all object-oriented languages, Smalltalk, works in the most radical way. Variables are
not bound to a type and can therefore represent any kind of object. If an operation is called for a variable,
at run time the actual class of the object is evaluated. Depending on this, either a corresponding operation
of the class is called or you get an error message that the operation is not possible.
° In C++, polymorphism can also be implemented using templates. This is discussed in Section 7.5.3 on
page 457.
18 2: Introduction: C++ and Object-Oriented Programming
2.3.2 Templates
Many object-oriented languages have freedom of type. This means that a variable is not of a
particular type, but can stand for all possibilities. For example, a variable can first be assigned
an
integral value, then a collection and then a string.
This freedom of type does not exist in C++. Variables in C++ always have a particular type.
This is not necessarily a disadvantage, as, during compilation, it is always guaranteed that
types
are not mixed, and no operation can be called for an object that is not provided by its class.
However, type binding can also be very restrictive, as shown in the example of the
car col-
lection. For all possible types for which a collection is needed, an appropriate collection
class
must also be implemented. That is, as we have to implement a class CarCollection
to manage
2.3 Other Concepts of C++ 19
a set of Cars (see page 17), we would have to implement a class WindowCollection to handle
Windows, etc.
To avoid these disadvantages of type binding, templates were introduced to C++. Using these
templates, individual functions or whole classes can be implemented without a particular type
being defined. A template is implemented for arbitrary types. When types become clear, code is
generated for these types. That is, template code is just pseudo code that produces different code
for different types (in contrast to polymorphism, where the same code is used for different types
because the common term is used).
For instance, a class collection can be implemented for the management of all possible kinds
of objects’:
template <typename T> // template for the adopted type T
class Collection {
private:
Tx elems; // elements have type T
unsigned num; // actual number of elements
public:
void insert(T); // insert object of type T
T operator[](int); // define index operator to return an element of type T
int number(); // return number of elements
’ The implementation shown here leaves out some details that are necessary to be able to compile this code.
20 2: Introduction: C++ and Object-Oriented Programming
2.3.3 Namespaces
Symbols can be grouped symbolically using the concept of namespaces. This avoids name con-
flicts and also clarifies what symbols logically belong together (i.e. constitute a package or com-
ponent).
For example, all symbols of the standard library are defined in the std namespace. In order
to use a string from the standard library, the type must therefore be qualified accordingly:
Std 2stringss * // string of the standard library
All custom symbols should be defined in their own namespaces.
2.4 Terminology
In the object-oriented-language world, there is a certain terminological chaos. One and the same
features are named with different terms, while identical terms have different meanings. In C++,
this chaos is made even worse by the fact that object-oriented terms are mixed with terms bor-
rowed from conventional procedural programming, especially with C terms. I will therefore list
the most important terms used in this book.
In C++, a class is a description of a type. In this respect, it is often formulated so that an
object has a particular type, which means the same as class. This might lead to the consequence
that we avoid the term type and use only class instead. However, in C++, there are also types that
are not classes (for example, fundamental types such as float). If something in C++ applies to
‘different types’, then this means not only classes, but also fundamental types. For this reason, I
will use the term type in the sense of ‘any type’, which can also be a class. However, note that the
class keyword, which is usually used to define a class, sometimes stands for any type, including
fundamental types.
An instance is often just simply described as an object, as it is a concrete object of a class.
This, however, contradicts the fact that an object is sometimes understood as a generic term,
which means more. A class can also be regarded as an object. However, in C++, there is no
difference between the terms instance and object. In the language specification, the term instance
is not used at all. However, it is confusing that the term instantiate is used in connection with
templates. This does not relate to the creation of an object of a class, but to the generation of the
actual code to be compiled from template code. For this reason, I follow the current terminology
of C++ and describe instances of a class simply as objects of the class.
2.4 Terminology ZA
The largest confusing mixture of terms occurs with the elements of classes. In the context
of object-oriented programming and in the context of C++, they are called elements, members,
attributes, data, etc. The operations that can be called for objects are described as methods,
member functions or just functions. As it is common in the C++ community, I will use the term
member as a generic term for class elements,and make use of the distinction data member and
member function when needed.
These naming choices can be argued over indefinitely. A reader familiar with the world of
object-oriented languages may well ask, for example, why member function is used instead of
method and why object is used instead of instance. I have chosen C++ terminology, as the use of
the language of C++ from an object-oriented point of view is more prominent than the concept
of object-oriented programming from the point of view of C++.
- a
ae a!
_ ni
” ‘ z . a
e a as 7 ¢
.
' @
Ta le Tepe ee
opp gM, PPT teemia “Yh oth byinal
the ent Thee tra cd il) Wikre Angee, [vier 9, inSee GahdOp
‘WatBos Centen thtcrane, tiny att ied Sei fleet! I pineal
ara tyees”, thoy ty cle ol pele tunasenrieal
WES ieee the bamdyew
inthe, orice ah" pay Ragy hy ihets, tur'g fuathe 20
” 2 Lvae Repword, whice a weal! word aiCOMI aan, dee tomes ween -
Som taeneorte) ype. =
fn jonre es often [ut aoe meriakes a eatin,
Tite, howwuver, comes cu tee feed Gal a and Os
wivris Meant acre. A hank"owe <p Pies »
=.” Mitoneape batesethrur) 7s tromeee eall ghey
te pont gan af ull ‘Hoaeovel: 1 ew
eae. Tie ioe me eebeie ye ale
a" al anh: seni ’
ania! code th be Congeiied freee cae - : =—
ad Cis vad fewire (fue 4 ens of i¢ aay eomga ae - . i TO,” ” << pia
3
Basic Concepts of C++ Programs
There are many ways of looking at C++ code. We can look at it from the point of view of
the applications programmer, who combines the types and the classes to create programs us-
ing functions and control constructs. Or we can look at it from the point of view of the system
programmer, who provides new types and classes, which are abstractions of concepts. In prac-
tice, programmers usually take on both roles. This is especially true because, when abstracting
problems, types and classes are repeatedly used to program higher levels and classes.
In this chapter we will only deal with the basic concepts that are relevant from the point of
view of the applications programmer. That is, we will ‘only’ use existing types and classes to
write C++ programs. Doing this, we learn the syntax of functions and function calls, the use
of fundamental types, the division of programs into modules, and the use of the most important
types and classes from the standard library.
In the following chapters, we will then see how to implement specific types and classes.
24 3: Basic Concepts of C++ Programs
If this program cannot be compiled, this could be the result of using a C++ compiler that does not conform
to the current standard. In this case, a modified form of this program might work, which is shown in
Section 3.1.5 on page 28.
3.1 The First Program 25
A function with this name must be present in all C++ programs. This function is called
automatically when the program starts.
e Within main(), there is only one statement, which ends with a semicolon (as all statements
do):
std::cout << "Hello, world!" << std::endl;
This statement writes the string "Hello, World!", followed by the symbol std: :end1, to
the standard output channel std: : cout. The symbol std: : endl stands for ‘end of line’. It
makes sure that a line break is issued and the output buffer is flushed.
The program is supplemented by different comments, which are either enclosed by /* and */ or
reach from // to the end of a line.
The following sections explain in more detail the language features introduced by this exam-
ple.
A C++ program is made up of various functions. The term function is used for every type
of ‘sub program’ (subroutine, procedure, or whatever name you are familiar with from other
programming languages). A function can pass parameters as arguments and can (but does not
have to) return a value. That is, even operations that return no value (often called procedures
in other languages) are called functions in C++. They just return nothing (i.e. have return type
void).
There is a special function in every C++ program: the main() function. This function must
be present once in a C++ program. It is called automatically at the start of the program. Leaving
main() ends the program.
According to the main() example, you can see the general structure of functions:
e The function head consists of
— the return type (the type of the return value),
— the function name,
— the parameter list, enclosed by parentheses.
e The function body consists of a block of statements. This block is enclosed by braces. The
block consists of statements that all end with a semicolon (including the last one). The block
itself does not have a semicolon at the end.
The return type of main() is always int. int stands for ‘integer’ and represents an integral value
type (see the list of types in Section 3.2 on page 33). The corresponding return value is returned
by the program to the calling environment (e.g. the operating system). Thus main() returns an
integral value to the calling environment. This feature is used to give the calling environment
the ability to evaluate whether the program has run properly. According to operating system
conventions, a return value of 0 for main() indicates that the program has run properly. Any
other value means that the program has not run properly. The exact meaning of values other than
zero are up to the programmer.
Normally, every function that declares a return type must have an accompanying return state-
ment. main() is an exception here. At the end of main() you will always find an implicit
statement that returns 0:
return 0;
3.1 The First Program 2
That is, every C++ program by default indicates that it has run correctly. You can also write the
statement explicitly:
#include <iostream> // declarations
for I/O
An include statement is processed by a preprocessor, which is able to modify the source code be-
fore the actual compilation starts. The #include statement asks that all code found in iostream
be inserted into the program, as it would have been had it been manually written there. The in-
cluded code is usually a file that has a corresponding name. Such‘a file is called a header file.
Thus <iostream> is the general header file for I/O.
With the inclusion of the declarations from iostream (data streams for input and output) the
symbols listed in Table 3.1 are defined.
Input is made through a standard channel, which is generally assigned to the keyboard. Out-
put is made through another standard channel, which is generally assigned to the screen. Both
can be ‘redirected’ by the operating system or by using system functions, so that, for example,
standard output is written to a file.
* If your compiler finds that a corresponding return statement is missing at the end of main(), it means that
it does not conform to the standard.
28 3: Basic Concepts of C++ Programs
Meaning
std::cin | standard input channel (typically the keyboard)
std::cout | standard output channel (typically the screen)
std::cerr | standard error output channel (typically the screen)
std::endl | symbol for the output of a line break (newline) and the actual flushing
of the output
There is also a standard error output channel for error messages, which is also generally
assigned to the screen. By separating normal outputs and error outputs, error messages can be
treated differently by the environment in which the program runs. This allows operating systems,
for example, to write error messages to a log file, while normal output is still written to the screen.
In order to output data, it simply has to be ‘sent’ to the output channel using the << operator.
Doing this, multiple outputs can be chained. For example:
#include <iostream>
The integer 42 is automatically converted into the character sequence ‘4’ followed by ‘2’ and
written to the standard output channel std::cout. After this, std::endl ends the line by
appending a newline character and flushing the whole output (which might be buffered) to the
corresponding output device.
In a similar way, you can read from a channel using the operator >>. Further examples of this
will follow.
3.1.5 Namespaces
Both symbols of the input and output library begin with std: :. This prefix determines that cout
and end1 are both defined in the std namespace. This namespace is used for all symbols of the
C++ standard library.
Using the namespace concept, symbols can be grouped logically (defining a package or com-
ponent). Thus, writing std: : cout means that we use the cout symbol of the std component >
Nevertheless, you should not use the old style anymore, because it does not conform to the
standard and may no longer be compilable or portable.
3.1.6 Summary
C++ programs have the main function main(). This function is automatically called at the
start of the program, and the program ends when this function is exited.
main() can return an integral value with return, which indicates whether or not the program
has run properly. The statement
return 0;
indicates the end of a program, after the program has run successfully; every other value
denotes an unsuccessful program run. ;
main() ultimately has an implicit
return 0;
int main()
{
int counter = 0; // current number of found four-digit numbers
x
Inside main (), a variable is defined that counts how many four-digit numbers were found with
the required property:
int counter = 0;
The type int stands for integer. counter is the name of the variable. A variable is defined from
the point of its declaration to the end of the block in which it was declared. A block is enclosed
by braces. counter is therefore defined until the end of the main() function.
counter is initialized with the value zero. If this initialization does not occur, the initial
value of this variable is not defined! In fact, for all fundamental types, initial values for local
variables are not defined. For this reason, you should always explicitly specify the initial value
of a variable that has a fundamental type.
The most interesting part of main() is covered by a for loop:
// for every number from 1000 to 9999
for (int number=1000; number<10000; ++number) {
}
for loops are a generalized mechanism in C++ for loops that iterate over certain values. A for
loop is nothing more than a combination of three statements:
1. The first statement is executed once at the beginning:
int number=1000 // initialize loop variable number with 1000
2. The middle statement defines the condition under which the loop works:
number<10000 // as long as number is less than 10000
3. The last statement is carried out after every loop run:
++number // increment number (increase by one)
You could also write the loop as follows:
{
int number=1000;
while (number<10000) {
++number;
}
f
The loop initializes a loop variable number with the value 1000 and allows this variable to carry
out statements in the loop for every value smaller than 10000. The loop is therefore called for
all values from 1000 to 9999, while the actual value is always located in number.
32 3: Basic Concepts of C++ Programs
Within the loop, it is checked whether the square of the two digits produces the same value
as before. To do this, the first and last two digits are separated:
int front = number/100; = _// the first two digits
int back =I! number%100; = // the last two digits
The / operator is the division operator, which divides number by 100 here. The % operator is the
modulo operator, which yields the remainder of an integer division.
An if statement then checks whether the sum of the squares of these two numbers produces
the original number:
if (front*front + back*back == number) {
}
The * operator is the multiplication operator and + is the addition operator. The use of two equals
signs to compare two values might look unusual. The reason is that the single equal sign is used
for assignments:
a=b; // a is assigned the value of b
The designers of C had chosen one equal sign for assignments because this is by far the most
frequent operation and therefore using a two-character notation such as := is a waste of time and
space.
If the condition inside the loop is satisfied, an appropriate message is written to the standard
output channel:
std::cout << number << " == "
<UL TOM ty << CEO nee
<< back << "x" << back << std::endl;
In this multi-lined statement, the values of number, followed by the characters ‘==’, followed by
the value of front, followed by the ‘*’ character, followed by the value of front, and so on, are
written. An output line looks as follows:
1233 == 12*12 + 33*33
Finally, if the condition is true, the counter is increased by one using the increment operator:
++counter;
The following sections focus on the language features used here, such as variables, fundamental
types and control constructs.
3.2 Types, Operators, and Control Constructs 58)
Type Meaning
int integer (typical word size of the machine)
float floating-point value with single precision
double | floating-point value with double precision
char character (can also be used as an integral value)
bool Boolean number (true or false)
enum for enumeration types (names that represent integral values)
void ‘nothing’ (for functions without a return value
and empty parameter lists in ANSI-C)
Numeric Types
The numeric types (int, float, double) can have different sizes and therefore different value
ranges. The actual size of the numeric types is therefore not determined in C++. Behind this is
the concept adopted from C that int has the typical size of the underlying hardware, so that it
performs best. On machines with a 32-bit processor, int would typically have 32 bits; with a
64-bit processor, it would have 64 bits.
This size and the question as to whether we are dealing with a signed or unsigned type can
be qualified by the attributes listed in Table 3.3.
If you enter a qualifying attribute, the actual fundamental type does not need to be entered.
int is assumed in this case:
cpap ile // integer with a normal value range
long int x2; // integer, possibly with a larger value range
long x3; // ditto
unsigned x4; _ // unsigned integer with a normal value range
34 3: Basic Concepts of C++ Programs
In standard C++, the smallest sizes listed in Table 3.4 can be processed.
Minimum Size
char 1 byte (8 bits)
short int 2 bytes (16 bits)
int 2 bytes (16 bits)
long int 4 bytes (32 bits)
float 6 digits up to 10+”
double 10 digits up to 10*°”
long double | 10 digits up to 10*°”
The precise sizes are defined using numeric_limits in the <limits> header files, along
with further information on the type (see Section 9.1.4 on page 531).
Characters
The char type is used for individual characters. Character literals are enclosed in single quotes.
a’ therefore stands for the character ‘a’. Any given character can be located between the quotes,
apart from a line break. Some special characters must be masked with a backslash (see Table 3.5).
The literal ‘\n’ stands for a line break, the literal ‘\’’ stands for a single quote and the literal
‘\\’ stands for the backslash itself.
Literal Meaning
Ne
single quote
\"!
double quotes
NN backslash
\n line break/newline
NG tab
\oct-digits character with octal value of one to three digits
\xhex-digits characters with hexadecimal values
\b backspace
\f form feed
\r carriage return
These special characters can also be used in string literals. Using an ASCII character set, the
string
"A few special characters: \"\t\’\n\\\100\x3F\n"
stands for the following character sequence (in ASCII, ‘@’ has an octal value of 100, and the
question mark has a hexadecimal value 3F):
3.2 Types, Operators, and Control Constructs
a 35
ee ee
int main()
{
// for every character c with a value of 32 to 126
for (unsigned char c=32; c<127; ++c) {
// output value as number and as character:
std::cout << "Value: " << static_cast<int>(c)
<< " Character: " << c
<< std::endl;
}
s;
Inside the program, c is used as an integral value that iterates values above 32. Within the loop,
c is written once as a number and once as a character:
Siu COUL AG seece Static castwintr«e)"<< 2. <<se7<<)..
Because c has the char type, it is written as a character by default. Using the expression
static_cast<int>(c)
c is converted to the int type. static_cast is an operator that can be used for logical type
conversion, provided type conversion is valid. This type conversion guarantees that the value of
c is output as its integral value. The output of the program is (provided an ASCII character set is
used) as follows:
> Because only the minimum value of char is determined by the standard, the actual value ranges may
exceed this.
36 3: Basic Concepts of C++ Programs
Value: 32 Character:
Value: 33 Character: !
Value: 34 Character: "
Value: 35 Character: #
Value: 36 Character: $
Value: 37 Character: %
Value: 38 Character: &
Boolean Values
For a long time, in both C++ and C, there were no special types for Boolean values. They were
managed using integers, with 0 denoting false and all other values true.
Later in C++, the bool type was introduced with the literals true and false. To guarantee
backwards compatibility, integers can be used alongside the constants true and false. Zero
therefore stands for false, and every other value for true. Conditions in control constructs are
always satisfied if the expression inside has the value true or an integer value other than zero.
3.2.3 Operators
C++ provides an unusually large number of operators. The basic operators, taken from C, are
outlined below. A complete list of all C++ operators can be found in Section 10.1 on page 569.
Basic Operators
Table 3.6 lists the basic operators, found in every programming language, for combining values.
It is worth noting that the equality test is carried out with two equals signs. A single equals
sign is used as the assignment operator.
Caution: if you inadvertently use only one equals sign, this is still valid code, but it will not
do what it is supposed to. For example, the line
if (x = 42) // correct code, wrong semantics
does not test whether x has the value 42, but assigns 42 to x, and the condition is viewed as being
satisfied. This is because an assignment returns the assigned value. Thus the expression ‘x =
42’ yields the value 42, which is not equal to zero and is therefore interpreted as true. In this
3.2 Types, Operators, and Control Constructs Bd
Operator | Meaning
+ addition, positive sign
= subtraction, negative sign
* multiplication
/ division
hh _| modulo operator (remainder according to division)
< less than
<= less or equal to
> greater than
>= greater than or equal to
== equal to
!= not equal to
&& logical AND (evaluation up to the first false)
|| logical OR (evaluation up to the first true)
! logical negation
respect, we are dealing with correct code, which always assigns the value 42 to x, and where the
condition is always satisfied, regardless of the previous value of x.
This feature is even used by some users in order to write concise code, which, however, is
rarely legible. It is unusual to formulate the following conditions:
ita(ocr=it()) // assigns the return value of£() to x and
// simultaneously tests whether this value is equal to 0
This kind of code is probably the reason why C and C++, for some people at least, have a
somewhat dubious reputation. We can formulate very concisely, but the resulting code may well
be quite illegible. You should wait until you feel more comfortable with C++ before writing this
kind of code. However, this does not mean that you need to be familiar with everything. The
most important thing is being able to read the code.
Note that always placing literals to the left of a comparison helps to avoid some of these
errors, because a statement such as 42 = x is illegal.
Assignments
* The designers of C believed that there was no point in giving the most commonly used operator a name,
which, as in other programming languages, usually consists of two characters (such as :=).
38 3: Basic Concepts of C++ Programs
Operator Meaning
simple assignment
It is true of all assignment operators that an assignment can be part of a larger expression.
An expression with the assignment operator yields the value of the variable to which a new value
was assigned. This makes chained assignments possible:
x = y = 42; // 42 is assigned to both x and y
The expression is evaluated as
x = (y = 42);
Inside the parentheses, y is assigned a value of 42. The expression inside the parentheses then
yields y, which is finally assigned to x. In this way, x is also given the value 42.
In the same way, you can make an assignment part of a larger expression:
if ((x = £()) < 42) — // assign the return value of£() to x and
// compares this value with 42
The other assignment operators are abbreviations for a combination of two operators: the opera-
tor situated before the equals sign and the assignment, i.e.
X Op= y corresponds to So Sova,
These operators were introduced because compilers were not optimized at the time C was in-
vented, and C was developed for a time-critical operating system (UNIX). Instead of
X= x + 7; // increasex by 7
This did shorten the running time, because the compiler knew, without special code analysis,
that the value to which seven was added would be at the same address to which the result of the
addition is to be stored. Compilers nowadays should be able to cope with these kinds of trivial
optimizations. However, the style is still used, as it is more concise, without being illegible.
3.2 Types, Operators, and Control Constructs )
Operator | Meaning
a Postfix increment (‘a++’)
= Postfix decrement (‘a--’)
ba: Prefix increment (‘++a’)
re Prefix decrement (‘--a’)
There are two versions of the increment and decrement operators, a prefix and a postfix
version. Both versions increase or decrease the value of a variable by one, respectively. The
difference lies in what the expression yields as a whole. The prefix version (the version in which
the operator precedes the variable) returns the value of the variable after the increase:
x = 42;
Std>:cout << ++x; // writes 43 (first increase, then yield the value of x)
Side Goubm<<aor- // writes 43
The postfix version (the version in which the operator follows the variable) returns the value of
the variable it had before the increase:
x = 42;
Stdecou vaca: // writes 42 (first yield the value x, then increase)
stds Colt << x: // writes 43
It is from this operator that the name of C++ originated: ‘one step further than C’°.
As long as increment and decrement operators are not used as part of a larger expression,
you should stick to using the prefix version, i.e. ++x. The other version may lead to a temporary
value being created for the value of x before the increase, which is less efficient.
Bit Operators
The low-level nature of C++ can be recognized by the fact that there are special operators that
operate bitwise. These are listed in Table 3.9.
The >> and << operators should be familiar from I/O operations discussed previously. That
usage of them can be considered as a misappropriation of the shift operators for special types,
namely the //O stream types. In the meantime, this frequent (ab)use of << and >> leads to the fact
> An interesting question is why the language was not called ‘++C’. If you use C++ as part of a larger
expression, this expression returns the old value, i.e. C :-).
40 3: Basic Concepts of C++ Programs
Operator | Meaning
<< left shift
>> right shift
bit-wise AND
i bit-wise XOR (exclusive or)
| bit-wise OR
bit complement
that these operators are being called input and output operators®. As a consequence, the exact
semantics of these operators depend on the operands:
e Provided both operands have an integral type, it is a shift operation:
an teexes
ob RES Le // left-shifts the bits in x
The right operand must not be negative.
e Ifthe first operand is an I/O stream, such as std: : cout, it is an input or output operation:
std::cout << 2; // write the number2
Special Operators
Three further operators require special attention. They are listed in Table 3.10.
Operator Meaning
ae conditional evaluation
> sequence of expressions
Sizeore ce storage size
e Conditional evaluation
The operator ?: enables a conditional evaluation. Depending on the condition, one of two
possible values is returned. This is the sole three-digit operator of C++.
The expression
condition ? expression1 : expression2
yields expression! if the condition is satisfied, otherwise expression2.
Using this operator, if a value is manipulated in the then-case as well as in the else-case,
then if statements can be shortened. Instead of
° The discussion of the input and output techniques of C++ in Section 4.5 on page 195 will discuss the
motivation of this ‘misuse’.
3.2 Types, Operators, and Control Constructs 41
Operator Precedence
The previous operators, and all others introduced during the course of the book, have different
precedences and evaluation orders. The table of all operators on page 569 lists these. The usual
arithmetic rules apply, such as ‘point calculation comes before line calculation’.
The precedence and evaluation order can be changed by using parenthesis. Two examples are
listed below:
(xo y uk 2 // without parentheses, first y is multiplied by z
while ((x += 2) < 42 )_ // without parentheses, x is increased by 1 (which is the
// numeric value of true, which is the result of 2 < 42)
42 3: Basic Concepts of C++ Programs
All types of control constructs in C++ are adopted from C. There are single and multiple selec-
tions, different loops and the facility to combine several statements in one block.
Selections
e switch
This is used to select between multiple constant values:
switch (x) {
case 7:
Sti: COUGECGN eS iu<cusitdrsendls:
break;
case 17:
case 18:
Std
ss COlte<Gulyan Sal (mot Olas GC enain
break;
default:
std::cout << "x is’ neither 7 nor 17 nor 18" << std::endl;
break;
hp
The case labels define the places where a case begins. If the expression evaluated by switch
has this value, all statements starting from the corresponding case label are executed until a
break is found. Multiple case labels can be positioned as a case entry.
The break statement is used within a switch statement to end a case and continue with
the statement after the switch statement. If a case is not ended by a break statement, the
following statements are executed, even if there is another case label in between.
The optional default label indicates ‘all other cases’. This default case can be located
anywhere. If no default case is executed and none of the given cases apply, no statement
inside the switch statement is executed and the program continues after the closing brace.
3.2 Types, Operators, and Control Constructs 43
Loops
/* loop that will run for every second value from 100 to 0
* (100, 98, 96, 94, ... , 0)
ef
for (i=100; i>=0; i-=2) {
std::cout << "i has the value: “ << i <<"std::end1-
ds
Here, i starts with the value 100 and is decreased by 2 after each iteration. The loop runs as
long as i is greater than or equal to 0.
The individual expressions in the head of a for loop can also be skipped. If no condition
is specified, this means that the condition is always satisfied:
// endless loop
fore at
std::cout << "this goes on like this forever" << std::endl;
}
Using the comma operator (see Section 3.2.3 on page 41), initialization or reinitialization can
consist of numerous statements. For example, with an array, two indices can run to each other
(although arrays are not introduced until Section 3.7.2 on page 106, this code should still be
comprehensible):
// swapping values in an array
int array[100]; // array of 100 whole integral values
Blocks
The bodies of all control constructs are enclosed by braces in the examples. These brackets are
used to combine several statements into a single block of statements. This is usually required with
control constructs, because the body may only comprise one statement or a block of statements.
If two individual statements are written, only the first one belongs to the control construct. The
second statement is considered as the first statement after the control construct:
Sie x eam fy
std::cout << "x is smaller than 7" << std::endl;
std::cout << "this statement is executed in every case" << std::endl;
Even though it is unnecessary, I would recommend using braces even if the body of a control
construct consists of only one statement:
at cee)
std::cout << "x is less than 7" << std::endl;
}
std::cout << "this statement is executed in every case" << std::endl;
Not only is it more legible, but it also prevents the brackets from being forgotten when a second
statement is inserted in the loop body.
3.2.5 Summary
e C++ provides various different types for characters, integrals, floating-point numbers and
Boolean values.
e The exact value range of the numeric types is system specific.
e C++ has many operators. This includes operators for incrementing and decrementing, as well
as for modifying assignments.
e The = operator is the assignment operator; the == operator is the equality test.
e C++ has all the usual control constructs (selections, loops).
e The for loop allows us to iterate values flexibly.
e Several statements can be combined, using braces, into one statement block.
46 3: Basic Concepts of C++ Programs
C++ is a language with type checking. This means that, at compile time, you can see whether
operations such as function calls are meaningful, at least as far as type is concerned. If you divide
functions into different modules that are compiled separately, the question arises as to how we
can ensure this kind of type check.
For the passing of data between functions of different modules to be successful, the interface
of a function is declared in a separate header file. This file is then included by the module that
implements the function, as well as all modules that call the function.
The following program shows an example of this. It consists of three files, which, as shown
in Figure 3.1, have the following roles:
e Incross.hpp, the crosssum() function is declared. It is therefore known and can be called
in all modules that include this file.
e In cross.cpp, the crosssum() function declared in cross.hpp is implemented. By
in-
cluding cross.hpp, we ensure that declaration and implementation do not contradict each
other.
3.3 Functions and Modules
e e
ee ee ee ee ee EG47
#tendif
#endif
The header file is enclosed by preprocessor instructions that ensure that the contents of the file
are processed only once, even if the file is included more than once by a translation unit:
#ifndef CROSS_HPP // is only satisfied in the first run, because
#define CROSS_HPP // CROSS_HPP gets defined in the first run
#endif
The uppercase filename is typically used as a symbol. Every header file should be enclosed by
this sort of ‘guard’. The precise reasons are discussed in Section 3.3.7 on page 52.
48 3: Basic Concepts of C++ Programs
The source file cross. cpp with the implementation of crosssum() has the following structure:
// progs/cross.cpp
#include "cross.hpp"
return cross;
}
In this file,
#include "crosscross.hpp"
is used to include the file with the declaration. By doing this, the compiler can determine whether
there are contradictions between declaration and implementation.
Every file that calls crosssum() includes the header files with its declaration, as every function
must be declared before being called:
// progs/crosstest.cpp
// implementation of main ()
int main()
{
printCrosssum(12345678) ;
printCrosssum(0) ;
printCrosssum(13*77) ;
}
// implementation of printCrosssum()
void printCrosssum(long number)
el
std::cout << "the cross sum of " << number
<<" 71s “<< crosssum(number) << std::endl;
In this case, the test program contains two functions, main() and printCrosssum(). Within
main(), the printCrosssum() function is called three times. As this function is implemented
after having been called, it also has to be declared before main(). Otherwise an error message
occurs, warning that the function called is unknown. As we can see, you can omit the parameter
names when declaring a function. These names are nevertheless often written because the name
documents what the parameter is for (see, for example, the declaration of crosssum() in the
header file cross .hpp).
Every time printCrosssum() is called, a number is passed whose crosswise sum should be
output. Within the function, the number itself and the crosswise sum calculated by crosssum()
are output. The parameter is usually passed ‘by value’. This means that the parameter number
in printCrosssum() is a copy of the argument passed. Thus it could be changed inside
crosssum() without any consequences for the argument passed by the caller.
crosstest or
crosstest.exe
’ Although, there is no standard ending specified, all endings start with .c; which is why these files are
called dot-C files.
* In fact, these system header files do not even need to exist as files. The standard only requires that
#include <iostream> makes all required I/O declarations available. The way this is done is up to the
C++ environment. However, in practice, you can find corresponding files without endings in a system
directory.
3.3 Functions and Modules 51
As previously explained, the contents of another file can be included by the preprocessor using
#include. The required filenames can be enclosed by double quotes or angled brackets:
#include <header1.hpp>
#include "header2.hpp"
In both cases, these files are searched in the system directories for header files. Their exact
location is system specific. In many systems, you can influence the path for the system directories
when compiling by making use of the -I option.
If you use double quotes, the files are additionally searched for in the local directory before
the system directories.
Conditional Compilation
The preprocessor can also be used to influence what parts of a file should actually be compiled.
For example:
52 3: Basic Concepts of C++ Programs
void f()
if
#ifdef DEBUG
std::cout << "x has the value: " << x << std::endl;
#endif
}
Here, the output statement is only compiled if the DEBUG constant is defined. The constant can
be defined both inside the source code and when calling the compiler (typically by passing the
compiler option -Dconstant).
Because specific constants are defined for each system, we can also handle system differences
if necessary. For example:
#if defined __GNUC__
// special treatment for the GNU compiler
# if __GNUC__ == 2 && __GNUC_MINOR__ <= 95
// up to Version 2.95
# else
// afterwards
# endif
#elif defined _MSC_VER
// special treatment for the Microsoft Visual C++ compiler
#endif
Another example of the use of conditional compilation are the guards around the code in header
files:
#ifndef CROSS_HPP // is only satisfied in the first run, because
#define CROSS_HPP // CROSS_HPP gets defined in the first run
#endif
If a file includes this header file, the statements of this file are adopted. Therefore, from
#include "cross.hpp"
we get
3.3 Functions and Modules 53
#endif
This causes all lines of cross. hpp to be processed only if the constant CROSS_HPP is not yet
defined (#ifndef stands for ‘if not defined’). This is the case with the first run through the file.
However, consider the file is then included a second time. Thus, from
#include "cross.hpp"
#include "cross.hpp"
we get
#ifndef CROSS_HPP // is only satisfied in the first run, because
#define CROSS_HPP // CROSS_HPP gets defined in the first run
#endif
#ifndef CROSS_HPP // is only satisfied in the first run, because
#define CROSS_HPP // CROSS_HPP gets defined in the first run
#endif
In this case, the contents of cross. hpp are only taken into consideration with the first inclusion.
During the second inclusion, all characters from cross.hpp are ignored, because the first run
defines CROSS_HPP and so the second condition does not apply. In this way, error messages
regarding double definitions are avoided.
You might argue that the same header file is rarely included twice, one directly after the other.
However, this often happens indirectly, when two included header files include the same header
file. So, because it is always possible that header files are included more than once for the same
translation unit, the enclosing guards should always be part of a header file.
Definitions
Definition commands of the preprocessor can be used to define your own constants in the code:
#define DEBUG
#else
#endif
}
In old C programs, preprocessors were also used for defining constants:
#define NUM 100 // BAD
}
This kind of preprocessor misuse for the actual data flow can and should be avoided in C++. We
are dealing with ‘dumb’ text replacement, which is neither subject to the rules of type checking
nor takes scope into consideration. C++ uses the concept of constants for this:
const int num = 100; //OK
4;
Using #define you can even pass parameters. These so-called preprocessor macros should
also be avoided. There are also suitable language features in C++, such as inline functions (see
Section 4.3.3 on page 173) and function templates (see Section 7.2 on page 429).
3.3.8 Namespaces
As explained in Section 3.1.5 on page 28, the concept of the namespace is used in C++ for the
logical grouping of types and functions. All symbols can be assigned a namespace. If a symbol
is used outside the namespace, it must be qualified. By doing this, name conflicts are avoided and
it is made clear what symbols logically belong together (form a logical package or component).
The following example demonstrates the use of namespaces:
namespace A {
typedef .. String; // define type A: : String
void foo (String) ; // define A: :f00(A: :String)
3.3 Functions and Modules 55
namespace B {
typedef ... String; // define type B: :String
void foo (String); // define B: :f00(B: : String)
}
The namespace keyword assigns the symbols defined within to the scope of this namespace.
These symbols can no longer clash with symbols of other namespaces or global symbols that
have the same name.
When using these symbols outside the namespace, the namespace has to be qualified:
ASsetring sls // create a string of namespace A
Be String -s2: // create a string of namespace B
Normally, all local variables and objects of a function are created when the corresponding point
of definition is reached at run time. If the block or function is left, the local variables and objects
are automatically destroyed:
void foo()
4
5
Uae Bie // is created with an undefined value each time this point is reached
std::string s; //is created using a empty string as its value each time this
3 // point is reached
} // each leaving of £00() destroys x ands
56 3: Basic Concepts of C++ Programs
Using static, we can establish that a local variable is initialized the first time the definition is
reached and destroyed at the end of the program. In this case, the variable keeps its value even
after leaving the function. This value can then be reused the next time the function is called. In
other words, this is a global variable that can only be accessed locally within a function:
void foo()
{
static int number0fCalls = 0; _ //is initialized with thefirst call and
// destroyed at the end of the program
}
The number 0fCal1s variable is initialized with the value passed for initialization purposes when
foo() is first called. It remains valid for the duration of the program. Once the value is initial-
ized, the initial value is no longer important. The value of the variable is increased by one with
every function call. The number of calls of this function is therefore counted in this variable.
This number is then output with each call.
These kinds of static variables or objects are useful if we wish to keep a status between two
function calls, without explicitly passing data from one call to another. However, it is better to
avoid these kinds of variables altogether. One reason is that there are considerable problems if
you can access these variables in a parallel manner, such as in multi-threading programs.
Another application of the static keyword is used to limit the visibility of a variable or a
function to one module. By doing so, you can program auxiliary variables or functions whose
names cannot conflict with symbols of other modules.
In the following example, a status variable and an auxiliaryFunction() function are
declared, which are only accessible within the module:
static std::string status; // only known in this module
++status;
3.3 Functions and Modules 57
if (status == 17) {
auxiliaryFunction() ;
t
Calling auxiliaryFunction() or accessing status outside the module causes an error. Only
foo() can be called from outside this module.
There is a new language feature for this kind of use of static in C++: anonymous names-
paces. This is a namespace for which no name is assigned. Using it, the above example reads as
follows:
namespace { // everything here is only known in this module
std::string status;
void auxiliaryFunction();
t
++status;
y
The use of anonymous namespaces has the advantage that static is now only useful in influ-
encing the lifespan of the variables. The semantics for limiting visibility is omitted for other lan-
guage features. In this respect, the use of anonymous namespaces is preferable to using static;
static should only be used in influencing the lifespan of variables in C++.
58 3: Basic Concepts of C++ Programs
3.3.10 Summary
e Two types of files are used in C++: header files for declarations and dot-C files (or sometimes
just called source files) for actual implementations. The header files are included by the dot-C
files for compilation.
e Header files typically have the ending .hpp.
e Dot-C files typically have the ending . cpp.
e Using the preprocessor, header files can be included before the actual compilation of a dot-C
file.
e Using the preprocessor, code can be written that is only compiled under certain conditions.
e Header files should essentially be enclosed by preprocessor guards that avoid errors by in-
cluding them more than once.
e There are better ways of defining constants and macros in C++ than using the preprocessor.
e Symbols can be logically grouped using namespaces.
e Using static, local variables can be defined whose lifespan is the duration of the program.
e Using anonymous namespaces, the visibility of variables and functions can be limited to
modules.
3.4 Strings 59
3.4 Strings
In C++, there are no built-in types for the use of strings. However, in contrast to C, there is a
string type, provided by the standard library. More precisely, we are dealing with a class that
defines the capabilities and properties of strings. This class is implemented in such a way that
we can work with strings in the same way as with fundamental types. We can therefore assign
strings using =, compare them using == and concatenate them using +.
The fact that a string class exists, which provides this type as a fundamental type, is a great
advantage. Modern data processing is, by and large, string processing. Names, data and texts
are recorded, passed and processed as character sequences. In languages in which there are no
simple string types (for example, in C or Fortran), strings are often a source of trouble, which is
no longer the case in C++.
int main()
{
// create two strings
std::string firstname = "bjarne";
std::string lastname = "stroustrup";
std::string name;
// manipulate strings
firstname[0] = ’B’;
lastname [0] = "S’;
// chain strings
name = firstname + " " + lastname;
// compare strings
ifs (names!= "") tf.
// output strings
std::cout << name
<< " is the founder of Ct+" << std::endl;
60 3: Basic Concepts of C++ Programs
First, in addition to the standard header file for I/O, the standard header file for string processing
is included:
#Hinclude <iostream> // C++ header files for I/O
#include <string> // C++ header file for strings
Three strings are then created:
std::string firstname = "bjarne";
std::string lastname = "stroustrup";
std::string name;
The first two strings are initialized with "bjarne" and "stroustrup". The third string has no
value for initialization. Its initial default value is the empty string.
What happens here can be described in two different ways:
e From the point of view of traditional programming, variables of type std: : string are de-
fined.
e From the point of view of object-oriented programming, objects or instances of the class
std::string are created.
In object-oriented terminology, types are called classes. If concrete variables of a class are
created, these variables are described as objects or instances. The effect is basically the same:
something is created that represents a character sequence under a particular name.
The strings are manipulated in the following statement:
firstname[0] = ’B’;
lastname[0] = ’S’;
The first characters in the two initialized strings are corrected appropriately. As you can see, we
can access the ith character of a string using an index operator. As always in C and C++, the first
character has the index 0.
Finally, both initialized strings are concatenated with a space in between and are assigned to
the name string:
name = firstname + " " + lastname;
As you can see, we can use the usual comparison operators != and == for strings.
Strings can be output like any other types:
std::string name;
” In Java, the operator == exists for strings but it compares whether two strings are identical (i.e. have the
same address), instead of whether their values are equal. This is often a source of trouble and confusion.
'© The actual type of string literals is const char*. An explanation and the precise consequences of this
type are given in Section 3.7.3 on page 109.
62 3: Basic Concepts of C++ Programs
this reason, we cannot use string literals as both operands. For example, we cannot simply
concatenate only string literals:
"hellots+: ele+tesworld! //ERROR
In this case, we must convert at least one of the first operands explicitly into a string. This is
easily done (cf. page 35, where we converted a char into an int):
static _cast<std.:string>(
hello") + 7 ar word 9) OK
Value Semantics
Especially for those programmers coming from languages such as Java or Smalltalk, the follow-
ing hint is important: C++ has value semantics for every type. That means that when a variable
is declared, memory is created for the necessary data.
This is an important difference from languages with (partial) reference semantics such as Java
and Smalltalk. In these, a variable declaration simply creates a reference, which at first refers to
nothing (NIL or null). Only when new is called is a real object created with memory for the
necessary data.
In C++, there is also a new keyword. However, this is only required for creating objects that
have to be managed independently from block boundaries (this will be discussed in Section 3.8
on page 114). Thus, usually there is no need to use new in C++.
int main()
4
const std::string start("http:"); // start of an HTML link
const std::string separator (" \"\t\n<>"); // characters that end the link
std::string line; // current line
std::string link; // current HTML link
std::string: :size_type begIdx, endIdx; // indices
// output link
// - ignore "“http:" without further characters
if ‘(link !=o"http:") G{
Tink*="std: string (Link: ") 4) link;
Sittd-jcout << lanky <<estd-endir:
}
Types
In main (), variables are declared and objects are created as follows:
e Anunchangeable start string as a search criterion for the start of an HTML link:
consti stds stringrystart (http: )s // start of anHTML link
All strings that start with http: are therefore searched for. To make sure that this object
cannot be manipulated, it is declared as being const.
e A constant string in which all characters that end an HTML link are given:
const std::string separator(" \"\t\n<>"); // characters that end the link
In this case, a link is ended by a space, the " character, a tabulator, or a newline (which is
unnecessary because we operate line by line), as well as the < and > characters (see the table
of special characters on page 34).
e Two variables for the current line and the current link:
std::string line; // current line
std::string link; // current HTML link
e ‘Two variables for the start index and the end index of a link:
std::string: :size_type begIdx, endIdx; //indices
Here, two different types are used, std::string and std::string: :size_type. Type
std::string is the string type of the standard library std. std::string: :size_type is
an auxiliary type of this string class. This special type is used for indices because there is a
special value representing ‘no index’: std: : string: :npos (‘no position’).
The main part of the program comprises a loop that reads from the standard input line by line:
std::string line;
}
The lines are read from std: :cin and stored in line. The character for the line break, ‘\n’, is
read, but not stored.
while tests the return value of getline() to decide whether to continue the loop. The
return value is the input stream, std: : cin, passed as the first argument. This stream can be
3.4 Strings 65
used as a condition in a status query. This is possible due to some language features that will
be introduced later in this book. Thus, without trying to understand why, it is enough for the
moment to understand that it is possible to read a line and check whether the read was successful
this way.
String Operations
Different string operations are called within the loop. They all have the syntax of a member
function, i.e. line. operation(). An operation is called for the current line, which processes
this line in any form.
First, find() is used to search for the first occurrence of the start of a link:
const std::string start("http:"); // start of anHTML link
std::string line; // current line
std: :string::size_type begIdx; /| index
}
Provided the search for the beginning of an HTML link is successful in the current line, the inner
loop will be entered. An additional occurrence of http: in the line is then searched for inside
the inner loop.
Once the start index has been found, the end of the HTML link is searched for inside the inner
loop. The find_first_of () operation enables you to search for a number of characters at the
same time. As soon as one of the characters passed as a parameter is found, its index is returned.
Again, std: : string: :npos is returned if none of the characters are found.
In this case, the separator characters defined above are searched for in line:
const std::string separator(" \"\t\n<>"); // characters that end the link
std::string line; // current line
std::string::size_type begIdx, endIdx; // indices
The optional second parameter determines the point from which the separator characters are
searched for. In order that the characters before the start of this HTML link play no role, we only
begin looking after the start of the HTML link.
66 3: Basic Concepts of C++ Programs
In the next statement, the actual HTML link is extracted from the line. Depending on whether
a trailing character was found or not, all characters from the first index to the last index, or to the
end of the line, are extracted:
// extract link
if (endIdx != std::string::npos) {
// extract from the start to the end
link = line.substr(begIdx, endIdx-begIdx) ;
}
else {
// no end found: use remainder of the line
link = line.substr (begIdx) ;
}
When called with a parameter, substr() finds the substring of all characters from the passed
index to the end of the string. An optional second parameter is used as the maximum number of
characters to be extracted. In this case, this is the difference between the end index and the start
index.
The next statement outputs the link. But, before, it checks whether any characters follow
http:
// output link
// - ignore “http:" without further characters
bio (linke! = Shi tp: "i.
Jink== string Clink: s+ link.
std: :icout << link << std::endl;
}
The fundamental operators for strings (!=, + and =), which were introduced in the first string
example, are again used here.
All that remains to be done is to find an additional available HTML link on the same line,
which can, of course, only be the case if the end of the previous link was not the end of the line:
Operation Effect
=, assign() hee new value
swap () swap values between two strings
+=, append(), push_back() append characters
insert () insert characters
erase() erase characters
clear () erase all characters (empty string)
resize() change number of characters
(erase or add characters at the end)
replace() replace characters
+ create sum string
==, !=, <, <=, >, >=, compare() | compare sum string
length(), size() return number of characters
empty () test whether a string is empty
Pi eacc) access a single character
>>, getline() feed in string
<< output string
find functions search for a part string or character
substr() issue a part string
eCetr() use string as C-string
copy () copy a character into a character buffer
For a more precise description of the operations (parameters, return value, semantics), the
reader is referred to the appropriate reference handbooks for the C++ class library (such as my
book, The C++ Standard Library; see [JosuttisStdLib}).
In Section 3.6 on page 93, we discuss another important general aspect of the string class:
behavior in the event of an error. Behavior in the event of an error plays a particular role in
accessing a single character of the string. When using the index operator, using an invalid in-
dex leads to undefined behavior. If we use the member function at () instead, an exception is
thrown in the case of an invalid index, which automatically leads to proper error handling in the
program. More information on error handling when accessing a character of a String is given in
Section 3.6.4 on page 96.
68 3: Basic Concepts of C++ Programs
std::string s;
std::string s;
3.4.5 Summary
e The C++ standard library provides the std: : string type for strings.
e Strings can be used as fundamental types.
e Several search functions allow you to search for characters or substrings in strings.
e The std::string: :size_type type is used for string indices.
e The std::string: :npos value stands for ‘no index’.
e c_str() allows strings to be converted to C-strings.
e Strings can contain any characters (including special characters).
3.4 Strings 69
3.5 Collections
Modern data processing involves not only single data values, variables or objects being pro-
cessed, but collections of these. In many conventional programming languages, appropriate data
structures such as arrays, linked lists, trees or hash tables must be programmed to manage col-
lections. This is not necessary in C++.
The C++ standard library offers an individual framework for working with collections: the
Standard Template Library (STL). This framework provides mechanisms in which different data
structures (so-called containers) can be used for collection processing by using single type decla-
rations. There are also mechanisms with which you can apply operations (so-called algorithms)
to these types of collections, independently of the underlying data structure.
The main advantage of the STL is that it provides an interface, which means that you no
longer need concern yourself with the details of the data structures and the necessary memory
management when processing collections. These details are abstracted. This abstraction was
done while paying careful consideration to performance issues. As a result, using the STL is just
as fast as manually programming data structures and algorithms.
A complete introduction to the STL would be too much information for this book '!. However,
the use of the STL for simple collection management is explained in this section.
int main()
{
Stdurvector<int> «colls // vector container
for ints
'' | would recommend my book The C++ Standard Library — A Tutorial and Reference, published by
Addison-Wesley.
3.5 Collections
WA
By using this type, we are dealing with a class template. This means that this is a class in which
not all details are fixed. The type of the elements managed in the collection must be indicated in
angled brackets. The declaration
std: :vector<int> coll;
creates this kind of container. In this case, this is a vector that contains elements of the int type.
The newly created container is initially empty.
By calling push_back () for the container, elements can be added to the end of it:
coll.push_back(i) ;
In the loop for outputting elements, the current number of elements in the container is queried
using size():
for (unsigned i=0; i<coll.size(); ++i) {
J
An unsigned integral type is used as a running variable i. If i is simply declared as int, then
comparing it to coll.size() may cause a warning that an unsigned value is being compared
with a value that is signed.
Inside the loop, the element with the index i is output. Here, the typical array syntax can be
used:
Std -coutss<ecolilil<<e 7s
As usual, the first element has the index 0 and the last element has the index size() — 1.
The program produces the following output:
iL 2s) 42 & Is
72 3: Basic Concepts of C++ Programs
Looks easy, doesn’t it? Pay attention though—STL containers are not ‘idiot proof’. The con-
tainers are programmed so that you see nothing of the underlying data structure. However, good
performance was a design goal. As a consequence, when accessing an element with [i], as with
strings, no test is made to determine whether the index is correct. This must be determined by the
programmer. The test is omitted because it takes time. When weighing up the advantages and
disadvantages (faster versus safer), it should be remembered that this behavior could be wrapped
at any time by a checking collection type. However, you cannot wrap a checking type that then
does not do any checking.
#include <iostream>
#include <deque>
#include <string>
int main()
{
std: :deque<std::string> coll; // deque container
for strings
In this case, the header file for the deque is included using
#include <deque>
The declaration
std: :deque<std::string> coll;
creates an empty collection of string elements.
The elements are inserted using push_front ():
coll.push_front ("often") ;
A vector is fast if you insert or erase elements only at the end. A deque, however, is fast at
both ends (though it may be a little slower than vectors, due to the internal structure). In both
containers, the insertion and deletion of elements in the middle is relatively slow, as elements
must be moved to one side.
This difference in capabilities is also mirrored in the interfaces of the two classes. The deque
offers functions for inserting and removing at both ends. As with vectors, elements can be
inserted using push_back() with deques. However, the push_front () function is not provided
for vectors.
STL containers generally offer special functions that mirror the special capabilities of the
container and have a good running time. This helps prevent programmers using functions or
containers with a poor performance. However, elements can be inserted into both containers
at any position using a common insertion function (this is explained later in Section 3.5.7 on
page 84).
3.5.4 Iterators
STL containers are generally accessed using iterators. Iterators are objects that can ‘iterate’
containers. Every iterator object represents a position in a container.
The advantage of iterators is that they allow all containers to use the same interface when
accessing the elements. The increment operator always advances an iterator one position. This is
independent of whether the underlying data structure is an array, a linked list ora binary tree. This
is possible because the actual iterator types for a container are provided by the container itself.
Thus it is implemented such that the iterator does the right thing according to the underlying data
structure.
The following fundamental operations define the behavior of an iterator:
e Operator *
returns the element at the position of the iterator.
e Operator ++
advances the iterator one position.
e Operator == and operator ! =
returns whether two iterators represent the same object (same container, same position).
e Operator =
assigns the position of one iterator to another.
Containers provide appropriate member functions for working with iterators. The two most
Important are:
e begin()
returns an iterator that represents the position of the first element in the container;
e end()
returns an iterator that represents the ‘past-the-end’ position behind the last element of the
container.
3.5 Collections 5)
The range from begin() to end() is therefore a half-open range (see Figure 3.4). This has the
advantage that a simple end condition can be defined for iterators that iterate over containers:
The iterators run until end() is reached. If begin() is equal to end(), the collection is empty.
The following sample program outputs all the elements in a vector container with iterators.
This is the example of page 70, transposed for use with iterators:
// stl/vector2.cpp
#include <iostream>
#include <vector>
int main()
.
std::vector<int> coll; // vector container
for ints
After the collection has been filled with the values 1-6, all elements in the for loop are output
using an iterator. The iterator pos is initially defined as an iterator of the corresponding container
class:
std: :vector<int>:: iterator pos;
The type of the iterator is therefore ‘iterator of the container type vector<int> of the stan-
dard library std’. This shows that iterators are provided by the corresponding container class.
Inside the for loop, the iterator is initialized with the position of the first element, and then
iterates over all elements until it reaches the end (i.e. the position after the last element; see
Figure 3.5):
for (pos = coll.begin(); pos != coll.end(); ++pos) {
cout << *pos << 7 7;
}
In the body of the loop,
*pos
is used to access the actual element.
Instead of iterator, const_iterator can be used as the type for the iterator:
std: :vector<int>::const_iterator pos;
This type ensures that elements cannot be modified when they are accessed via the iterator. This
type also has to be used if the container is non-modifiable (that is, declared as being const).
In Section 7.5.1 on page 453, a const_iterator is used in a generic function that can output
elements of any given STL container.
Selection-free access is therefore no longer possible. In order to access the tenth element, you
must navigate through the first nine elements. A step to neighboring elements is possible in both
directions in a constant time. Accessing a particular element takes a linear time (it is proportional
to the distance from the actual position). Inserting and removing elements in all positions takes
the same amount of time. Only the appropriate links have to be changed.
The following example defines a list of characters, inserts the characters ‘a’ to ‘z’ and outputs
them using an iterator:
TA /iustl/listie epp
#include <iostream>
#include <list>
int main()
{
std::list<char> coll; // list container
for chars
The loop that outputs all the elements looks exactly the same as before. Only the type of the
iterator has been changed. Because this type belongs to a list, it knows that it has to traverse a
link, rather than increment an index, to get to the next element (see Figure 3.6).
int main()
if
std: :set<int> coll; // set container
for ints
3.5 Collections 79
In this case, we could also specify a sort criterion. As this is not the case, the elements are sorted
by default in ascending order using the < operator.
An element is inserted using the insert () member function, which inserts the elements into
the position according to the actual sorting criterion:
col Peinsert
(3) 5
coll.insert (1);
After the values have been inserted, the condition displayed in Figure 3.7 is produced. The
elements are sorted in the internal tree structure of the container, so that elements with a smaller
value are found on the left and elements with a larger value on the right. Because a set is used
rather than a multiset, the element that is inserted twice, the value 1, only exists once in the set.
It would have been available twice in a multiset.
80 3: Basic Concepts of C++ Programs
The output of all elements works according to the same pattern, as in the previous example.
An iterator traverses all elements and outputs them:
std::set<int>::iterator pos;
for (pos = coll.begin(); pos != coll.end(); ++pos) {
std::cout << *pos << ’ ’;
}
The increment operator of this iterator is defined in such a way that it finds the correct successor
element by taking into account the tree structure of the container. From the third element, we
move up to the fourth element, and then down again to the fifth element (see Figure 3.8).
The output of the program is as follows:
h23 4b
oN
\
Ulyy
When using maps, we must ensure that an element of the collection is a key/value pair. This
impacts the way declarations, insertions and element access is carried out:
// stl/mmap1.cpp
#include <iostream>
#include <map>
#include <string>
int main()
{
// datatype of the collection
typedef std: :multimap<int,std::string> IntStringMMap;
Because the type of the container is required in many places, it is defined once as a type:
typedef std::multimap<int,std::string> IntStringMMap;
Instead of with
std: :multimap<int,std::string> coll;
82 ee Le Concepts
eS3: Basic of C++IEPrograms
tiated Aiea al ese
9 ee eee
In general, typedef is used to define symbols that represent a certain type. This can be used to
replace complicated type names by a simple name or to get more flexibility (depending on the
platform, a type might be defined differently). In any case, this type definition is no distinct type,
but an alias that might be mixed with the original type.
The type defined here is a multimap, whose elements have the int type as the key and the
string type as the value.
Because the elements are value pairs, they are inserted using make_pair():
coll.insert
(std: :make_pair(5,"heavy")) ;
coll.insert
(std: :make_pair(2,"best")) ;
make_pair() creates objects of the type std: : pair, which is provided to represent value pairs.
We cannot simply output the elements. If we access an element using *pos via an iterator,
we get the type std: :pair<int,std::string>. With a value pair such as this, you can access
the first part of the value pair (in this case, the key) using .first, and the second part (in this
case, the value belonging to the key) using . second. The following could be written to output
the key and value:
Stdjcout <<) (*pos) fara <9 07, // output the key of the element
std::cout << (*pos).second << ’ ’; // output the value of the element
You should ensure that the expression with the operator * is inside parentheses, because it has
a lower precedence than the dot operator. For this combination of operators, the -> operator is
provided as an abbreviation, which is why the value of the element here in the example can be
output using
std::cout: << pos->second << 7777; // output the value of the element
The output of the program could be as follows:
The best parties are: heavy long
There are two elements with the value 5. Depending on the implementation of the standard
library, the following output is also possible:
The best parties are: long heavy
3.5.7 Algorithms
As the examples seen above indicate, the actual data structures of STL containers are encapsu-
lated. There may be some special operations (such as the index operator for vectors and deques),
but there are also common ways to access elements, such as using iterators.
We can use these common interfaces to write functions that can operate on any container (and
therefore on any data structure). The STL offers a collection of functions of this kind. They are
3.5 Collections 83
called algorithms. There are algorithms for finding, swapping, sorting, copying, combining and
modifying elements.
The example below shows a few algorithms and their use:
// stl/algol.cpp
#include <iostream>
#include <vector>
#include <algorithm>
int main()
f
std::vector<int> coll; // vector container
for ints
std: :vector<int>::iterator pos; // iterator
coll.push_back(2) ;
First of all, the two algorithms std: :min_element() and std: :max_element () are called.
As a parameter, they are assigned a range defined by two iterators, in which the minimum or
maximum element should be searched. An iterator for the position of this element is returned.
With the assignment of
pos = std::min_element(coll.begin(), coll.end());
min_element () therefore produces an iterator for the smallest element in the entire collection
(if there are more than one, the first is taken) and assigns it to the iterator pos. This element is
then output:
std::cout << "min: " << *pos << std::endl;
The next algorithm to be applied is std: :sort(). It sorts the elements of the range that is
passed by two iterators. As the range encompasses all elements of the container, all its elements
are sorted:
std::sort(coll.begin(), coll.end());
The last algorithm used in this example is std: :reverse(). It reverses the sequence of all the
elements in the given range:
std: :reverse(coll.begin(), coll.end());
The output of the program is:
aye a
max: 6
ey oy 4 Sy al
This example shows how easy it is to use different data structures. By using other types, you can
easily swap the data structures around. The same example works if you swap vector with either
deque or list. There is only a problem if a set is used. Because a set determines the sequence of
the elements itself, sort () and reverse() cannot be called. The attempt is stopped by a cryptic
error message from the compiler. In addition, push_back() has to be replaced by insert ().
A further example demonstrates how elements can be inserted in front of certain other elements
using algorithms and operators. The program has the following structure:
3.5 Collections 85
// stl/algo2. cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
int main()
f
std: : vector<std::string> coll; // container
for strings
std: :vector<std: :string>::iterator pos; // iterator
At first, different city names are inserted in a vector and sorted. The most interesting part is the
assignment with which we attempt to insert Hanover in front of Hamburg. The find algorithm is
called first, in order to find the position of Hamburg:
pos = find(coll.begin(), coll.end(), // range
"Hamburg") ; // search criteria
As is usually the case with algorithms, the beginning and end of the range to be searched are
passed to find(). The third parameter is the value that is being searched for. It is then compared
to all elements using the == operator.
If a corresponding element is found, its position is returned in the form of an iterator. If no
suitable element is found, the end of the collection is returned, which is checked:
if (pos != coll.end()) {
2;
If Hamburg is found, its position is used to insert Hanover in front of it. For this purpose, the
member function insert () is used. An iterator for this position is passed as a first parameter
and the value to get inserted is submitted as a second parameter:
coll.insert(pos,"Hanover") ;
int main()
Ul
std::list<int> Colla
std::vector<int> coll2;
3.5 Collections 87
/* RUNTIME ERROR:
* - copy elements into the second collection
*/
std::copy(coll1.begin(), colli1.end(), // source range
coll2.begin()); // destination range
The std: :copy() algorithm gets the beginning and the end of the source range and the be-
ginning of the destination range in which the elements of the source range are to be copied. It
assumes that the destination range is big enough to include all the elements. If the destination
range is empty (as in this example), unavailable memory is being accessed, which causes the
program to crash (hopefully, because then you see that something has gone wrong).
There are two ways to avoid this type of error:
1. We must make sure that the destination range is large enough.
2. We must use insert iterators.
To ensure that the destination range is large enough, it must either be initialized with the correct
size or explicitly set to the right size. Both are only possible with sequential containers (vectors,
deques, lists).
The following program shows this:
// stl/copy2.
cpp
#include <iostream>
#include <vector>
#include <list>
#include <deque>
#include <algorithm>
int main()
af
Stace sit amity @@llitils
Std savecron<aint> cold.
88 3: Basic Concepts of C++ Programs
Note that when passing an initial size to the container, the initial elements are initialized by their
default value.
Insert Iterators
The other method, using insertion iterators, is shown in the following example:
// stl/copy3.cpp
#include <iostream>
#include <vector>
#include <list>
#include <deque>
#include <algorithm>
int main()
{
std::list<int> Golan:
std: :vector<int> coll2;
std::deque<int> coll13;
3.5 Collections
89
Two special predefined iterators are used here (so-called iterator adapters):
e Back inserters
A back inserter inserts the elements at the end of a container (i.e. appends them). Each
element is inserted in the container, which is passed at initialization time using push_back ().
By calling
std::copy(coll1.begin(), colli.end(), // source range
std: :back_inserter(coll2)); // destination range
all elements of coll11 are inserted at the end of col12.
The operation can only be called for destination containers in which the push_back()
member function is available. These are vectors, deques and lists.
e Front inserters
A front inserter inserts the elements at the beginning of a passed container by calling the
push_front() function. This causes the sequence of the elements to be inserted in reverse
order. By calling
std: :copy(coll1.begin(), coll1.end(), // source range
std: :front_inserter(col13)); // destination range
all elements of col 11 are inserted at the beginning of co113.
The operation can only be called for destination containers for which the push_front ()
member function is available. These are deques and lists.
An additional form of iterator adapter is the stream iterator. These are iterators that read from
or write to a stream. Keyboard input or screen output form the ‘collection’ or the ‘container’,
whose elements are then processed. The following example shows how this may look:
90 3: Basic Concepts of C++ Programs
// stl/ioiterl.cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
int main()
i
using namespace std; // all symbols in std are global
/* read strings from the standard input up until the end of the data
* - copy from the ‘input collection’ cin, inserting into coll
it/,
copy (istream_iterator<string>(cin), // start of source
range
istream_iterator<string>(), // end of source range
back_inserter(coll)); // destination range
The expression
ostream_iterator<string>(cout,"\n")
creates an iterator for the output stream cout, which outputs string elements. Any type can
be passed here in angled brackets, for which the respective output operator << is called. The
optional second parameter defines what character sequence is inserted between two elements. In
this case, it is a line separator, which ensures that each string is written on an individual line.
To summarize, the program reads all the strings from the standard default input cin, sorts
them and outputs them to cout, with each string on its own line.
This example shows how concisely things can be programmed in C++ when making use of
the STL. In addition, by changing the types, we can easily check whether other data structures
accomplish the task faster as vectors. For example, we could try to use a set container instead.
Because a set sorts automatically, we no longer need to call sort (). Try it!
3.5.10 Endnotes
As mentioned previously, this was a short general introduction to the STL. Many details, back-
ground information and supplementary techniques were not explained. However, these examples
should be sufficient to start programming with collections.
Much use of STL containers is made in the remainder of this book. In Chapter 9, further
details and helpful techniques for working with the STL are discussed:
e In Section 9.1.1 on page 518, all the operations of vectors are discussed in more detail.
e In Section 9.1.3 on page 526, all standard algorithms are listed.
92 3: Basic Concepts of C++ Programs
Section 9.2.1 on page 536 shows how to manage references to objects in STL containers with
the help of smart pointers.
Section 9.2.2 on page 540 explains how we can define processing criteria for STL algorithms
using helper functions and function objects.
For further details, the reader is referred to special books on the C++ standard library and the
STL (see [JosuttisStdLib]).
3.5.11 Summary
The STL framework of the C++ standard library provides different containers, iterators, and
algorithms, which can be used to manage collections in different data structures.
In particular, the following types exist (listed with their typical data structure):
These sorts of errors can only be poorly tested and processed (if at all) by conventional lan-
guage features. How is one supposed to test whether everything has gone right when creating a
temporary object in the middle of an expression?
The problem here is that the error can be detected, but not handled appropriately. The only
possibility that remains with conventional languages is to issue a warning and to continue as best
as possible, or to end the program with an error message.
The basic problem is this: error cases can be detected, but not properly dealt with, because
the context from which the error originated is not known. Often the error cannot be reported
back to the caller because return values are either not defined or else used for other purposes.
We therefore require a mechanism that allows a distinction between error detection and error
handling, and allows the passage of information between both without using parameters or return
values. This could separate error handling from the normal data flow.
Exception handling provides this mechanism: errors can be detected at any given position in
the code and reported to the corresponding caller using a mechanism designed for this purpose.
The error can then be intercepted and processed according to the situation. If this does not occur,
the error is not just simply ignored, but leads to a clean program exit (rather than a program
abort).
To prevent a possible wrong interpretation, it is noted that exception handling in C++ means
that errors or situations are handled that appear during the normal processing of a program. It is
not a mechanism for interrupts or signals; thus it is not a mechanism for messages that interrupt
the running program from the outside, resulting in a jump to another place in the program. The
exceptions I talk about here are explicitly triggered by special C++ statements inside the program,
and are processed in the current scope.
It should also be noted that the mechanism is called ‘exception handling’ rather than ‘error
handling’. The exceptions do not necessarily have to be errors, and not every error is an exception
(input errors are more likely the normal data flow). The mechanism can and should be used to
react flexibly in unusual situations that do not correspond to the bandwidth of a ‘normal’ program
run. This does not include incorrect user input.
Exception Classes
Keywords
bad alloc
[see domain
invalid
error
argument
bad_typeid
exception eas
os_base::failure
range error
underflow error
bad exception
_
// progs/eh1.cpp
#include <iostream> /| header file for I/O
#include <string> // header file for strings
#include <cstdlib> // header file for EXIT_FAILURE
#include <exception> // header file for exceptions
int main()
1
ceva
// create two strings
std::string firstname("bjarne"); // may trigger std: :bad_alloc
std::string lastname("stroustrup") ; // may trigger std: :bad_alloc
std::string name;
3.6 Exception Handling 97
// manipulate strings
firstname.at(20) = ’B’; // triggers std: :out_of_range
lastname [30] = ’S’; // ERROR: undefined behaviour
// concatenate strings
name = firstname + " " + lastname; // may trigger std: :bad_alloc
5
catch (const std::bad_allock e) {
// special exception: no more memory
std::cerr << "no more memory" << std::endl;
return EXIT_FAILURE; // exit main () with error status
y
catch (const std::exception& e) {
// other standard exceptions
std::cerr << "standard exception: " << e.what() << std::endl;
return EXIT_FAILURE; // exit main() with error status
i;
catene( 2.) 4
// all other exceptions
std::cerr << "other unexpected exception" << std::endl;
return EXIT_FAILURE; // exit main() with error status
std::cout << "0K, everything was alright until now" << std::endl;
Using try, an area is set aside in which a common exception handling is defined. An ‘attempt’
is made to execute the statements found in the try block:
Le yar
// create two strings
std::string firstname("bjarne") ; // may trigger std: :bad_alloc
std::string lastname("stroustrup") ; // may trigger std: :bad_alloc
std::string name;
// manipulate strings
firstname.at(20) = ’B’; // triggers std: :out_of_range
asi¢name ls Ollm—moue // ERROR: undefined behavior
// concatenate strings
name = firstname + " " + lastname; // may trigger std: :bad_alloc
98 3: Basic Concepts of C++ Programs
If an exception occurs, the whole try block is exited immediately. In this example, if an ex-
ception is triggered in one of the declarations, the following statements are no longer executed.
Here, the try block is left by calling
firstname.at(20) = ’B’;
as an attempt is made to access the character with index 20 in firstname, and this character does
not exist. In contrast to the index operator, at () checks strings to find out whether the index is
correct and, if necessary, triggers a corresponding exception. When using the index operator,
checking is avoided for performance reasons (the following line would therefore not trigger an
exception, but would lead to the system crashing or some other undefined behavior).
The appearance of a reaction to an exception is defined via the catch blocks that follow the
try block:
catch (const std::bad_alloc& e) {
// special exception: no more memory
std::cerr << "no more memory" << std::endl;
return EXIT_FAILURE; // exit main() with error status
}
catch (const std::exceptionk e) {
// other standard exceptions
std::cerr << "standard exception: " << e.what() << std::endl;
return EXIT_FAILURE; // exit main() with error status
}
Gatch: (ie rt,
// all other exceptions
std::cerr << "other unexpected exception" << std::endl;
return EXIT_FAILURE; // exitmain() with error status
}
Reactions to exceptions of std: :bad_alloc type (i.e. memory shortage), std: :exceptio
n
type (i.e. all standard exceptions) and reactions to all other exceptions are defined in sequence.
The last catch block demonstrates a special facility to react to any given exception:
CauChat.n ama
// all other exceptions
}
A sequence of three dots in a catch statement stands for ‘any given exception
’. As we do not
know anything about the type of the exception, we can only react very generally here.
This sequence of catch blocks is no coincidence. When an exception occurs in
a try block,
the following catch blocks are searched in the sequence in which they are
given, in accordance
with a suitable handling technique. The statements from the first suitable handler
are used. As a
consequence, the sequences of the catch blocks have to be defined so that
special classes come
before general exception classes. A catch block for any given exception,
which is defined by
3.6 Exception Handling 99
Catch (Gs it
}
should therefore always be the last catch block.
Any statements can be enclosed in a catch block. In this case, an error message is issued
to the standard error output channel std: : cerr and the function main() is exited with an error
status using return:
Std-icerr <<) << std::endl:
return EXIT_FAILURE; // exitmain() with an error status
The constant EXIT_FAILURE defined in <cstdlib> is used as the return value of main() to
indicate an incorrect run of the program (see Section 3.9.3 on page 122).
Provided that an exception object is defined as a parameter of a catch clause, this object can
be accessed to gain further information about the exception. With standard exceptions, a simple
call of what () is possible:
catch (const std::exception& e) {
e.what() ...
}
The return value of what () is an implementation-specific string. As an exception is triggered by
the cail of at () in this example, and it is a standard exception of the std: : out_of_range type,
this second block is found to be the suitable handler of exceptions. For example, implementation-
specific output can look as follows:
standard exception: pos >= length ()
Provided the function is not exited in the catch block, the program continues after the last catch
block. In this example, without the return statements, the following output statement would be
called after an exception.
It should be noted that the exception object in the catch block is declared in the form ‘const
type&’. This is a so-called constant reference, which ensures that no unnecessary copies of the
exception object are made. The necessary language features are explained in detail in Section 4.4
on page 181.
As previously mentioned, all the blocks located between detection and handling are left
bottom-up. This process is called stack unwinding. While doing this, all local objects of these
blocks are destroyed. If these are class objects for which clean-up operations (so-called destruc-
tors) are defined, then these are also called.
The following example clarifies the scenario once more:
// progs/eh2. cpp
char 21 (consteetd:estring s, int. idx)
{
std::string tmp = s;_ //local object that is destroyed
Se // if there is an exception
char c = s.at(idx); // could trigger an exception
100 3: Basic Concepts of C++ Programs
return c;
di
void foo()
z
Enya.
std::string s("hello"); //is destroyed if there is an exception
se ores bs ee // triggers an exception
£2.) // is not called if there is an exception in £1()
;
Cat chm (ce) a4
std::cerr << "Exception, but we will go on" << std::endl;
I
From foo(), £1() is called with the string ‘hello’ and the index 11. In £1(), this triggers an
exception when at () is called. In this case, £1 () is immediately exited, and the local object tmp
is cleaned up. The try block in foo() is also exited immediately. £2() is no longer called.
If
a suitable catch block exists, its statements are executed and it continues after the last
catch
block. If a suitable catch block does not exist, then f00() is exited immediately. This happens
until a suitable catch block is found.
void processException()
t
try
throw; // rethrow the exception again so that it
// can be handled here
}
catch (const std::bad_allock& e) {
// special exception: no more memory
std::cerr << "no more memory" << std::endl;
}
catch (const std::exceptionk& e) {
// other standard exception
std::cerr << "standard exception: " << e.what() << std::endl;
}
catch leh 4
// all other exceptions
Std: cerm << “other exception "<< std::endl;
c
}
int main()
4
try +
// create two strings
std::string firstname("bjarne") ; // may trigger std: :bad_alloc
std::string lastname("stroustrup") ; // may trigger std: :bad_alloc
std::string name;
102
a 3: Basic Concepts of C++ Programs
ee
// manipulate strings
firstname.at(20) = ’B’; //triggers std: :out_of_range
lastname[30] = ’S’; // ERROR: undefined behaviour
// chain strings
name = firstname + "" + lastname; //may trigger std: :bad_alloc
}
catche@. ..) <1
// deal with all exceptions in auxiliary function
processException() ;
return EXIT_FAILURE; // exitmwith
ain()
error status
Inside main(), a catch block is defined for all exceptions, which calls the auxiliary function
processException() for the actual handling of the exception:
try {
}
catch (en) ot
processException() ;
}
Within processException(), throw is called (without parameters) in a try
block. This causes
the exception that is currently being dealt with to be triggered again. In this
way, all blocks are
then exited in sequence until a suitable catch block is found. By means of
the catch blocks
that follow in processException(), the handling is done inside this function.
3.6.7 Summary
e Exception handling is a language feature for the treatment of errors and exceptio
ns that appear
in the normal processing of the program.
e It provides a facility to separate normal data flow from error handling.
e Because no return values have to be used with this mechanism,
the concept is especially
useful for error handling when creating objects.
e Exceptions are objects whose types are defined in corresponding classes.
3.6 Exception Handling 103
e If an exception is triggered, a corresponding object is created and all superior blocks are
dismissed in sequence until the object is intercepted for exception handling. This is called
stack unwinding.
e If an exception is not handled, a program error is triggered, which causes an abnormal termi-
nation using std: :terminate() and std: :abort().
e Several standard exception classes are defined in C++. what () returns an implementation-
specific string for the corresponding exception objects.
e Using throw, we can trigger exceptions that have just been dealt with. In this way, we can
implement general functions for exception handling.
e Exceptions should always be declared in the ‘const type&’ form (i.e. as constant references).
e If strings or vectors are accessed with the index operator, we must ensure that the index is
valid. If we use at () for access purposes, an exception is triggered whenever an index is
invalid.
104 3: Basic Concepts of C++ Programs
3.7.1 Pointers
e Having this situation, the expression *xp stands for x because it yields whatever xp points to.
Using this pointer, x can, for example, receive a new value:
*xp = 7; // that to which xp points (i.e. x) gets the value 7
In this way, the following situation occurs:
Declaration of Pointers
Pointers are declared by placing an asterisk in front of the name. Because C++ is a format-free
language, there are different ways of defining pointers. The following methods are equivalent:
int *xp; // K&R notation
int * xp;
int*xp;
int* xp; // typical C++ notation
The first form, which was introduced by Kernighan and Ritchie, the founders of C, and is nor-
mally used in C.
I prefer the latter notation (as many in the C++ community do), which may confuse one or
two readers at first. It has the advantage that the type (pointer to int) is clearly separated from
the name (xp). However, it also has one severe disadvantage: It cannot be used for the declaration
of more than one pointer because, when declaring
inet pl, pz; // NO, not two pointers, so avoid!
the asterisk only belongs to the first variable. p1 is declared as a pointer to an int, but p2 is
declared as an int, which becomes clearer when using the K&R notation:
ihte*pi 5. p2; // pointer and simple variable
The special constant NULL denotes a pointer that points nowhere. This is in contrast to a non-
initialized pointer, which points to any place, a defined state. NULL can be assigned and queried:
106 Basic Concepts
3: ety ea yfes Programs
of C++eas See
e eae
}
NULL is defined as the value 0. Zero is the only integer value that can be assigned to pointers.
Thus the value 0 can simply be used instead of NULL:
int* xp = 0; // xp is a pointer that points nowhere
}
As 0 corresponds to the value false and all other values correspond to true, we can also test
pointers logically:
Peecxp) ee // does xp point anywhere?
ii
3.7.2 Arrays
An array is a collection of several elements of the same type, arranged in sequence. The num-
ber of elements has to be indicated when an array is created. Element access is done via the
operator [ ]:
int values[10]; // array of ten ints
Arrays can be accessed using pointers. This mirrors the fact that an array variable inside the
program is actually a pointer to the first element (and therefore contains the address of the first
element in the array). By means of the declaration
int values[10]; // array of ten ints
the condition displayed in Figure 3.10 is created.
Pointer Arithmetic
The analogy between arrays and pointers extends further, in that pointers can move around all
elements of an array. It is also possible for pointers to call arithmetic operations:
e If an integer n is added to the pointer, the pointer is increased by n elements. The ++ operator
increases the pointer by one element.
e If we process the sum of a pointer and an integer n, the result points to the nth value after the
pointer.
e If we subtract two pointers, we get the distance between the elements to which they point.
The following loop outputs all elements of an array:
int values[10]; // array of ten ints
At the beginning, p is initialized as a pointer to the first element of values. Provided this pointer
is before (less than) a pointer that points to the position of ten elements after the beginning of
values, the current element is accessed using *p, and the pointer is increased, with ++p, by one
element (see Figure 3.11).
If you process the difference of p and values inside the loop, you get the distance between
the elements from the beginning, which corresponds to the current index:
std::cout << "index: " << p-values
<< " value: ™ << #p << std::end!:
According to these rules, it is totally irrelevant whether we access the elements of an array using
the index operator or pointer notation. Instead of
values [5]
we can access the sixth element of the values array using
* (valuest5)
Does the way pointers iterate over arrays look familiar? The interface corresponds to the
iterator
interface (see Section 3.5.4 on page 74). This is no coincidence. The behavior of
arrays and
pointers was abstracted when designing the STL: Iterators can access containers using
the same
interface with which pointers can access arrays. Iterators can therefore be described
as ‘smart
pointers’ (see Section 9.2.1 on page 536). On the surface, they behave like
pointers, but are
intelligent in converting the operations suitably for the data structure on which
they operate.
3.7 Pointers, Arrays, and C-Strings 109
3.7.3 C-Strings
The standard string types of C++ encapsulate the details of the low-level string processing with
fundamental types, which is adopted from C. In C, strings are managed as arrays of characters.
To signify the end, C-strings use the character ‘\0’ (again, this is nothing else but the value 0).
The length of a C-string is therefore the number of characters until ‘\0’ appears.
String literals have this low-level format. That is, they are arrays of characters in C++, suf-
fixed with ‘\0’. The string constant
"hello"
The type of a string literal is const char*; thus, it is a pointer to non-changeable characters.
However, many (old) C functions do not use const (for C-strings). For this reason, we are
allowed also to use a string literal as type char*. However, we are still not allowed to modify
the characters of a string literal in this case.
If you use strings in C++, you usually do not see the underlying management adopted from C.
The std: : string type hides the fact that strings are arrays of characters. By declaring
std: :string-s = "hello";
one object of the standard std: : string type is initialized with a value of the const char*
type. Instead of this, we can also write the following:
std::string s{*hello");
If we (for whatever reason) use the const char* type, we can access all characters of a C-string
by allowing a pointer to iterate over these characters:
// loop that outputs each character of a C-string s character by character
const char* s = "hello";
p is initially assigned to the C-string s, which means that p points to the first character of the
string. As long as p does not point to the string-ending character “\0’, the statements in the loop
body are executed and the pointer is advanced by one character using ++p.
Without using a type that encapsulates C-strings, the problem of arrays and pointers arises. By
managing strings as an array, they cannot be treated as fundamental types. If a string is assigned
to another string using an assignment operator, the addresses are copied, rather than the elements
(as arrays are actually pointers to the first element, these pointers are assigned to one another).
For example, if two strings are declared as follows:
const char* s = "hello";
const char* t = "Nico";
By assigning
Ss ="; // CAUTION: pointers are assigned
we get:
The pointers (addresses of the C-strings) are therefore assigned rather than the characters. Be-
cause of this, s and t represent the same C-string afterwards.
In order to copy the C-strings properly, the individual characters have to be copied. In addition
to this, C programmers must always ensure that sufficient memory is available for the strings
themselves. For this reason, C programs that operate with strings nearly always turn out badly.
For example:
3.7 Pointers, Arrays, and C-Strings 111
// progs/cstring.cpp
// C header file for I/O
#include <stdio.h>
Wonyel 12}
{
const char* c = "input: "; // string constant
char text [81]; // string variable for 80 characters
char e{e1i; // string variable for the input (up to 80 characters)
/* read string s
* - because of limited memory, no more than 80 characters
7
af(scani("/S0s" +s) t= 1) {
// read error
The problems displayed by this small program should be familiar to all C programmers:
e For an assignment, the strcpy() function has to be used. The programmer must ensure that
the string memory to which a new value is assigned is large enough.
e In order to append one string to another, the strcat () function is useful. Sufficient memory
also has to be provided in this case.
e The strcmp() function has to be used to compare two strings.
e When entering a string, it has to be specified that space is only available for a certain number
of characters.
String processing in C is therefore not only tedious, but also dangerous. Special functions have
to be called for all operations (the most important ones are listed in Table 3.12), and we need to
worry about memory management.
Function Meaning
strlen() returns the number of characters in a string
strcpy () assigns one C-string to another
strncpy() | assigns up to n characters of one string to another
strcat() appends one C-string to the other
strncat() | appends up to n characters of one string to another
strcmp () compares two C-strings
strncmp() | compares up to n characters from two strings
strchr () searches for a certain character in a C-string
All these problems no longer occur in C++. By using the standard string type introduced
in Section 3.4 on page 59, we can use standard operators and need not worry about memory
management. In C++, the above C program would look as follows:
// progs/string2. cpp
int main()
it
const std::string c = "input: "; // string constant
std: string text ; // string variable
Sid. stringas. // string variable for the input
// read string s
ase (CU (gisele seilm SS @))) 4
// read error
3.7 Pointers, Arrays, and C-Strings 113
The handling of dynamic arrays in C has the same complications. C++ has the advantage that the
standard library makes classes available with STL containers that encapsulate these difficulties.
For this reason, you should also use vectors instead of dynamic arrays.
3.7.4 Summary
e Pointers are variables that refer to other variables. Their values are the addresses of these
variables.
e The unary operators * and & are used in the processing of pointers.
e Ifa pointer has the value 0 (NULL), it does not refer to anything. This is different from a
non-initialized pointer, which can point anywhere.
e Arrays are created with brackets. Their index ranges from 0 to size — 1.
e The type and value of an array variable corresponds to a pointer to the first element.
e Pointers can move around arrays using arithmetic operators.
e C-strings are arrays of characters.
e The difficulties associated with C-strings are eliminated by using the C++ type for strings,
std; :string.
114 Se oe es Basic Concepts of C++ Programs
3:ee eee
BA 8 ee hy St he
The new and delete operators are available in C++ for dynamic memory management and the
explicit creation and deletion of objects. With new, memory is requested and a new object is
explicitly created; with delete, this object is removed and any reserved memory is released.
The objects managed with new and delete can belong to a class or can have a fundamental
type. Arrays of objects of any given type can also be created.
Demarcation of C Functions
The new operations replace the memory-management functions familiar from C, malloc() and
free(). This has many advantages:
e As operators, new and delete are part of the C++ language and do not belong to a standard
library.
e For this reason, the types of object to be created can be given directly as operands, and do not
have to be bracketed as a parameter or be provided with sizeof.
e In addition, new, in contrast to malloc(), returns a pointer to the correct type. Thus, its
return value does not have to be explicitly converted into the correct type.
e Provided that new creates objects, these are initialized. When requesting memory for funda-
mental types, the memory of local variables may not be initialized.
e The return value does not necessarily have to be tested for NULL because a standard error
handling is installed that throws exceptions if it is not possible to allocate memory or create
objects.
e Finally, the new operator can itself be implemented for individual types. By doing so, any
kind of optimization is possible for memory management, without changing the interface for
the caller.
3.8 Memory Management Using new and delete 115
}
initPerson(personPointer,"Nicolai","Josuttis") ;
becomes, in C++,
personPointer = new Person("Nicolai","Josuttis") ;
We only indicate in C++ what kind of object should be created and, optionally, submit initializing
arguments.
if (condition-which-may-be-achieved) {
ptr = new Person;
I
Multi-Dimensional Arrays
Multi-dimensional arrays can also be created using new. Again, a pointer to the first object of the
array is returned:
std::string(*twodim) [7] = new std::string[10][7]; //10*/7 strings
By calling new, 70 strings are created and initialized with the default value of the class. The
delete statement then releases these strings.
Note that by calling
new std::string[num] [7]
a pointer of num elements of the type ‘array of seven strings’ is returned:
std::string (*twodim) [7]
118 3: Basic Concepts of C++ Programs
Due to the fact that every further dimension is part of the returned type, all but the first dimension
have to be passed as a constant value:
new std::string[num][7]; //OK
new std::string[10][num] //ERROR
Because of the complicated types of multi-dimensional arrays, programmers usually use a one-
dimensional array and manage the dimension themselves.
The new operator can create arrays with no elements. This avoids special handling for empty
arrays:
int size = 0;
delete [] numbers; // OK
3.8.5 Summary
e The new and delete operators are introduced in C++ for memory management.
e For arrays, new[] and delete[] are provided.
e The memory management of C++ using new and delete should never be confused with the
C functions malloc(), free(), etc.
e Ifno memory can be allocated by new, an exception of the type std: : bad_alloc is usually
thrown. Programs should handle exceptions of this type appropriately.
120 3: Basic Concepts of C++ Programs
}
e On the other hand, with two parameters:
int main(int argc, char* argv[]) {
}
In the second case, argv contains the arguments from the call of the program, passed as an array
of C-strings. The number of elements in this array is located in argc. The array always contains
the program name as the first element; the other elements are program parameters (if any).
A simple example showing the processing of program parameters is shown in the following
program:
// progs/argu.cpp
#include <iostream> // C++ header file for I/O
#include <string> // C++ header file for strings
If the program is simply called using the name argv and without any parameters, it has the
following output:
argv was called without parameters
If the program is called like this:
argv hello "two words" 42
the output is as follows:
argv has 3 parameters:
argv[1]: hello
argv[2]: two words
argv[3]: 42
With the getenv() function defined in <cstdlib> or <stdlib.h>, values can be requested
from environment variables:
#include <cstdlib>
#include <string>
std::string path;
const char* path_cstr = std::getenv("PATH") ;
if (path_cstr == NULL) {
t
else {
path = path_cstr;
t
The value of the environment variable is either returned as a C-string or as NULL. Note that NULL
cannot be assigned to C++ strings. For this reason, the return value has to be checked before
being used as a C++ string.
22 3: Basic Concepts of C++ Programs
Function Meaning
exit () interrupts a program in an ‘orderly’ manner
atexit() installs a function that is called at the end of the program
abort() interrupts a program in a ‘disorderly’ manner
We can terminate a C++ program at run time anywhere using exit (). This is not an ordered
ending to the program, because the program is exited while still running. Certain clean-up work
is still carried out (for example, output buffers are usually flushed), but temporary files are not
removed.
More precisely, all global objects are cleaned up, but local objects are not. This can lead to
undesirable side effects, which is why calling exit () should be avoided whenever possible.
exit () receives an integer value as a parameter, which is passed to the caller of the program.
This can be used to indicate whether the program ran successfully (the so-called exit code). These
values correspond to the return value of main(). If 0 or the EXIT_SUCCESS constant is passed, it
is a ‘successful’ program end. The EXIT_FAILURE constant indicates an ‘unsuccessful’ program
end:
#include <cstdlib>
if (fatal-problem) {
std: :exit (EXIT_FAILURE) ; // abort with partial clean-up
}
The constants EXIT_SUCCESS and EXIT_FAILURE can also be used as return values of main().
Using EXIT_FAILURE is particularly helpful, as this clearly indicates that something has failed:
int main()
<c
if (error) {
return EXIT_FAILURE; // end program with an error status
3.9 Communication with the Outside World i123)
A function can be defined using atexit(), which is automatically called directly before the
program terminates via exit(). This is typically used to end a program in case of an error.
In the installed function, opened files can be closed, buffers flushed or connections to other
processes closed in an orderly manner. These functions are not only called when a program is
terminated by exit (), but also with the regular ending of main().
abort () is used to abort a program immediately, without any additional cleaning up, in case
of a fatal error. The precise reaction is system specific, and a core dump is typically created.
abort () is called without parameters:
#include <cstdlib>
if (every-further-operation-is-no-longer-useful) {
std: :abort()< // abort with core dump
}
int status;
if (is-unix-system) {
status = std::system("1s -1"); // list files under UNIX
}
else {
status = std::system("dir") ; // list files under Windows
}
}
The return value of system() is the exit code of the program, or an error code if the program is
unable to be called at all.
3.9.5 Summary
e A C++ program can be called with arguments that can be evaluated as parameters of main().
e Environment variables can be accessed using getenv().
124 3: Basic Concepts of C++ Programs
e Programs can be terminated using exit () and abort (). These functions should normally
be avoided.
e Other programs can be called using system().
e Ifa C-string has the value NULL, then you cannot simply assign it to a std::string. The
value has to be treated as a special value.
Class Programming
This chapter introduces two basic concepts of object-oriented programming: programming with
classes and data encapsulation.
The various language features and programming techniques required in C++ for the use of
classes are introduced and explained with the help of examples.
As an introductory example, a class Fraction is used. As the name implies, this class
describes objects that represent fractions. These rather simple objects are used deliberately so
that the example itself does not pose a problem when different languages features are introduced.
126 4: Class Programming
For our first example, the requirements on the Fraction class will be kept to a minimum. We
are only interested in clarifying the principles. For this reason, we will require only one single
operation:
e The output of a fraction.
In the following sections, further operations will be added that will be used, for example, to
multiply or input fractions.
Nevertheless, we should remember that, in order to execute an operation with fractions, frac-
tion objects must exist in the first place. In addition, a fraction should be able to be deleted if it
is no longer needed. Therefore, two additional operations are required:
e The creation (and initialization) of a fraction.
e The destruction of a fraction.
The resulting life-cycle of a fraction is displayed in Figure 4.1.
4.1 The First Class: Fraction 127
Fraction
Design of Fractions
Trying to find the attributes and internal structure of an object involves transform this kind of
abstract construct directly or indirectly to already available types. The information content and
state of the object must be described by several individual members that have types already
available.
Initial questions that may be asked are as follows:
e What kind of data describes the fundamental state of the object?
e What does the object represent?
e What are the ‘parts’ of the object?
These questions often lead to the first members of an object. During the implementation of a
class, supplementary members may be added that represent an internal object state and therefore
make the implementation process easier, or shorten the running time.
It is fairly easy to say what a fraction represents or what it is made up of: A fraction consists
of a numerator and a denominator. We can use fundamental types such as int for these members.
As a result, the two essential members of a fraction are as follows:
e Anint numer for the numerator.
e Anint denom for the denominator.
Further auxiliary members may well be required. For example, a member can determine inter-
nally whether the numerator and denominator can be reduced. Thus the members essential for
information content, the numerator and denominator, do not need to remain the only members.
Ultimately, the diagram shown in Figure 4.2 is the result: a fraction consists of a numerator
and a denominator, and can be created, output, and destroyed.
Terminology
In object-oriented terminology, the numerator and denominator are the data or attributes from
which an instance of the Fraction class is created. The creation, output, and destruction opera-
tions are the methods that are defined for the class.
128 4: Class Programming
Fraction
int numer
int denom
In C++ terminology, this means that a Fraction class is needed that consists of the members
numerator and denominator, as well as functions for creating, outputting, and destroying in-
stances. The methods, i.e. the operations, defined in a class are called member functions in C++,
which are special kinds of member. Those members that are not member functions are called
data members in C++. Thus, in C++, we generally speak of members of a class, which might
be member functions or data members. The members of the Fraction class could therefore be
categorized into the numerator and denominator data members, and the member functions for
creation, output, and destruction.
Encapsulation
Fractions have a fundamental property: Their denominator cannot be zero because division by
zero is not allowed (at least without introducing semantics for infinite numbers). This constraint
should, of course, also be valid for the Fraction class.
If unlimited access is available to all members of the Fraction class for all users, the de-
nominator could inadvertently be set to zero. For this reason, it is useful to prevent direct access
to the denominator when using a fraction. Provided that access is only possible using functions,
attempting to assign zero to the denominator could result in an error message.
In general, it is recommended that access to objects only be permitted using operations that
were specifically designed for this purpose. By implementing these carefully, errors can be han-
dled and inconsistencies prevented. For example, if, in an auxiliary member, we store internally
whether a fraction can be reduced, an appropriate adjustment can be made, once the contents
have been changed externally by assigning a new numerator.
In the case of fractions, it is therefore reasonable to allow access only to the numerator and
denominator members via functions belonging to the Fraction class. This gives an additional
advantage: the internal behavior of a fraction can be modified without changing the external
interface.
4.1 The First Class: Fraction 129
A class must be declared before it can be used in C++. This is done by declaring a class structure
in which the principal properties of the class are listed. For the Fraction class, these properties
contain the data members for the numerator and denominator, as well as the member functions
that make up the interface.
To compile all modules in which the Fraction class is used, the declaration is placed in a
header file. The file name should usually be indicative of the class, so that frac. hpp would be
an appropriate filename.
The first version of the header file frac. hpp is as follows:
// classes/fraci1.hpp
#ifndef FRACTION_HPP
#define FRACTION_HPP
// 38 8 8 BEGIN namespace CPPBook 3B OS382S2S i 2 isHS21S2k2k2s fsi ois018ofof2k2 2kofok okokokok okok
namespace CPPBook {
/* Fraction class
=,
class Fraction {
/* public interface
*/
public:
// default constructor
Fraction();
// output
void print();
130 4: Class Programming
} // wea END namespace CPPBook Oise fs24s24s2s2 is2k2s2k24s2k2 is2 oeoisoieoieok2kofof2 2 2kok2kokokok
#endif /* FRACTION_HPP */
Preprocessor Statements
The complete header file is enclosed by preprocessor statements. These prevent the declaration
being executed multiple times when the file is included more than once:
#ifndef FRACTION_HPP // is only fulfilled with the first #include because
#define FRACTION_HPP // FRACTION_HPP gets defined then
#endif
If the FRACTION_HPP constant is not already defined, the #ifndef (‘if not defined’) statement
processes the following lines until the corresponding #endif is reached. Because the constant
is defined on the first pass, subsequent attempts to define it again (due to other direct or indirect
#includes) will be ignored.
As header files may be included by other source files in various ways, declarations in header
files should always be enclosed by these kinds of statement (see Section 3.3.7 on page 52).
Classes are declared using a class structure. All members of the class are declared here (i.e. both
attributes and operations).
The declaration starts with the class keyword. This is followed by the name of the class and
then the class body, in which the class members are declared, enclosed by braces and separated
by semicolons:
class Fraction {
};
If a library consisting of several classes is defined, every class name represents a global symbol
that can be used anywhere. This may lead to name conflicts when various libraries are used. For
this reason, the Fraction class is declared within a namespace:
namespace CPPBook { // start ofnamespace CPPBook
class Fraction {
+3
The CPPBook symbol is therefore the only symbol found in the global scope. Strictly speak-
ing, we define ‘Fraction in CPPBook’. In C++, the notation used to access this class is
CPPBook: :Fraction.
All classes and other symbols should always be assigned to a namespace. This both avoids
conflicts and clarifies what members of a program logically belong together. This kind of group-
ing is described as a package in object-oriented modeling.
Further details on namespaces can be found in Section 3.3.8 on page 54 and Section 4.3.5 on
page 175.
Access Keywords
The class structure contains access keywords that group the individual members together. This
enables us to determine which members of the class are internal and which can also be accessed
externally.
The members numer and denom are declared as private:
class Fraction {
private:
int numer;
int denom;
};
By using the private keyword, all the following members are declared as private. Though this
is the default setting in classes, it should be stated explicitly in order to make the code more
readable.
The fact that the members are private means that access is only granted within the implemen-
tation of the class. Any code using a fraction has no direct access to them. Only indirect access
can be granted by making appropriate member functions available.
The user of the Fraction class only has access to the public members. These are declared
with the public keyword. Obvious candidates for public members are the functions that form
the interface of the objects of this class with the outside:
class Fraction {
pubic:
void print();
}3
It is not compulsory for all member functions to be public and all data members to be private.
Larger classes often have auxiliary internal functions, declared as private member functions.
Data members can also be provided for public access. However, this is generally not recom-
mended because you lose control when somebody accesses data members directly. Providing
access to data members only through member functions ensures that you have a clear separation
of internal state and external interface and that you can always intervene when data members are
set or queried.
2 4: Class Programming
Access declarations can occur in a class declaration any number of times, and access to the
members can be changed repeatedly:
class Fraction {
private:
public:
private:
ti
From the point of view of a C programmer, a class can be regarded as a C structure endowed
with additional properties (e.g. access control and member functions). In fact, in C++, structures
are Classes. You can do everything with structures that you can with classes. Thus, instead of the
class keyword, the struct keyword can also be used to define a class. The only difference is
that with the struct keyword, all members are public by default. Therefore, from a C++ point
of view, each C structure is a class with only public members.
Nevertheless, I would recommend using class and struct in a way that helps clarify the
semantics. That is, class should be used when we define objects that have data members and
member functions with different access levels, while struct should be used if we only need a
data structure (a composition of values) with public access to all data. A type that can be defined
without certain C++ features is also called a POD, which stands for ‘plain old data (type)’. Thus,
every ordinary C struct is a POD and you should use struct only for PODs.
Unfortunately, the semantic difference between class and struct is not made in much of
the literature. Thus you may often find examples where the struct keyword is used just to avoid
the need to put a public declaration in front of the first member.
One member declared in the Fraction class structure is the print () function:
class Fraction {
tee
This is used to output fractions (that is, write the value of a fraction to the standard output
channel). As no value is returned, it has the void return type.
4.1 The First Class: Fraction 133)
Note that, although we have a fraction to write, it is not passed as a parameter. For member
functions, there is always an implicit parameter, which is the object for which the function is
called:
CPPBook::Fraction x;
4.1.5 Constructors
The first three functions declared in the Fraction class are special functions. They determine
how a fraction is created (‘constructed’). The constructor bears the name of the class as a function
name:
class Fraction {
// default constructor
Fraction() ;
$3
A constructor is always called if a concrete object (instance) of the corresponding class is cre-
ated. For example, this happens when a variable of this type gets defined. Once the memory
required for the object has been allocated, the statements in the constructor are executed. They
are typically used to initialize the created object by assigning sensible initial values to the data
members of the object.
Having three constructors means that a fraction can be created in three different ways:
1. A fraction can be created without an argument.
This constructor is called if an object of the Fraction class is defined without any other
parameters:
GPPBooks: Fraction x; // initializing with the default constructor
A constructor without parameters is called a default constructor.
134 4: Class Programming
2. A fraction can be created with an integer as an argument. As we will see in the implemen-
tation of the constructors, this parameter gets interpreted as whole number, with which the
fraction is initialized.
This kind of constructor is called when, for example, a fraction is defined with an initial
integral value:
CPPBook::Fraction y = 7; // initializing with the int constructor
This manner of initialization was adopted from C.
However, a new notation was introduced in C++ for initializing an object, which can be
used instead:
CPPBook::Fraction y(7); // initialization with int constructor
3. A fraction can be created with two integer arguments. These parameters are used to initialize
the numerator and denominator.
The new notation for initializing an object can be used to call this kind of constructor, as
it enables several arguments to be passed:
CPPBook: :Fraction w(7,3); // initialization with the int/int constructor
Constructors have an unusual feature: they have no return type (not even void). They are there-
fore not functions or procedures in the ordinary sense.
If no constructors are defined for a class, concrete objects (instances) of the class can still be
created. However, these objects are not initialized. For each class, there is a predefined default
constructor that does nothing.
You should always try to avoid creating objects with undefined states. If it is required that an
object has the state ‘not initialized’, a Boolean member should be introduced that manages this
status. The constructor can then initialize the object as ‘not initialized’ and, when operations are
called, a check can be performed to see whether the data members have been initialized in the
meantime.
As soon as constructors are defined, objects can only be created using them !. If a constructor
but no default constructor is defined, the creation of an object without parameters is not possible.
In this way, the passing of values for initialization can be forced.
' It is still possible to create a copy, which is discussed in Section 4.3.7 on page 179.
4.1 The First Class: Fraction 15
The following example shows the overloading of the global function square (). One version
calculates the square of an integer, the other the square of a floating-point value:
// declaration
int square(int) ; // square of an int
double square(double) ; // square of adouble
void foo()
x
// function call
square (713) ; // computes the square of an int
square (4.378) ; // computes the square of adouble
yi
In addition, functions can be overloaded for user-defined types such as Fraction:
// declaration
int square(int) ; // square of an int
double square (double) ; // square of adouble
CPPBook: :Fraction square(CPPBook::Fraction); // square ofa Fraction
void foo()
£
CPPBook: :Fraction f;
// function call
square (f) ; // computes the square of aFraction
}
When overloading functions, we need to ensure that each of the overloaded functions do the
same thing. As every function is implemented separately, this is up to the programmer.
Parameter Prototypes
For a distinction to be made between overloaded functions, they must be declared with param-
eter prototypes. This means that the parameter type must be given with the declaration, and is
included in the list of parameters when a function is defined:
/| declaration
int square(int, int);
// definition
int square(int a, int b)
{
return a * b;
136 4: Class Programming
Parameter prototypes can also be defined in ANSI-C. However, in C, the parameters did not nec-
essarily have to be included on declaration. There is therefore a fundamental difference between
C and C++. In C, the declaration
void foo();
defines a function with as many parameters as desired. In C++, this means that the function does
not have any parameters. The ANSI-C notation of declaring a function without parameters
void foo(void) ;
// classes/frac1.cpp
// include header file with the class declaration
#include "frac.hpp"
namespace CPPBook {
/* default constructor
a
4.1 The First Class: Fraction 137
Fraction: :Fraction()
numer(0), denom(1) // initialize fraction with 0
// no further statements
// no further statements
/*print
/
void Fraction: :print()
This inclusion is necessary because it lets the compiler know what members the Fraction class
has. Otherwise the compiler would have no chance to detect invalid member access or function
calls at compile time.
138 4: Class Programming
Then various other header files are included that declare the types and functions being used
by the class. In this case, there are two files:
#include <iostream>
#include <cstdlib>
The first statement includes all declarations of C++ for input and output (see page 27 and
page 195). The second statement includes a header file in which various standard functions
are declared, which are also available in C (see Section 3.9 on page 120). In general, all standard
functions from C are also available for use in C++. The corresponding header files have a c
prefix instead of the .h extension. With this modification, the symbols declared in the header file
belong to the namespace std instead of the global namespace.
In this program, <cstdlib> is included so that we can make use of std: :exit() function
and the EXIT_FAILURE constant. As explained in Section 3.9.3 on page 122, std: :exit() can
be used to terminate a program in the event of an error.
The include statements have two different forms:
#include "frac.hpp"
#include <iostream>
The second form, with the angled brackets, is intended for external files. The associated files are
searched for in the directories seen by the compiler as system directories”. The files indicated
between double quotes are first searched for in the local directory. If they are not found, the
system directories are also searched (see page 51).
The expression
Fraction::
prefixes all function names. The :: operator is the scope operator, which assigns a symbol
according to scope of the class preceding it. It is the operator with the highest precedence.
In this case, it is made clear that a member function of the Fraction class is involved, which,
in turn, implies that there exists a Fraction object, for which the function will be called and its
members accessed.
The constructors are the first functions that are implemented. As mentioned already, their
name
is the same as the class name, i.e. Fraction: : Fraction.
2
“ In Unix systems, these directories
. . .
can usually be defined with the -I option.
. . . .
Microsoft Visual C++ also
provides a facility for indicating ‘additional include directories’.
4.1 The First Class: Fraction 139
The default constructor initializes the fraction with 0 (more precisely, with °):
Fraction: :Fraction()
: numer(0), denom(1) // initialize fraction with 0
// no further statements
}
This constructor is called every time a fraction is created without arguments. This ensures that a
fraction created without arguments for initialization does not have an undefined value.
A special feature of the constructors becomes apparent at this point. As they are used in the
initialization of the objects, they are able to determine the initial values of the members before
the actual statements of the function body. The initial values can be set for every attribute in a
list, separated from the constructor name by a colon. This is known as an initializer list. It has
the same effect as if the fractions had been declared with initial values for each member:
int numer(0);
int denom(1);
}
This has the same effect but is not quite the same. It is similar to the difference between
int. x = 0% // create x and immediately initialize it with 0
or
int = CO); // create x and immediately initialize it with 0
and
inks ee // create x
x = 0; // assign 0 in a separate step
These differences are not important here; however, for more complicated members, the difference
may matter. Thus you should familiarize yourself with the syntax of the direct initialization used
with the initializer list.
The second constructor is called when an integer is passed. This parameter is interpreted as
whole number, which is used to initialize the fraction. Thus the numerator is initialized with the
parameter passed and the denominator with 1:
Fraction: :Fraction(int n)
: numer(n), denom(1) // initialize fraction with n.
// no further statements
140 4: Class Programming
The third constructor contains two parameters, which are used to initialize the numerator and
denominator. The constructor is always called if two integers are passed when a fraction is
created. However, if the denominator is passed as 0, the program will exit and display an error
message:
Fraction: :Fraction(int n, int d)
: numer(n), denom(d) // initialize numerator and denominator as passed
It is very radical that, in this example, the program exits due to a faulty initialization. It would
have been better to report the error and then react accordingly. The only problem is that construc-
tors are not ordinary functions that can be explicitly called and return a value: they are called
implicitly when an object of the class gets created and cannot return anything to the caller.
The program therefore exits for lack of any better alternatives. To prevent a program from
exiting in this way, we must ensure that the denominator is not passed as 0 in all programs in
which this class is used.
You may consider alternatives to exiting. The fraction could, perhaps, also receive a default
value. However, this is far from ideal, as it ‘hides’ an obvious error (i.e. the numerator cannot
be 0), so that it might (by accident) simply be ignored. This goes against the guideline that you
should always notify an error (or at least have a chance to notify it).
You could also introduce additional auxiliary members that maintain the state of whether or
not the fraction has been initialized correctly. However, these members would then have to be
evaluated and taken into account with each operation. This has the effect of simply postponing
the problem. We still need to know what to do if someone attempts to use a fraction that was
initialized with zero as the denominator.
The best way to deal with these kinds of errors is to use the concept of exception handling,
introduced in Section 3.6 on page 93. This would enable errors to be handled during a declara-
tion, which would not necessarily lead to a program exit. Instead, the program could intercept the
error and treat it accordingly. We will modify the Fraction class to incorporate this mechanism
later (See Section 4.7 on page 234).
4.1 The First Class: Fraction 141
After the constructors follows the definition of the print () member function, which outputs
a fraction. In this case, the function simply writes the numerator and the denominator to the
standard output channel, std: : cout, in the format ‘numerator/denominator’:
void Fraction: :print()
rt
std::cout << numer << ’/’ << denom << std::endl;
e.
As this is a member function, it must always be called for a particular object. Therefore, the data
members numer and denom can freely be used in the function’s implementation. They belong to
the object the member function was called for. This means that there is a new type of scope for
member functions, which is taken into account when assigning symbols: the scope of the class
to which the function belongs. The declaration of a symbol used in a member function is first
searched for in the local scope of the function, then in the class declaration, and finally in the
global (or namespace) scope.
Using w.print(), the print() function is called for the object w. In this case, it follows
that the numerator of object w is output (i.e. w.numer), along with the character ‘/’ and w’s
denominator. Likewise, x. print () would output x. numer and x.denom.
Notice that the numer member cannot be accessed in the application program directly, as it is
declared as private within the Fraction class. Only the member functions of a class have access
to the private members of this class.
ftest or
ftest.exe
ftest.o or
£test.cpp ftest.obj
// classes/ftest1.cpp
// include header files for the classes that are being used
#include "fraci.hpp"
int main()
if
CPPBook: :Fraction x; // initialisation using the default constructor
CPPBook: :Fraction w(7,3); // initialisation using the int/int constructor
// output fraction w
w.print();
// output x and w
Roprant() ;
w.print();
4.1 The First Class: Fraction 143
Declarations
e Asx is defined without a value for initialization, the default constructor will then be called:
Fraction: :Fraction()
: numer(0), denom(1) // initialize fraction with 0
// no further statements
}
By using the initializer list, the fraction x is initialized with the value 0 (i.e. 2):
144 4: Class Programming
e Because w is initialized with two arguments, the constructor for two integer parameters is
called:
Fraction: :Fraction(int n, int d)
: numer(n), denom(d) // initialize numerator and denominator as passed
i
The constructor initializes the numerator and the denominator with the parameters passed,
creating the fraction ¢:
Destruction of Objects
The object created is automatically destroyed if the program leaves the scope of the surrounding
block. According to the automatic memory allocation for the object during its creation,
this
memory is automatically deallocated when the object gets destroyed. For local objects,
this
happens when the program leaves the block in which they were defined:
void foo()
{
CPPBook: :Fraction x; // creation and initialization of x
CPPBook: :Fraction w(7,3); // creation and initialization of w
It is possible to define functions that are called immediately before the memory of the object is
released. These counterparts to the constructors are called destructors. They can be used, for
example, to output the destruction or to decrement a counter for the number of existing objects.
Explicitly releasing allocated memory belonging to an object is also typical. As destructors are
not needed by the Fraction class, they will be introduced later (see Section 6.1 on page 352).
Static or global objects exist once for the whole lifetime of a program. You can consider them
as objects that are created at the beginning of the program and destroyed at the end. This has an
important consequence: the main() function is not the first function of a program to be called.
All constructors for static objects are called first, which, in turn, can call any auxiliary functions.
In the same way, once main() has been exited or exit () is called, destructors for static and
global objects can also be called.
For example, for a global fraction twoThirds, the constructor of the Fraction class is called
before main() is called:
/* global fraction
* - creating and initializing (via constructor) at program start
* - qutomatic destruction (via destructor) at program end
#/,
Fraction twoThirds (2,3);
int main()
{
t
As constructors can call any auxiliary functions, much can happen with complex classes before
main() is called. One example of this would be a class for objects that represent opened files. If
a global object of this class is declared, a corresponding file is opened by calling the constructor
before calling main(). However, as this may cause problems (for example, the general settings
for error handling may not yet have been set), global and static objects in general should be
avoided.
Arrays of Objects
Constructors are called for any created object of a class. If an array of ten fractions is declared,
the default constructor is called ten times:
CPPBook: :Fraction values[10] ; // array often fractions
You can also define values for the initialization of the array elements, as the following example
shows:
146 4: Class Programming
The
w. print()
statement calls the print() member function for w. The member function then outputs the
fraction. The dot operator is, in general, the operator that accesses members for an object. If the
member is a member function, this member function is called for it.
A pointer (see Section 3.7.1 on page 104) to a fraction can also be defined in C++. In this
case, a member, or call to a member function, can be accessed using the -> operator:
CPPBook::Fraction x; // fraction
CPPBook::Fraction* xp; _ // pointer to fraction
xp = &x; // xp points to x
Assignments
The statement
6 Aye
The expression
CPPBook: :Fraction(1000)
creates a temporary object, in which the corresponding one-parameter constructor is called. This
is essentially an explicit type conversion of the integer constant 1000 to a Fraction type.
The statement
w = CPPBook: :Fraction(1000) ;
therefore creates a temporary object of the Fraction class, initialized with +%2° using the ap-
propriate constructor, assigns the temporary object to the w object, and destroys the temporary
object then.
In general, to create temporary objects of a class, corresponding constructors have to be
defined. In this case, a temporary fraction can be created with none, one or two arguments:
CPPBook: :Fraction() // creating temporary fraction with the value 0/1
CPPBook: :Fraction(42) // creating temporary fraction with the value 42/1
CPPBook::Fraction(16,100) // creating temporary fraction with the value 16/100
When modeling classes, the UML defines a de facto standard for graphical representations. Fig-
ure 4.4 displays the UML notation of the Fraction class.
In UML notation, classes are represented by rectangles. These contain the class name, at-
tributes and operations, separated by horizontal lines. Leading minus signs in attributes and op-
erations indicate that these are private. Leading plus signs indicate public access. If an operation
has a return type, it is given after the operation, separated by a colon.
Depending on the status of the modeling, details can be omitted. For example, you could
skip the package name (the namespace), the leading sign for visibility, the types of the attribute
or the parameters of the operations. You could also omit attributes and operations as a whole.
Therefore, the shortest form of a UML notation for the Fraction class is a rectangle, containing
only the word Fraction.
4.1.11 Summary
e Classes are declared by a class structure with the class keyword.
e As with all other global symbols, classes should be declared in a namespace.
148 4: Class Programming
CPPBook:
: Fraction
- numer: int
- denom: int
+ Fraction ()
+ Fraction (n:int)
+ Fraction (n:int,d:int)
cr jerasbane (())
The members of a class have access restrictions, denoted by the public and private key-
words. The default access is private.
Classes can have data members and member functions. Member functions typically form the
public interface for the data members, which define the state of the object. However, private
auxiliary functions may also exist. Member functions have access to all members of their
class.
Constructors are special member functions, called when objects of a class are created,
in
order to initialize these objects. They adopt the name of the class as their function name,
and
have neither a return value nor a return type.
For every class, there is a predefined default assignment operator, which assigns member-
wise.
All functions must be declared with parameter prototypes.
Functions can be overloaded. This means that the same function name may be
used for
different functions, provided they have differing parameter counts or types.
A difference in
return types only is not allowed.
The scope operator ‘: :’ assigns the scope of a certain class to a symbol. This
operator has
the highest precedence.
A structure is the same as a class in C++. The only difference (besides using
the struct
keyword instead of class) is that the default access for a struct is public.
However, you
should only use struct if you define a plain data structure (a composition
of public values).
A type that can be defined without certain C++ features is called a POD
(“plain old data
(type)”).
To model classes, the UML notation is usually used.
4.2 Operators for Classes 149
Peace
nce c) 7
c =a* b;
}
A new version of the fraction class is introduced in this section that enables operators to be used
when working with fractions.
In addition, the this keyword is introduced. In a member function, this can be used to refer
to the object for which the member function was called.
With the inclusion of these operators, the class structure of the Fraction class now looks as
follows:
// classes/frac2.hpp
#ifndef FRACTION_HPP
#define FRACTION_HPP
namespace CPPBook {
/* fraction class
ah
class Fraction {
/* private: no access from outside
is
private:
int numer;
int denom;
150 4: Class Programming
/* public interface
ah
public:
// default constructor
Fraction();
// output
void print();
#endif /! FRACTION_HPP
The declarations for the operators have been added. These are declared using the operator
keyword.
Note that although we added binary operators (operators that have two operands), they are
declared with one parameter only. This is because, for operator functions of a class, as for all
member functions, one object is passed implicitly when the operation is called. This is always
the first operand, or in the case of a unary operator, the only operand. The parameter passed
(explicitly) is therefore the second operand (if any).
It should be understood that in the declaration
class Fraction {
the * operator is declared so that the first operand is a fraction (as the declaration takes place in
the class Fraction), and that the second operand is also a fraction (as Fraction is used as
parameter type). The Fraction before ‘operator *’ is the return type and indicates that a new
fraction is returned as result.
The fact that only the second operand is explicitly passed as a parameter is the consequence
of adopting the object-oriented approach, which says that every operation is merely a message to
a recipient, which may have parameters. In the object-oriented world, the multiplication of two
fractions is not globally understood; instead, the message ‘compute the product with the passed
fraction’ is sent to an existing fraction. In C++, the message is the function, and the recipient of
the message is the object for which the function is called. In other words,
a *b
is interpreted as
a.operator*(b)
We will see that, in fact, you can also use the second form to call the operator function.
Accordingly, the declaration
class Fraction {
z;
means that the < operator is declared so that both the first and second operands are fractions (as it
is declared within the Fraction class and has Fraction as a parameter type). A Boolean value
is returned.
As C++ is a format-free language, blank spaces can be omitted in the declarations of operator
functions:
class Fraction {
ae
Overloading Operators
Operators can also be overloaded. For example, the multiplication of a fraction can be declared
with different types for the second operand:
class Fraction {
35
However, this is only possible if at least one operand is not a fundamental type. Operations where
only fundamental types (char, int, float, etc.) are involved are predefined by C++ (as in C)
and cannot be extended in this way.
// OB oRoR BEGIN namespace CPPBook BE2S 8 3 282482 282g3 2 fe2 2kfe2 okfea okfefe ok9Koe2kokokokokok
namespace CPPBook {
/* default constructor
sa
Fraction: :Fraction()
: numer(0), denom(1) // initialize fraction with 0
// no further instructions
// no further instructions
4.2 Operators for Classes 153
denom i} Q
/* print
rf
void Fraction: :print()
{
std::cout << numer << ’/’ << denom << std::endl;
/* new: operator *
isi
Fraction Fraction::operator * (Fraction f)
{
/* simply multiply numerator and denominator
* - this is quicker
ff:
return Fraction(numer * f.numer, denom * f.denom) ;
154 4: Class Programming
/* new: operator*=
oi
Fraction Fraction::operator *= (Fraction f)
{
WES Ey Tee beaten iy
*xthis = *this * f;
// return object for which the operation was called (first operand)
return *this;
As mentioned before, for all three operators, two fractions are involved because binary operators
are defined. By qualifying them for the Fraction class using the scope operator (Fraction: :),
it is determined that the operator function is called for a fraction as the first operand.
The
Fraction parameter type determines the type of the second operand.
Thus, the first operator is available automatically because the function is called for it. This
means that its members can be addressed directly. Therefore, using numer
and denon, you can
access data members of the first operand. Every other numerator and denominator
must be ac-
cessed by qualifying the name of the appropriate object. Thus the numerator and the denominat
or
of the second operand are accessed using the name of the parameter, i.e. f.numer
and f .denon.
Note that this means that private access is interpreted type-wise, and not object-wis
e, in
C++. In a member function, you not only have access to the object for which
the function was
called, but also for any other object that has the same type. Other object-oriented
languages such
as Smalltalk handle this differently, and allow access to objects other
than the one for which the
function was called via the public interface only.
Detailed discussions of the implementation of each of the three operator functions
follow.
4.2 Operators for Classes Ease)
The * operator multiplies a fraction object by another fraction object. The first operand is the
object for which the operation is called, and the second operand is passed as a parameter. The
resulting fraction is returned. Its value has the product of both numerators and denominators as
numerator and denominator, respectively. Reduction is not performed.
As a first attempt, the function might be implemented as follows:
Fraction Fraction: :operator * (Fraction f)
{
Fraction result; // create fraction result
// return result
return result;
}
This implementation will work fine. However, it has a running-time disadvantage: an additional
local result fraction object is used unnecessarily. This object will have to be created and
then destroyed. In addition, the default constructor for result is called when it gets defined.
This constructor initializes the object ‘incorrectly’ with 0/1. The ‘correct’ value is assigned
afterwards.
In the actual implementation,
Fraction Fraction::operator * (Fraction f)
|
return Fraction(numer * f.numer, denom * f.denom);
the ‘incorrect’ initialization of the local object is omitted. A new temporary fraction is created
instead, which is also used directly as the return value.
This avoidance of the unnecessary creation of objects may save a considerable amount of
time. It would not be noticeable if you were to just multiply two fractions together, but would
become apparent if you were to perform the operation thousands of times. Note that you are
providing the implementation of a class that may well be used a lot in commercial code. Thus
you should definitely take time to consider code optimizations of the kind highlighted above.
However, always remember that performance is only one aspect of quality of code. Readability
is another.
The < comparison operator determines whether the fraction for which the operation was called
(i.e. the first operand) is smaller than the passed parameter (i.e. the second operand). At this point,
156 4: Class Programming
we face a minor pitfall: To be able to do the computation as pure integer computation we may
reformulate the computation by multiplying the inequalities with both denominators. However,
for inequalities, comparison signs must be reversed when multiplied by a negative value. The
following rule applies:
ae cen ioe if b and d are of the same sign
bd, axd>cxb if b and d have different signs
Therefore, the implementation of the operation should differentiate between the two cases:
bool Fraction::operator < (Fraction f)
{
if (denom * b.denom > 0) {
// multiplication by the denominators is OK
return numer * f.denom < f.numer * denom;
}
else {
// multiplication by the denominators reverses the comparison
return numer * f.denom > f.numer * denom;
}
a;
However, this check can be avoided altogether by ensuring that the denominator of a fraction is
never negative. This is possible because all operations for fractions, including the initialization
of new fractions, are under our control.
The only place where a fraction with a negative denominator can exist (namely in the int/int
constructor) is therefore rewritten, so that the negative sign is shifted from denominator to nu-
merator:
Fraction: :Fraction(int n, int d)
t
sige (Gl < ©) af
numer = -n;
denom = -d;
A;
else {
numer = n;
denom = d;
}
M
The reversal of the comparison if the denominator is negative can therefore be omitted, and the
comparison operator can then be implemented as follows:
4.2 Operators for Classes ET
C++ has numerous assignment operators. In addition to the ordinary assignment with the =
operator, there are value-changing assignments of the form +=, *=, and so on (see Section 3.2.3
on page 37). These value-changing assignments can also be defined for classes.
There is no default behavior that when an operator * is defined, the corresponding operator *=
is also defined. In contrast, the programmer of a class must alsways define all operators (except
the assignment operator) himself, and it is also up to the programmer to ensure that the usual
rules of operators for fundamental types apply:
Xx Op= y corresponds to x = xopy
In other words, there is no guarantee that this analogy is valid for non-fundamental types. How-
ever, if this does not apply to a class, it definitely contradicts the purpose of readable code and
intuitive interfaces.
On the basis of these initial thoughts, we can now look at the implementation of the multi-
plicative assignment. It is implemented as though it contains a multiplication with an assignment:
// return object for which the operation was called (first operand)
return *this;
-
Here, the this keyword is used. In all member functions, *this denotes the object for which
the function or operation was called.
In every member function, this is automatically defined as a pointer to the object for which
the function is called (pointers are introduced in Section 3.7.1 on page 104). The expression
*this
dereferences this pointer and returns the object to which it was pointing. The whole expression
therefore represents the object for which the member function was called. In an operator function,
*this thus describes the first operand. So, if the operation
x #5y
158 4: Class Programming
is called, then *this denotes the first operand x, while the second operand y is submitted as the
parameter f.
*this is also used in the return statement at the end of the operation. This means that the *=
operation returns the object to which the new value was assigned as a return value. A call such
as
xX *= y
is not an entirely closed statement, but returns something, and can therefore be part of a larger
expression. For example, we can formulate the condition
if) (€x (4222) 5< 10) // tests ifx is less than 10 after duplication
The fact that the object for which the statement was called is then returned is a property that
applies to all fundamental types in C++, as well as in C. This is used mainly by the assignment
operator, making expressions like
Sec ye= 10% // assigns the value 10 to y, and then y (i.e. 10) tox
or
// assigns return value from fopen() to fp and then tests for NULL
if ((fp = fopen(...)) != NULL) {
i
possible.
The default assignment operators of classes also return the object to which something is
assigned. This can be used to implement the *= operator more concisely:
Fraction Fraction::operator *= (Fraction f)
{
‘x *=y’ ==> X%=x*y’ andx returned
return *this = *this * f;
}
The readability of such expressions can, of course, be scrutinized. For some people, this is the
elegance of C++; for others, it is the ugliness.
Anyway, in C++, the programmer is able to define whether the object should be returned by
the assignment operator or not, and thus controls the elegance, or ugliness, of the code.
If you are undecided, you can simply define the multiplicative assignment so that no return
value is defined:
void Fraction::operator *= (Fraction f)
i
Hx *=y’? ==> K=x *y’
*this = *this * f;
b;
However, this is rather unusual in C++. Users of this class would usually assume that assignment
operators can form part of a larger expression.
4.2 Operators for Classes 159
In the version introduced above, the *= operator is implemented so that it uses the = and * opera-
tors. It is therefore guaranteed that the statement ‘x *= a’ always does the same as the statement
‘x = x * a’. However, this implementation has a possible running-time disadvantage: another
function for multiplication is called that creates a temporary object for the return value, which
can then be assigned.
The semantics of the operator can also be directly implemented:
Fraction Fraction::operator *= (Fraction f)
1
// numerator and denominator multiplied directly
numer *= f.numer;
denom *= f.denom;
// include header files for the classes that are being used
#include "frac.hpp"
int main()
{
CPPBook: :Fraction x; // declare fraction x
CPPBook::Fraction w(7,3); // declare fraction w
160 4: Class Programming
// output fraction w
w.print();
// multiply x by a
X *= W;
// and output
eeprincw
os:
As this is also not a member function (i.e. Fraction: : is missing), the given parameter is the
only operand. Therefore, we define a unary operator for -, namely negation, rather than a binary
subtraction.
Again, as this operation does not belong to a class, there is no access to the private members
of the operand. The numerator of f cannot simply be multiplied by -1. Therefore, the integer -1
is converted into a fraction, which is then multiplied by f.
Both globally defined operators could be used, for example, as follows:
CPPBook=; Fraction x, ay;
Wy 2 So) i see
The fraction x is negated with the globally defined negation, and the result is then multiplied by
the integer 3 using the globally defined multiplication operator. This result is then assigned to
the fraction y using the predefined assignment operator of the Fraction class.
sizeof
wx
ae
Overloading them is mainly not permitted because the operators already have a predefined
meaning for all objects, which serves as a fundamental base for the whole syntax of C++.
For the ?: operator, overloading is not considered to be worthwhile (?: is the only ternary
operator).
The . * operator is introduced in Section 9.4 on page 557.
On page 569 there is a list of all the C++ operators.
A default assignment operator is defined for every class. It assigns member-wise and returns
the object to which the new value is assigned. If the default behavior does not make sense for a
user-defined class, an assignment operator can (and should) be defined.
If no assignment is possible, the assignment operator should be declared private. For ex-
ample, the following declaration will make an assignment impossible for two fractions *:
class Fraction {
private:
// assignment forbidden, as operator is declared private
Fraction operator = (Fraction) ;
3;
Because a member-wise assignment is usually a problem for classes with pointers as members,
the assignment operator must be defined for such classes. Detailed informations and examples
of this can be found in Section 6.1.5 on page 362.
> The assignment operator should actually be defined with a slightly different syntax; however, the necessary
language feature, a reference, is not introduced yet. The correct syntax can be found in Section 6.1.5 on
page 362.
164 4: Class Programming
The unary increment and decrement operators ++ and -- have both a prefix and a postfix notation
(see also Section 3.2.3 on page 39). In the prefix notation
++X
for fundamental types, the expression ++x returns the value of x after the incrementing it (i.e. the
value is increased, then returned). In the postfix notation
x++
for fundamental types, the expression x++ returns the value x has before it is incremented (i.e. the
value is returned, then increased).
For example, this difference is often used when accessing elements of an array:
x = elems[it++];
In this statement, the index i is incremented, but x gets the value of the element with the index i
has before it is incremented.
This distinction can also be implemented for classes. As both involve unary operators, the
differentiation is determined by declaring a dummy parameter to the declaration of the postfix
operator, while the prefix version has no such parameter. For example, the following declarations
define a prefix and a postfix increment operator for class X:
class X {
public:
operator ++ (); // prefix notation, called for ++x
operator ++ (int); // postfixnotation,
called for x++
¥5
Again, it is up to the programmer to ensure that the operators behave as expected. That is, both
increment an object (whatever this means), while the first returns the value of the object before
it is incremented and the second returns the object after it was incremented.
The same applies to the decrement operator.
The index operator, [ ] (sometimes also called the array operator or subscription operator),
can
also be defined for classes. This gives objects array-like characteristics, even though
they may
not be arrays. Such objects then receive array characters.
The index operator is a binary operator, in which the index passed is the second operand.
For
member functions, the index is therefore passed as parameter.
Typical examples of the implementation of index operators are collection classes,
in which
the ith member can be accessed using operator []. One example applicati
on is the standard
vector class (see Section 3.5.1 on page 71). As another example, the following
declaration
defines operator [] for a class whose concrete objects (instances) are members
of a collection
of Persons:
4.2 Operators for Classes 165
class Coll0fPersons {
public:
// return idxth member from the collection (a person)
Person operator [] (int idx);
};
A person of an Coll0fPersons can then be accessed as follows:
void foo (Coll0fPersons allEmployees)
{
Person p;
}
Note that the parameter of the index operator can have an arbitrary type. Because of overloading,
the operator can even be implemented differently for different index types. So-called associative
arrays can be implemented in this way. For example, when accessing the collection of Persons
using the [ ] operator, it is also possible to pass the name of the person as an index rather than
an integer. To be able to do this, we can declare the operators as follows:
class Coll0fPersons {
public:
// return ith member of the collection (a person)
Person operator [] (int i);
de
The following call can then be made:
void foo (Coll0fPersons allEmployees)
{
Person p;
}
In Section 6.2.1 on page 373, an additional example of the definition of an index operator is
given.
166 4: Class Programming
Just as it can be useful to define objects that behave like arrays, it can also be useful to de-
fine objects that behave like pointers. As the semantics of the operations can be defined freely,
pointer-like objects can be assigned a certain degree of intelligence. For this reason, such objects
will be called smart pointers. This will be discussed in more detail in Section 9.2.1 on page 536.
The function call is also an operator, which can be defined for classes. By doing this, we can let
objects behave like functions. This sounds strange, I know, but can be useful.
Its declaration looks as follows:
class X {
public:
void operator () (); __ // declare operator () without parameters
‘:
This kind of declaration enables the following call to be made:
Kea; // create object a of class X
4.2.7 Summary
¢ Operators can be defined for objects of a class. They are declared as operator functions with
the keyword operator.
e Operators can be overloaded for classes.
e The collection of possible operators, their priority, syntax and evaluation order, is predefined
and cannot be changed.
e Operators should always be implemented so that they perform ina way that you would expect.
This usually corresponds to how they behave for fundamental types.
4.2 Operators for Classes 167
For example, if operators are defined for classes, the following relationships should apply:
aptly neither changes x nor y
xeeairy correspondsto x + (-y)
XxOp= y correspondsto x = xopy
xe=e xe 1 corresponds to x =h1 corresponds to ++x
p->k correspondsto (*p).k corresponds to pL0]
e In an member function, this describes a pointer to the object for which the function was
called. *this is therefore always the (dereferenced) object for which a member function
was called. In the case of operators defined as member functions, this is the first or only
operand.
A list of all C++ operators can be found on page 569.
168 4: Class Programming
Header File
The header file for the Fraction class with the new language features is as follows:
// classes/frac3.hpp
#ifndef FRACTION_HPP
#define FRACTION_HPP
/* Fraction class
a|
class Fraction {
private:
int numer;
int denom;
pubieve:
/* new: default constructor and one- and two-parameter
* constructors combined into the one function
fe
Prackitom (Gnte—0 ante —s))-
/* output
* - new: defined inline
*/
void print() {
std::cout << numer << ’/’ << denom << std::endl;
// multiplication
Fraction operator * (Fraction) ;
// multiplicative assignment
Fraction operator *= (Fraction) ;
// comparison
bool operator < (Fraction);
¥3
/* operator *
*- new: defined inline
ae
inline Fraction Fraction::operator * (Fraction f)
{
/* simply multiply numerator and denominator
* - no reducing yet
170 4: Class Programming
Hh
return Fraction(numer * f.numer, denom * f.denom);
#endif /| FRACTION_HPP
Before discussing the changes in detail, we give the new dot-C file.
Dot-C File
According to the changes in the header file, the dot-C file of the Fraction class now only has
one constructor. In addition, the definition of the two inline operations are omitted:
// classes/frac3.cpp
namespace CPPBook {
denom = -d;
}
else {
numer = n;
denom = d;
i
}
/* operator
*=
cl
Fraction Fraction::operator *= (Fraction f)
{
Hx *=y’ ==> K=ax*y’
*this = *this * f;
/* operator <
+/
bool Fraction::operator < (Fraction f)
<
// since the numerator cannot be negative, the following is sufficient:
return numer * f.denom < f.numer * denom;
The changes of the Fraction class are explained in the following sections.
The default value is automatically provided by the compiler if too few parameters are passed. As
this happens during the call, the origin of the parameters is irrelevant in the implementation of
the function. The implementation therefore declares both parameters with no default values:
Fraction::Fraction(int n, int d)
{
}
In order to ensure a clear assignment, default values may only be defined at the end of a parameter
list. On a function call, the passed arguments are assigned to the leading parameters in the same
order. Thus the first argument becomes the first parameter, and so on. If all arguments are
exhausted, the remaining parameters are assigned their default values. It is therefore clearly
defined that if only one argument is passed, it is always the first parameter.
Global functions can also have default values. For example, the following global function,
max (), returns the maximum of two, three or four unsigned integers:
unsigned max(unsigned, unsigned, unsigned = 0, unsigned = 0);
When calling the function with two arguments, the default value 0 is used for the final two
parameters.
When using default arguments, you must ensure that a blank space is placed before the as-
signment operator if it is a parameter or a pointer:
void f(const char* = "hello"); // OK
Otherwise, it is assumed that the *= operator is used, which is an error:
void f(const char*= "hello"); //ERROR
Default arguments cannot be redefined in a second declaration. It is also not possible to use
default arguments with operator functions.
It should be noted that the type of a function is not affected by some parameters having default
values. A pointer to the max() function must be declared as a pointer to a function that has four
unsigneds passed to it:
unsigned (* funcpointer) (unsigned, unsigned, unsigned, unsigned);
To use default values here, you must again define them:
unsigned (* funcpointer) (unsigned, unsigned,
unsigned = 0, unsigned = 0);
If a function can also be called via a function pointer, different default values can
be used. How-
ever, you should never do this. If calling max() directly with two parameters leads
to a different
result than if it is called via a function pointer, this will be very confusing.
One issue should be taken into account: default arguments combine multiple
functions, reduce
program code and improve consistency (changes must only be carried out at
one place). How-
ever, running time can be prolonged by using default arguments. For example,
if the max()
4.3 Running Time and Code Optimization 173
function for two, three or four arguments is called with only two arguments, all four parameters
are compared.
The new version of the Fraction class provides an example of this. If the constructor is
called with less than two arguments, the default value for the denominator is set to 1. However,
inside the function, it is checked (unnecessarily) whether 0 is negative. To avoid this kind of
running-time cost, only two constructors of the Fraction class need be combined:
class Fraction {
public:
/* default constructor and one-parameter constructor combined
* - defined as an inline function
=/.
Fraction(int n = 0)
: numer(n), denom(1)
/* two-parameter constructor
* - with special treatment for denominator less than zero
th
Pract
lon ante ani i
be
Functions can be declared as being inline. The inline specifier indicates that inline substitution
of the function body at the point of call is preferred over the usual function call mechanism.
That is, a function call may be replaced by the statements of the called function body without
changing the semantics. To be able to do this, the function body needs to be known when the
function is called. Therefore, the whole function is implemented in the header file.
Inline functions can be defined in two different ways:
e Either the function is defined in the header file with the inline keyword:
class Fraction {
void print() {
std::cout << numer << ’/’ << denom << std::endl;
ys
In both cases, the compiler is given the ability of generating code to execute the statements
directly, instead of generating a function call. Thus, a call such as
b. print()
can be translated into
std::cout << b.numer << ’/’ << b.denom << std::endl;
// include header files for the classes that are being used
#include "frac.hpp"
int main()
{
using namespace CPPBook; // new: all symbols of the namespace CPPBook
// are considered global in this scope
Using Declarations
A symbol of a namespace can be accessed Jocally by using a using declaration. The statement
using CPPBook: :Fraction;
causes Fraction to become a local synonym for CPPBook: : Fraction in the current scope.
These declarations correspond to a declaration of local variables, and can also hide global
variables. For example:
namespace N { // namespaceN
MG, Be, Wi, PAS
lj
void usingDeclarations()
{
int y = 0;
using N::x; // symbol x of namespace N is locally accessible
using N::y; // ERROR: y is declared twice in usingDeclarations()
usine’ Nie Zz; /| hides globalz
Paes //N::x
LEIRZAR aN ierz,
}
Using Directives
A using directive enables all names of a namespace to be accessed without qualification. Through-
out the current scope, all symbols in that namespace are considered to be defined globally. This
might cause ambiguities with existing global symbols. If the
using namespace CPPBook;
directive is used, all symbols of the CPPBook namespace will be considered as global variables
in the current scope.
If there is already a symbol in the global scope, which is also in the global namespace, its
usage Causes an error. For example:
namespace N { // namespaceN
LE Ge XV gas
void usingDirective()
{
int y = 0;
using namespace N;_ // symbols of namespace N globally accessible
oThxes Wa pases
Frys // local y
+tZ > // ERROR: N: :z or global z?
3
Using directives should never be used in a context where it is not clear what symbols are known.
Otherwise, a using directive can change the visibility of other symbols, leading to ambiguity
and different behavior (a function call suddenly finds a completely different function than it did
without the directive). Using directives should therefore be used with caution. In fact, they
should never be part of a header file.
Koenig Lookup
Without the use of using, it is not always necessary to enter the namespace of a symbol. Pro-
vided an operation with arguments is called, the operation is also searched for in all namespaces
of the arguments passed. This is commonly known as Koenig lookup. For example:
#include "frac.hpp"
CPPBook::Fraction x;
In the new version of the application program, it should be noted that the definition of x does not
occur at the beginning, but in the middle of the block, after some statements have been carried
out:
W Din), // statement
In C++, variables may be declared anywhere in a block. Its scope then covers the declaration up
to the end of the block.
This feature was introduced to avoid variables having to be declared at a time when they
cannot be initialized. If a parameter that has yet to be processed is needed to create an object, the
declaration can be postponed until the appropriate statements have been carried out.
The alternative would be an ‘incorrect’ initialization, such as with the default constructor.
The object may well be initialized, but would have to be corrected again later. This takes time
with larger objects. In addition, classes can be defined for which you cannot modify the value
once it is initialized. For these objects, it should be possible to create them later when all initial
values are prepared.
This freedom of point-of-declaration runs the risk of creating hard-to-read programs. The
declaration of variables is no longer restricted to the beginning of blocks, but can occur anywhere
in the code. For this reason, declarations after the beginning of a block should only be used if it
is necessary or improves readability. They should always be done at the beginning of a logical
section.
One frequent application of declarations in the middle of the code are declarations inside a for
loop. Values used to iterate over the loop are a typical example. You can define them just inside
the head of a for loop:
for (int i=0; i<num; ++i) {
}
Here, i is only known within the for loop. A second loop with a further declaration of i would
be valid. The for loop shown above is equivalent to the following implementation:
al alias ahs
for (i=0; i<num; ++i) {
}
However, in practice, this kind of declaration presents another small problem: in older versions
of C++, i declared as above would be known until the end of the block that contains the com-
plete for loop. i would therefore still be declared after the for loop. The loop head was not
considered to be part of the same block that contains the loop body. Therefore, a second for
loop in the same block could not be implemented:
for (int i=0; i<num; ++i) {
for (int i=0; i<num; ++i) { // ERROR: i defined for the second time
4.3 Running Time and Code Optimization £9
This behavior was corrected during the standardization of C++. In the conditions of if, switch,
while and for statements, declarations can appear that are only valid for the scope of the state-
ment. Thus, the whole statement can be considered to be a separate block.
Due to the relatively late standardization of the language, there are still systems in which i is
declared after the loop. This should be taken into account when implementing portable code.
In C++, there is a difference between a declaration with simultaneous initialization, and a decla-
ration without initialization but with a later assignment.
The initializations
CPPBook::Fraction tmp;
and
CPPBook: :Fraction tmp;
4.3.8 Summary
e Default arguments can be defined for the parameters of functions. These are used if the
corresponding parameters in a function call are not passed.
e Functions and operator functions can be declared inline. This enables the compiler to replace
a function call with the statements of the respective function. This has no effect on the
semantics of a program, and is only used to reduce the running time.
e Declarations do not necessarily have to be made at the beginning of a block. In cases where
a declaration is made in the middle of code, the scope starts from the declaration and lasts
until the end of the block.
e A constructor that creates a new object using an existing object of the same type is called a
copy constructor.
e There is a default copy constructor that copies member-wise.
e By using using declarations and using directives, the qualification of symbols with a names-
pace can be omitted.
e Using directives should not be used in header files.
e A function is also automatically searched for in the namespaces of any arguments passed.
And. References and Constants 181
return *this;
// a copy of the first operand is returned
}
The parameter f in the implementation of the operator function becomes a copy of w that is
created using the predefined copy constructor of Fraction. Inside the operator function, f
could be manipulated without any effect on the passed w argument.
In the same way, a copy of *this is returned by the function. Because in our example the
operator function is called for x as first operand, a copy of x is returned as a temporary object.
Again, this copy is created using the copy constructor.
If performance is an issue, creating copies is acceptable when creating simple types such as
ints, floats and pointers. With objects with several fairly complex members, this can lead to
182 4: Class Programming
a considerable running-time disadvantage because a copy of the object must be made with every
parameter passed.
The usual alternative in C would be to pass pointers. Thus, instead of passing an object,
the address of an object is passed. As a consequence, in order to access the passed object, you
must dereference the pointer. An additional consequence is that any modification of the passed
object now also modifies the actual object. The implementation with pointers then emulates the
counterpart to call-by-value, this being call-by-reference.
If the running-time disadvantages, resulting from copies being created when arguments are
passed, can only be dealt with by using pointers, it would no longer be possible to use abstract
types in the same way as fundamental types. A multiplication of fractions could then only be
declared with a pointer as a second operand. In the application program, a call for multiplication
would have the following form, then:
x * &a // multiply x by a (without copy)
To avoid this ugly syntax, references were introduced into C++. These enable arguments to be
passed without making copies or using pointers. Thus, call-by-reference is supported.
There are now only two problems:
e As an object passed as an argument might get modified, you can no longer pass constants.
This affects temporary objects that are constant.
e There is a danger of objects passed by reference being accidentally modified in the function
called.
However, here we can benefit from the fact that objects (including parameters) may be declared
as being constant. By declaration a parameter to be constant, you can specify that a passed
argument is not modified, which implies that you can pass constant and temporary values, then.
4.4.2 References
References as Parameters
If a reference is declared as a parameter, it will be initialized by a function call with the passed
argument. Again, no new object is created, but a second name for the passed argument is defined.
Every modification of the parameter inside the called function will therefore also be performed
with the argument (the call-by-reference mechanism)’.
For example, a function can be implemented that swaps two arguments:
// progs/swap.hpp
tmp = a;
a = b;
b = tmp;
}
4 Pascal programmers will already be familiar with references as parameters. Instead of &, they use the var
keyword for the parameter declaration.
184 4: Class Programming
If you pass two integers to this function, their values are swapped:
// progs/swap.cpp
#include <iostream>
#include "swap.hpp"
int main()
a
int x = 7;
ae yy S ilehe
When swap () is called, a is defined as a second name for x, and b as a second name for y. An
assignment to a therefore changes x, and an assignment to b changes y.
This kind of language feature has its advantages and disadvantages. For C programmers, who
do not have this feature, this might be a pleasant dream or a nightmare, depending on their point
of view:
e The advantage of references is that we are no longer forced to pass pointers as arguments
if we want to modify objects or improve running time. For the function above, simply call-
ing ‘swap(x,y)’ is sufficient for the values to be swapped. To achieve the same effect in
C, ‘swap (&x,&y)’ has to be called, and the swap() function has to be implemented with
pointers.
e The disadvantage of references is that, in C++, it is no longer clear at the point of the
call
whether the passed argument can be modified. In C, when “swap (x,y)’ is called, it is guar-
anteed that both arguments cannot be modified, as copies have been made in both
cases. In
C++, in order to know whether an argument can be manipulated within a function, you will
have to look at the function declaration.
Note that it only depends on the parameter declaration whether or not copies are
made. The
statements inside the function use a and b as if they were defined as ordinary integers.
In other
words: references do not affect the type of variables and objects; they only affect whether
copies
are created.
4.4 References and Constants 185
In the Fraction class, for example, you should declare the *= operator function with references
in order to prevent copies from being made:
Fraction& Fraction: :operator *= (Fraction& f)
t
jl x t=y => eax ty
*this = *this * f;
4.4.3 Constants
References as Constants
There is still one problem with the *= operator function: as the value of the submitted parameters
can be modified inside the function, we must pass an object for which this is allowed. However,
this rules out both constants and temporary objects. A call such as
x *= CPPBook::Fraction(2,3) ;
or
X *= W* W;
would not be possible. For this reason, a parameter passed as a reference should be declared as a
constant if it is not to be changed.
This leads to the following definition of the *= operator:
Fraction& Fraction: :operator *= (const Fraction& f)
{
[i wtayh=> Kax*y’
*this = *this * f;
By doing so, a constant or a temporary object can also be used as a second operand.
The return value could also be declared as a constant reference:
const Fractionk Fraction::operator *= (const Fraction& f)
{
ji kn=Vo So tex ey?
*this = *this * f;
Constants in General
Every object can be defined as a constant in C++. The const keyword simply has to be
used
during its declaration. Constness is part of the type. The type checking of the compiler
verifies
that no constants are accidentally modified.
For example:
const int MAX = 3;
const double pi = 3.141592654;
The const keyword replaces the historical practice in C of defining constant values
using the
Preprocessor statement #def ine. The ‘blind’ replacement by the preprocessor is replaced
by the
4.4 References and Constants 187
declaration of a symbol, for which the usual type checking and scope rules apply. The only (but
decisive) difference from variables is that their values cannot be modified. This means that their
values have to be initialized at the time of their creation.
In the same way, a fraction can be declared as a constant:
const CPPBook::Fraction vat(16,100) ;
If a constant is authorized as a second operand for the *= operation, calling x *= vat is permit-
ted.
But what about the first operand? These are modified within the *= operation, and therefore
no constants can be used as an operand. Nevertheless, with a pure multiplication (w * w), it is
reasonable that the first operand for which the operator is called can be constant.
For this to be possible, a member function that can be called for constant objects must be
labeled as such. This kind of constant member function is declared using the const keyword
between the parameter list and the function body.
The multiplication must then be declared as follows:
Fraction Fraction::operator * (const Fraction& f) const
{
return Fraction(numer*f.numer, denom*f.denom) ;
y
The const at the end of the declaration determines that the fraction for which this operation is
called (therefore the first operand) is not modified. Every attempt to manipulate numer or denom
will therefore cause a syntax error.
No reference is returned by the multiplication, as it is a return value of a local object. If
this was not the case, a second name would be returned, which would no longer exist when the
function was left. This is the temporary fraction created by the expression
Fraction(numer*f.numer, denom*f .denom)
Most compilers recognize this kind of error and output an appropriate warning.
As a copy is returned rather than a reference, the return value does not have to be declared
with the const keyword. Temporary objects are basically constant.
We will now discuss the new version of the Fraction class with an appropriately changed
application program.
188 4: Class Programming
Header File
The header file now declares the function with references and constants, provided that it makes
sense, and has the following structure:
// classes/frac4.hpp
#ifndef FRACTION_HPP
#define FRACTION_HPP
// oh Ko 3 BEGIN namespace CPPBook BG OBosAS2S2sIR283 ik2482482 2k2k2k3k2 ofokoo 2 2K2Koki OKOK Kok
namespace CPPBook {
class Fraction {
private:
int numer;
int denom;
public:
/* default constructor, and one- and two-parameter constructor
oy
Fraction(int = 0, ant =" 1);
// multiplication
Fraction operator * (const Fractionk) const ;
// multiplicative assignment
const Fraction& operator *= (const Fraction&) ;
// comparison
bool operator < (const Fraction) const;
4.4 References and Constants 189
/* operator *
* - defined inline
ae
inline Fraction Fraction::operator * (const Fraction& f) const
t
/* simply multiply numerator and denominator
* - no reducing yet
ni
return Fraction(numer * f.numer, denom * f.denom);
#endif //FRACTION_HPP
The print(), operator*() and operator<() member functions do not modify the object
for which they are called (the first operand), and are therefore declared as constant member
functions. For all three operator functions, the parameter is declared as a reference. As it is also
declared as a constant, it can be a constant or a temporary object.
In other words, to get the same semantics for the caller, you can always replace a parameter
declared as passed by-value as being a constant reference. This will only be a problem if the
parameter is modified inside the function. However, this will be noticed by the compiler and,
due to the resulting error message, you could then switch back to passing an argument by-value.
Dot-C File
The declaration of the Fraction class is adjusted accordingly in the dot-C file. This only affects
the declarations. The implementation of the functions do not have to be altered:
// classes/fraci.cpp
namespace CPPBook {
/* operator *=
z/)
const Fraction& Fraction::operator *= (const Fraction& f)
al
UBS TOSS Gea i
*this = *this * f;
/* operator <
oy,
bool Fraction: :operator < (const Fraction& f) const
‘
// since the numerator cannot be negative, the following is sufficient:
return numer * f.denom < f.numer * denom;
Application
The declarations that have been changed do not have any influence on the application program
at all. However, constant objects can also be used here, provided that they make sense:
// classes/ftest4.cpp
// include header files for the classes that are being used
#include "frac.hpp"
int main()
{
using namespace CPPBook; //all symbols of the namespace CPPBook
// are considered global in this scope
Here, print () can only be called for the fraction constant a because the function is declared as
a constant member function.
In addition, the comparison x < Fraction(1000) is only possible because the parame-
ter passed to the operator function < as a second operand can be a constant. The expression
Fraction(1000) returns a temporary object that is constant, like all temporary objects.
This section concludes with some remarks about working with constants. Pointers can also refer
to constants. However, constancy is part of the type. Therefore, in the declaration, you have to
specify that they refer to objects that cannot be modified. These kinds of pointers can also refer
to variables (but not vice versa).
192 4: Class Programming
For example:
erie // int variable
Consitm 1 ticm mila // int constant
Positioning const
The const keyword can be placed both before and after the type:
cOnstsipt.i2 = 13) // OK
int sconst"id) 2012: // OK
However, it is normally put at the front.
Constant references can also be declared differently:
void f1 (const int &); // OK
void f2 (int const &); //OK
4,45 References and Constants 193
is equivalent to
int const N = 100;
There follows a warning concerning constants: constants do not offer total security from mod-
ifications, as, in principle, it is possible to convert constants into variables using explicit type
conversions. This will work, provided that the constant is not in the read-only area of a program.
There are even examples in which the act of converting a constant into a variable may make
sense, or may even be necessary. Section 6.2.4 on page 381 discusses this in more detail, and
introduces the mutable keyword, which is related to this issue. In Section 3.4.4 on page 68,
the const_cast keyword is introduced, which is provided for type conversions that remove
constness.
It should be noted at this point that not all compilers report the use of constants as variables as
an error. Other compilers only issue a warning in such cases (for example, the GCC). However,
these kinds of warnings should always be regarded as an error.
4.4.7. Summary
e A reference is declared using the statement & in the type of a declaration. A reference is a
(temporary) ‘second name’ for an existing object.
e The reference symbol does not affect the type of an object.
e By using references in the declaration of parameters and return values, the creation of copies
is prevented (call-by-reference instead of call-by-value).
e References must be initialized when they get defined and cannot change the object or value
to which they refer.
e Constants can be declared using the const keyword.
e A const placed after the parameter list in a member function defines a constant member
function. This means that the object for which it is called may not be modified. This object
can therefore also be a constant, then.
e const should always be used if something cannot, or should not, be modified. This guaran-
tees that temporary objects can be used as arguments.
e You can place the const keyword before or after the type being declared to be constant.
4.5 Input and Output Using Streams 195
4.5.1 Streams
I/O in C++ is provided using streams. A stream is a ‘data stream’, in which the characters are
read and written in a formatted way. As stream objects are an application of the object-oriented
concept, their properties are defined in classes. Various global objects are predefined for the
standard channel for I/O.
Stream Classes
As there are different kinds of I/O (input, output, data access, etc.), there are also different classes
to handle them. The two most important classes are the following:
e istream
The istream class is an ‘input stream’ from which data can be read.
e ostream
The ostream class is an ‘output stream’ on which data can be output.
Like all elements of the standard library, both classes are defined in the std namespace. Thus,
their declaration may look as follows:
namespace std {
class istream {
ks
class ostream {
3
196 4: Class Programming
However, this is highly simplified. In reality, there are numerous classes, and the classes are also
defined in a fairly complicated way. In Section 8.1 on page 472, additional classes and examples
of their use are introduced in more detail.
Three global objects belong to the istream and ostream classes. These play a central role in
I/O, and are defined by the stream library:
e cin
cin (istream class) is the standard input channel from which a program normally reads
the input. It corresponds to the C variable stdin and is typically assigned by the run-time
systems to the keyboard.
e cout
cout (ostream class) is the standard output channel to which a program normally writes the
output. It corresponds to the C variable stdout and is typically assigned by the run-time
system to the screen.
e cerr
cerr (ostream Class) is the standard error output channel to which a program normally
writes error messages. It corresponds to the C variable stderr and is typically assigned by
the run-time system to the screen. The output to cerr is not buffered.
By separating output into ‘normal’ outputs and error messages, the two can be treated differently
in the operating system environment called by the program. This is the result of the Unix concept
of I/O redirection, for which C was originally written. While the normal output of a program
could be redirected to a file, the error message could still be displayed on the screen.
These three stream objects are also defined in the std namespace, which can be simplified as
follows:
std::istream cin;
std: :ostream cout;
std::ostream cerr;
There is also a fourth globally predefined object, clog, but this does not play such a significant
role. It is provided for protocol output, and sends its buffered output to the standard error output
channel.
// t0/toprog.cpp
int main()
<
IHG Dep AP
// read x
Sidi COU Ca Se Luxe
if (ieterd=:cing>>yx)) 1
/* error when reading
* => exit program with error message and error status
ah
std::cerr << "Error when reading an integer"
SK enolsBrehavolll &
return EXIT_FAILURE;
// read y
Siecle seeing << VMyeg Ne
toe estan Cito 2Vy)) a
/* error when reading
* => exit program with error message and error Status
*/
std::cerr << "Error when reading an integer"
<< std::endl;
return EXIT_FAILURE;
// error if y is zero
if (y == 0) f
/* division by zero
* => exit program with error message and error status
ae
198 4: Class Programming
First, the header file for the stream classes and global stream objects, <iostream>, is included:
/| header file for I/O with streams
#include <iostream>
In the line
std::cout << "Integral division (x/y):\n\n";
a string literal is written to the standard output channel. This is done by calling the << operator
for the std: : cout object, with a string literal as a second operand. The << operator is the output
operator for streams, and is defined so that whatever is passed as the second operand is written
to the stream (the data are sent in the direction of the arrow).
The special feature of this operator is that it is not only overloaded for all fundamental types,
but can also be overloaded for any user-defined type. Therefore, the second operand can be of
any type. Depending on the type, the appropriate operator function will be called automatically,
which outputs the second operand (converted into a sequence of characters). For example:
alia oh 7/8
stds :cout << i; // outputs
*7’
float f = 4.5;
Sicdncoucm<<arn- // outputs ‘4.5’
CPPBook::Fraction x(3,7);
Std scout << xX; // outputs ‘3/7’ (provided it is defined this way)
This is a fundamental improvement in comparison to the I/O techniques of C using printf ():
e The format of the output objects do not have to be specified but are automatically derived
from their type.
e The mechanism is not restricted to fundamental types and is therefore universally applicable.
More than one object can be written using the operator <<. The output operator returns the
first
operand (the stream) for further use. This enables a concatenated call of output operators,
as
shown in the last line of the example:
std::cout << x << " divided by " << y <<" gives a:
<< x / yix<istdsendi:
4.5 Input and Outputeee
ee Using eee
Streams ee eee 199
eee
With the knowledge acquired so far, we can introduce the implementation of the ostream class
necessary to provide this interface. For objects of this type, the << operator is overloaded with
all fundamental types as the second operand:
namespace std {
class ostream {
public:
ostream& operator<< (char); // output character
ostreamk operator<< (int); // output integer
ostream& operator<< (long); // output long integer
ostreamk operator<< (double) ; // output floating-point value
ostream& operator<< (const char*); // output C-string
};
ie
The first operand (therefore the stream itself) is returned when calling these operators. For ex-
ample:
namespace std {
ostream& ostream: :operator<< (char) {
// low-level function to output the character
return *this; // return stream for chaining output
Manipulators
As the name suggests, manipulators are special objects whose ‘output’ manipulates the stream.
For example, output formats can be defined or buffers can be emptied. Thus, although the ma-
200 4: Class Programming
nipulator is “sent as output’ to the stream, it does not necessarily mean that something is written
to the underlying channel.
The end1 manipulator stands for ‘endline’ and does two things:
e It outputs a newline (the character ‘\n’).
e It flushes the output buffer (i.e. empties the output buffer by sending all characters to the
underlying output channel).
The most important predefined manipulators are listed in Table 4.1.
Manipulator Meaning
std::flush ::ostream | flush output buffer
std::endl ::ostream | write ‘\n’ and flush output buffer
std: :ends ::ostream | write ‘\0’ and flush output buffer
::istream | skip whitespace
Section 8.1.5 explains exactly what manipulators are, which ones exist, and how they can be
implemented.
In the line
ie () (eam SS sa) 4
}
a new value is read into the integer x. This is done using the counterpart to the output operator,
the input operator >>:
Gui DS x
The >> operator is defined for streams so that whatever is submitted as the second operand is
read in (again, the data are sent in the direction of the arrow).
In principle, the input operator can also be overloaded for any type and called in a chain:
double d;
CPPBook: :Fraction f;
namespace std {
class istream {
public:
istreamk operator>> (char&); // read in characters
istream& operator>> (int); // read in integers
istream& operator>> (long&) ; // read in long integers
istream& operator>> (double&); // read in floating-point values
In the above example, the input operator is not called in a chained fashion for multiple values.
This is because, for each reading, an immediate check is performed to see whether the reading
process has been successful (reading could always go wrong). The check is done by calling the
! operator. For streams, the ! operator is defined so that it yields whether the stream is (still) in
good shape. If this is not the case, our program outputs an error message and exits:
tf CH (std cane > rx) 1
std::cerr << "Error when reading an integer"
<< std::endl;
return EXIT_FAILURE;
x
What actually happens here is quite tricky. The expression
Std. Gin >> x
returns no Boolean value. Instead, std::cin is returned, so that a chained call is possible.
By applying the ! operator to the stream object std: : cin, a Boolean value is returned, which
indicates whether this object has an error status.
The statement
ae (2 (geeks erm S550) Ft
}
therefore corresponds to
hues Gill, SS oes
Tiel Wstdescin) +
I
For stream objects, the ! operator is defined so that a Boolean value is returned, which states
whether the stream has an error status:
202 4: Class Programming
namespace std {
class istream {
public:
// returns true if there is something wrong with the stream
bool operator ! ();
3;
}
Using a similar trick, the corresponding positive test is also possible. Boolean statements in if or
while statements either require an integral type (all forms of int or char) or a type conversion
of an object of a class in an arithmetic or a pointer type.
Because there is such a conversion function defined for streams, we can write
it (std=scin >a) 1
// reading was successful
}
Again, this has the same semantics as the following:
Sitd Cane > sx
ita sitde cin) me
}
Such a usage of the stream object is possible because there is a corresponding implicit type
conversion defined. What occurs here exactly will be explained in the section on conversion
functions (see Section 4.6.4 on page 227) and the use of dynamic members with classes (see
Section 6.2.3 on page 378).
A typical example of the ability to query the status of a stream by its use in a condition is a
loop that reads in and processes objects:
// as long as obj can be read in
while (std::cin >> obj) {
// process obj (in this case, output)
std::cout << obj << std::endl;
}
This is the classical filter framework of C for C++ objects. However, note that the >> operator
skips leading whitespace. Therefore the version with char as obj must be implemented in
another way to process all characters (see Section 8.1.4 on page 480).
However nice the use of these special operators in conditions may be, one thing should be
taken into account: the ‘double negation’ does not neutralize the call, here:
e ‘std::cin’ is a stream object of the std: : istreamclass.
e ‘!!std::cin’ is a Boolean value, describing the state of std: : cin.
4.5 Input and Output Using Streams 203
This example shows that the mechanism must be used with caution (and can also be considered
dubious). The expression in the if statement is not what is normally expected, namely a Boolean
value. An implicit type conversion, whose existence and meaning has to be known in order to
understand the code, makes the expression comprehensible.
As in C, one can argue whether the current programming style is better or not. However,
there is no doubt that using the good() member function (which returns whether or not a stream
has an error-free status) would make the program (even) more readable:
Sigeks atouliay D> es
af (! std: cin tyood ©.
Using the ! operator shows that a stream can exist in several states. To represent the principle
conditions of a stream, different bit constants are defined as flags, managed in an internal stream
member (see Table 4.2).
The difference between failbit and badbit is basically that badbit signals a fatal error:
e failbit is set if an event could not be carried out correctly; the stream, however, can still be
used, in principle.
e badbit is set if the stream is, in principle, no longer OK, or data has been lost.
The status ofthe flags can be determined using the good(), eof (), fail(), and bad() member
functions. They return a Boolean value, indicating whether one or several flags are set. In
addition, there are two more general member functions for setting and querying these flags:
rdstate() and clear () (see Table 4.3).
By calling clear() without parameters, all error flags (including ios: :eofbit) are deleted:
// unset all error flags (including eofbit)
strm.clear();
However, if a parameter is passed, the corresponding flags are set and all others are unset. The
following example tests whether the failbit flag is set in the strm stream, and deletes it if
necessary:
204 4: Class Programming
Table 4.3. Functions for setting and querying stream status flags
e It can be interpreted in a strictly object-oriented fashion (taking the first operand as the re-
ceiving object and the second operand as the argument of the message):
a.operator*(b)
e It can also be viewed as a global combination of just two operands (both passed as arguments,
with no receiving object):
operator* (a,b)
In the first case, an appropriate operator in the class of the first operand, a, must be defined. In
the second case, an operator outside any class must be defined that handles both operands. In
this case, the first operand is also a parameter.
The same applies to a call such as
Sit.CicenG
Ol teeGum
Here, x must either have a type for which the operator << is defined within std: : ostream:
namespace std {
class ostream {
public:
ostream& operator << (type); // parameter is second operand
I;
si
or an operator that combines both operands with << must be defined outside any class:
// both operands are parameters:
std::ostream& operator << (std::ostreamk, type);
The former applies to all fundamental types. The second possibility is used to extend this mech-
anism to custom types. If access to the internal data of the custom type is required, a member
function of the appropriate class of the second parameter is called.
How this actually looks is now clarified with the following updated version of the Fraction
class.
The header file of the Fraction class now has the following structure:
// classes/frac5.hpp
#ifndef FRACTION_HPP
#define FRACTION_HPP
namespace CPPBook {
class Fraction {
private:
int numer;
int denom;
public:
/* default constructor, and one- and two-parameter constructor
*/
Fraction(int = O05 int = 1)";
// multiplication
Fraction operator * (const Fractionk&) const;
// multiplicative assignment
const Fractionk operator *= (const Fraction&);
// comparison
bool operator < (const Fraction&) const;
/* operator *
* - defined inline
al
inline Fraction Fraction::operator * (const Fractionk f) const
af
/* simply multiply numerator and denominator
* - no reducing yet
a/,
return Fraction (numer * f.numer, denom * f.denom) ;
4.5 Input and Output Using Streams 207
#Hendif //FRACTION_HPP
To enable fractions to be used with the standard stream mechanism, the I/O operators (<< or >>)
are globally overloaded:
inline
std::ostreamk operator << (std::ostreamk strm, const Fraction & f)
4
f.printOn(strm) ; // call member function for output
return strm; // return stream for chaining
Ly
inline
std::istreamk operator >> (std::istreamk strm, Fraction& f)
{
f.scanFrom(strm) ; // call member function for input
return strm; // return stream for chaining
208 4: Class Programming
As we need access to the internal members of the fraction (numer and denom) to perform the
input and output, both operator calls delegate the actual work to the corresponding member
function of the Fraction class.
The member function for outputting is a modified form of print (). This is used to pass the
additional parameter of the stream to which the fraction must be written:
class Fraction {
// output to a stream
void printOn(std::ostream&) const;
};
A member function is added to read in a fraction; its parameter is an input stream:
class Fraction {
+3
When implementing global operator functions, you must ensure that no copies of the manipulated
streams are made. This not only takes time, but also leads to errors: the stream is manipulated via
the operation (the buffer changes, the status can change to an error status, etc.). However, these
manipulations would be lost if they are done in a copy, which could then lead to inconsistencies.
If the original stream is used again in a later expression, its condition is not changed and therefore
does not correspond to the actual situation. For this reason, stream parameters and return values
must always be declared as references.
Appropriate changes are made in the dot-C file of the Fraction class.
First, the previous output function print () must be replaced with the printOn() member
function. This outputs numerator and denominator to the passed output stream (strm)
in the
form ‘numerator/denominator’ :
void Fraction: :printOn(std::ostream& strm) const
{
strm << numer << ’/’ << denom;
}
Second, the scanFrom() member function is added, which reads the fraction
of the passed input
stream (strm) by reading the numerator and denominator as integers in sequence:
4.5 Input and Output Using Streams 209
// classes/frac5scan. cpp
// **** BEGIN namespace CPPBook **%*#%# #28892 23 8H 3A AAA A BI ARR
namespace CPPBook {
/* new: scanFrom()
* - read fraction from stream strm
aif
void Fraction: :scanFrom(std::istream& strm)
{
Tae, Aas waele
// read numerator
Strip etl.
// read error?
Tf estrm) 4
return;
denom = -d;
|;
else {
numer = n;
denom = d;
}
For the input format to correspond to the output format, the ‘/’ character must appear between
the numerator and denominator. However, the operator is implemented in a way that both the ‘/’
character and the denominator are optional. Thus you could read an integral number as fraction.
The peek () stream function is used to decide whether an integral number or a complete fraction
is read. It returns the next character without reading it. If there is a ‘/’ character, the get ()
stream function then reads this character (compare with page 478).
The integer values to be read are first stored in auxiliary variables. The intention behind this
is that an object should only be changed after a successful reading (a common C++ convention).
However, several errors may occur:
e It may happen that an integer cannot be read because the format of the input buffer does not
match (for example, the next character is a letter). In this case, the stream switches to an error
condition that will be checked in the following if query:
// read error?
Tf Clestrm) =
return;
}
The subsequent reaction really depends on the situation. For example, you could exit the
program with an error message, or try to read the value again after the output of an error
message. Depending on the situation, one or the other, or even both of, these cases may not
make sense. The stream might, for example, represent a file or another process. The error
should therefore always be dealt with by the calling environment. As the stream changes into
an error status, these can also be recognized and evaluated by the calling environment.
This is exactly what happens in this case. The behavior is the same whether reading
in a fraction or an integer. The advantage of this is that the application program knows
the circumstances under which the function can be called, and can react accordingly. The
disadvantage is that if the application program does not carry out this test, the reading errors
will remain unnoticed.
Utilizing the concept of exception handling, a better mechanism for handling errors can
be established. This will be looked at in Section 4.7 on page 234, where a modified
version
of this reading-in function will be presented.
4.5 Input and Output Using Streams GAN
e An error can also appear when the format does match, e.g. the denominator can be 0. This
case will also be treated as a format error. To do this, the error flag of the streams is set»:
// denominator == 0?
if (d == 0) {
// set Failbit
strm.clear (strm.rdstate() | std::ios::failbit);
return;
}
The complete dot-C file for the Fraction class is shown below:
// classes/fracd. cpp
namespace CPPBook {
> The bit operator | links the bits together with the OR function and returns all bits that are set in both
operands.
PAs 4: Class Programming
}
else {
numer = n;
denom = d;
}
/* operator *=
7
const Fraction& Fraction::operator *= (const Fraction& f)
{
URES y= Iie x * ye
*this = *this * f;
/* operator <
ef
bool Fraction::operator < (const Fraction& f) const
x
// since the numerator cannot be negative, the following is sufficient:
return numer * f.denom < f.numer * denom;
/* new: printOn()
* - output fraction on stream strm
ih
void Fraction: :printOn(std::ostream& strm) const
{
strm << numer << ’/’ << denom;
/* new: scanFrom()
* - read fraction from stream strm
a)
void Fraction: :scanFrom(std: :istream& strm)
{
iadHe ial, Gls
4.5 Input and Output Using Streams 213
// read numerator
strm >> n;
// read error?
aor (Cl ier) af
return;
The test program now implements its output using stream mechanisms:
// classes/ftest5.
cpp
// include header files for the classes that are being used
#include "frac.hpp"
int main()
{
const CPPBook::Fraction a(7,3); // declare fraction constant a
CPPBook: :Fraction x; // declare fraction variable x
The call
std::cout << a << std::endl;
first calls the global overloaded output operator << for cout and a, defined in the Fraction
class. This operator then calls the printOn() member function for a, which finally outputs the
value of the fraction.
In addition, the program now checks to see whether the reading in of fractions was successful.
If the stream does not have an error-free status after the reading, the program exists with an
appropriate error message:
atone (SCC. Can >> x))* 4.
// input error: exit program with error status
Std: -cerre<s, ‘Frror during input, of fraction” << std:-end!;
std: :exit (EXIT_FAILURE) ;
-
At this point, the manner of the error could be examined and reacted to accordingly:
// classes/ftest5b.cpp
af i(stds:cinebad¢)) 4
// fatal input error: exit program
std::cerr << "fatal error during intput of fraction"
<< std::endl;
std: :exit (EXIT_FAILURE) ;
}
$f (etd: *cin.eot ()) {
// end of file: exit program
std::cerr << "EOF with input of fraction" << std::endl;
std: :exit (EXIT_FAILURE) ;
t
/* non-fatal error:
*- reset failbit
* - read everything up to the end of the line and try again (loops!)
at
std. -cin.clear();
while (std::cin.get(c) && c != ’\n’) {
}
std::cerr << "Error during input of fraction, try again: "
<< std::endl;
216 4: Class Programming
4.5.5 Summary
e 1/O is not part of the C++ language, but is provided by a standard class library.
e There are various stream classes. The most important of these are istream for objects that
can be read from and ostream for objects that can be written to.
e The global objects cin, cout and cerr are predefined as standard I/O channels.
e Streams have a status, which is altered by I/O operations, and which can be queried.
e The >> and << operators are typically used for I/O. These can be called in a chained fashion.
e Manipulators allow streams to be manipulated in an input or output operation.
e By globally overloading the I/O operators, the I/O concept can also apply to custom types.
e Streams should always be passed as references.
4.6 Friends and Other Types
// classes/ftest6.cpp
// include header file for the classes that are being used
#include "frac.hpp"
int main()
{
const CPPBook::Fraction a(7,3); // declare fraction constant a
CPPBook::Fraction x; // declare fraction variable x
/| read fraction x
std::cout << "enter fraction (numer/denom): ";
Tit) Catdzecin >> x4
// input error: exit program with error status
stds ceri << "Error during anput of fraction" “<< std:send1.
meturn EXIT FAILURE;
i,
Std. -coure<<. "Input Was: ” << x << std: end] ;
The subtle difference is the direct comparison of a fraction with the number 1000:
// new. instead
of while (x < CPPBook: :Fraction(1000))
while (x < 1000) {
}
This is possible because every constructor that can be called with one argument automatically
defines an automatic type conversion (implicit type conversion). This also includes constructors
with more than one parameter if there are default values for the other parameters.
The one-parameter constructor of the Fraction class therefore enables an automatic type
conversion of an int into a fraction:
namespace CPPBook {
class Fraction {
I;
}
int main()
{
CPPBook: :Fraction x;
i
J
This means that when a fraction is used as a parameter, an integer can always be used
instead. To
make this possible for other types as well, respective constructors have to be defined. It
would,
for example, be feasible to define an automatic type conversion for floating-point values:
namespace CPPBook {
class Fraction {
// constructor for automatic type conversion
//from double into CPPBook: : Fraction
Fraction (double) ;
4.6 Friends and Other Types 219
35
i;
int main()
“
CPPBook:) Fraction x;
}
An automatic type conversion can be avoided by directly implementing the operation with the
correct types. An overloaded function, in which the parameter type fits exactly, has a higher
priority than a version for which a type conversion is necessary. By doing this, you can avoid
any possible run-time disadvantages caused by the type conversion:
namespace CPPBook {
// comparison with an int
bool Fraction::operator < (int i) const
{
// because the denominator cannot be negative, the following is sufficient:
return numer < i * denom;
}
At this point, there has to be a balance between the effort of implementation and any correspond-
ing run-time advantages.
int main()
: CPPBook: :Fraction x;
i
i> (x <- CPPBook:;Fraction(3.7)) 4. SiOK
i;
}
In this case, the two different forms of object initialization during definition are noticeable. An
initialization of the form
5.8
Y"y (x) // explicit type conversion
is done using an explicit type conversion. On the other hand, an initialization of the form
MC Fee
Y y = x; // implicit type conversion
is done using an implicit type conversion.
If the constructor is declared as being explicit, the second form is therefore not possible.
There is a problem with automatic type conversion for member functions: The comparison
x < 1000 is possible, but the 1000 < x comparison is not:
namespace CPPBook {
class Fraction {
Ks
int main()
1
CPPBook: :Fraction x;
4.6 Friends and Other Types Zl
i
The comparison
x <K NOW
is interpreted as
x.operator< (1000)
Because 1000 is a parameter and there is an unambiguous type conversion, the following is
automatically produced:
x.operator< (CPPBook: :Fraction(1000))
For the comparison
1000 < x
there is no appropriate interpretation. The interpretation as
1000.operator< (x) // ERROR
is an error, because a fundamental type such as int can have no member function.
The problem is that automatic type conversions are only possible for parameters. Because
the first operand in an implementation of a operation as a member function is not a parameter, an
automatic type conversion is not legal. An explicit type conversion is still possible. For example:
CPPBook::Fraction(1000) < x // OK
could be interpreted in another way (as discussed already on page 204, in the definition of the
output operator for this class). Here, the expression could be seen as a global combination of two
operands that are both passed as parameters:
operator< (1000, x)
Such an operation must be declared as a global, rather than a member, function:
bool operator < (const CPPBook::Fractionk, const CPPBook: :Fraction&)
However, this operation is not a member function of Fraction anymore; therefore, there is no
access to private members of the CPPBook: :Fraction class. Provided that such an access is
required, auxiliary member functions of the class can also be called that carry out the actual
comparison:
222 4: Class Programming
namespace CPPBook {
class Fraction {
We
I
int main()
t
CPPBook::Fraction x;
The Fraction Class Using Friend Functions for Automatic Type Conversions
To enable global automatic type conversions, the declaration of the Fraction class should
be
changed as follows:
4.6 Friends and Other Types 223
// classes/frac6.hpp
#ifndef FRACTION_HPP
#define FRACTION_HPP
class Fraction {
private:
int numer;
int denom;
public:
/* default constructor, and one- and two-paramter constructor
Be
Braction(din te sOnmlnte— sl).
/* multiplication
* - new. global friend function, so that an automatic
* type conversion of the first operand is possible
+]
friend Fraction operator * (const Fraction&, const Fraction) ;
// multiplicative assignment
const Fractionk operator *= (const Fraction&);
/* comparison
* - new: global friend function, so that an automatic
* type conversion of the first operands is possible
We
friend bool operator < (const Fraction&, const Fraction&) ;
// output to a stream
void printOn(std::ostreamk) const;
/* operator *
* - new. global friend function
* - defined inline
PL.
inline Fraction operator * (const Fractionk a, const Fraction& b)
{
/* simply multiply numerator and denominator
* - no reducing yet
qt
return Fraction(a.numer * b.numer, a.denom * b.denom);
#endif //FRACTION_HPP
In this version, both the < and * operators are declared as global friend
functions. This is based
on the premise that if x * 1000 is possible by using an automatic type
conversion for the second
operand, the expression 1000 * x should also be possible.
4.6 Friends and Other Types 22)
This does not mean that all operators should be defined as global friend functions. Candidates
for friend functions tend to be binary operators, in which the first operand is not modified:
e Being a global friend function does not make sense for unary operators, because it involves
a function of the Fraction class, so at least one fraction should be involved in the original
call.
e For binary operators that modify the first operand (for example, assignment operators), it
makes no sense to have the first operand as a temporary object created by a type conversion.
The modified version of the header file shows that the friend functions will have to be imple-
mented differently. There are no longer implicit submitted objects (i.e. the first operand) whose
members can be accessed directly. The inline-defined multiplication in the header file therefore
has a different implementation. Instead of
/* fraction multiplication as a member function
zh
inline Fraction Fraction::operator * (const Fraction& f) const
if
return Fraction(numer * f.numer, denom * f.denom);
}
the implementation now looks like
/* fraction multiplication as a global friend function
*/
inline Fraction operator * (const Fractionk a, const Fraction& b)
{
return Fraction(a.numer * b.numer, a.denom * b.denom);
}
There are now two parameters, a and b, from which the numerator and denominator are accessed.
Access to these private data is permitted, because this is a friend function of the Fraction class.
As there are no objects from which the function can be called, this does not exist, and neither
numer nor denom can be accessed.
The implementation of the class must also be modified in the dot-C file because the definition
of the comparison operator has now become a definition of a global function:
// classes/frac6.cpp
/* operator <
* - new: global friend function
ws
226 4: Class Programming
Here, both operands are passed as parameters, whose members can only be accessed directly, via
a and b.
Note that you cannot see in the implementation of a function whether or not it is a friend of a
class. The declaration in the class structure is the decisive factor.
The solution presented here—of defining operators globally to enable an automatic type conver-
sion for the first operator—is used fairly often in commercial classes because, for most binary
operators, the first operand is not modified. However, a global definition can also be useful
for ‘normal’ member functions that do not define any operators. However, unlike for operator
functions, you can see the difference when calling ‘normal’ member functions.
Consider as an example a function that outputs the reciprocal value of a fraction. It is used as
follows when declared as a member function:
namespace CPPBook {
class Fraction {
}3
}
int main()
i!
CPPEOoks;practionex, ny;
};
}
int main()
{
CPPBook*| Fraction x, y;
One-parameter constructors define how an object of the class can be produced from an object of
a remote type. There is also the possibility of a reversed conversion, that is, the conversion of the
object into a remote type. This can be done using conversion functions.
The conversion function is declared using the operator keyword, followed by the conversion
type.
228 4: Class Programming
For example, in the Fraction class, it is possible to define a type conversion to floating-point
values. We define a function as operator double(). It returns a temporary value, to which the
fraction is converted. For example’:
namespace CPPBook {
class Fraction {
int main()
{
CPPBook: :Fraction x(1,4);
// include header file for the classes that are being used
#include "frac.hpp"
int main()
{
const CPPBook::Fraction a(7,3); // declare fraction constant a
CPPBook: :Fraction x; // declare fraction variable x
// read fraction x
std::cout << "enter fraction (numer/denom): ";
if Ci (stds tein’ Soe)
// input error: exit program with error status
std::cerr << "Error during input of fraction" << std::end1;
return EXIT_FAILURE;
t
Sstd--coun << “Input was: “<<"x << std?:endi;
7 In practice, the program is compilable on occasions. However, this indicates a fault with the compiler,
rather than bug-free code.
230 4: Class Programming
Both interpretations are possible and equally valid. This is because every kind of user-defined
automatic type conversion has the same priority (which is lower than predefined type conver-
sions such as from int to double. If there are two different equally valid interpretations, the
expression is always ambiguous.
The rules for automatic type conversion are one of the most difficult topics in C++ and are
covered in Section 10.3 on page 573.
This example shows that functions for automatic type conversion should be avoided. Cyclic type
conversions should especially never be possible automatically.
Primarily, this means that there should exist no conversion function that carries out the re-
versed conversion of a (non-explicit) one-parameter constructor. This ensures that if a class
defines a constructor for objects of another class, this class does not define the reversed construc-
tor (which, in addition, would introduce a cyclic dependency and would be even worse).
Avoiding functions that offer automatic type conversion not only minimizes the danger of
ambiguity. It also means that the object-oriented concept is adhered to more strongly. Custom
types are created so that there is a fixed set of permitted operations. Automatic type conversions
go against this concept.
This does not mean that type conversions should never be used. The point is that they should
only ever be performed by explicit function calls.
Common practice is to define member functions such as ‘asType()’ or ‘toType()’. In the
Fraction class, instead of the conversion function operator double(), the member function
toDouble() should be defined:
namespace CPPBook {
class Fraction {
// classes/sqrt2.
cpp
#include <iostream>
#include <cmath>
#include "frac.hpp"
int main()
{
CPPBook::Fraction x(1,4);
I;
means that all functions of the Y class have access to the X class. However, this does not apply
to friend functions of Y (‘my friend’s friend is not automatically my friend’). Therefore, a friend
relation is not transitive.
Declaring a class to be a friend is occasionally used for more effective implementation of
two classes that belong together (a typical example is a Matrix and a mathematical Vector
class). However, they should be part of the same file or distribution.
The friend keyword can be used to extend the abilities of automatic type conversions by
replacing a member function with a global operation, which is declared as a friend function.
The examples discussed in this section demonstrated this with the multiplication and reciprocal
functions. The use of friend in these cases was non-critical, as they were only different imple-
mentations of the operations for a class. As these functions were defined in the same file as the
class itself, it is still a ‘closed’ system, and no additional access to internal data is introduced in
any way.
In this case, using the friend keyword is a concession to the fact that a few things in C++
are programmed using loopholes (for reasons of efficiency or compatibility with C). It is a matter
of taste as to whether you make function calls using the global syntax or the syntax for member
functions.
However, problems with friend functions could be attributed to inheritance and polymor-
phism. This will be covered in Section 6.3.3 on page 399. In this respect, friend functions can be
a hindrance to the concepts of the object-oriented ideal.
Another consequence of using friend declarations is the transfer of access to the private data
of one class to another. The principle of strict data encapsulation is softened by doing so, as
functions other than those declared as member functions of global friend functions can access
the private data of a class.
However, it has been proved that, under some circumstances, a considerable run-time advan-
tage can be achieved. If, for example, we need to compute the product of a vector and a matrix,
the internal data of both classes have to be accessed. Without direct access, every access to a
member in the vector or the matrix must be executed via a member function. (However, it should
be noted that by using inline functions, you could also get this performance without making use
of the friend keyword.)
Such a use of friend therefore softens the concept of strict data encapsulation in favour
of run-time advantages. Because performance is always an important criteria for programs, in
practice, this is often seen as acceptable.
It should be made clear that using a friend function can make the checking and maintenance
of a class considerably more difficult, as access is transferred to external functions. It is always
best to use a friend declaration with a guilty conscience, so that the friend keyword is not
merely used for convenience.
In any case, it is not disputed that the friend keyword can produce nonsense. However,
everybody is likely to have had the experience of making friends with the wrong person. For this
reason, a declaration of friend classes should always be clearly indicated.
To prevent misunderstandings, it should be noted that it is not possible to use friend to
gain access to the private members of a class from the outside. The class decides whom it has
befriended in its declaration. Nobody can declare himself a friend of another.
4.6.8 Summary
e A one-parameter constructor defines the possibility of an automatic or explicit type
conver-
sion from the type of the parameter to an object of the class.
4.6 Friends and Other Types ue)
e Using the explicit keyword, an automatic type conversion by a constructor can be pre-
vented.
¢ Conversion functions provide a facility for converting an object in a class, implicitly or ex-
plicitly, into another type. They are declared with ‘operator type()’ and have a return
statement, but are declared without a return type.
e Ambiguities threaten functions for automatic type conversion. Conversion functions should
therefore be avoided, and one-parameter constructors should be used carefully.
e Using the friend keyword, single operations or entire classes can be granted access to the
private members of a class.
e Automatic type conversions for the first operand of operator functions implemented as a
member function have to be declared globally. Nevertheless, by using the friend keyword,
the implementation can access private members.
e friend is not transitive (i.e. ‘my friend’s friend is not necessarily my friend’).
234 4: Class Programming
}
This problem is not only restricted to constructors. For example, a similar problem exists for
a string class or a collection class if a character or element is accessed with the [] operator.
We can see in the function whether an incorrect index has been passed. However, there is no
possibility for handling the problems meaningfully, because the calling circumstances are not
known and there are no suitable possibilities for error handling: the only way to inform the caller
is via the return value, which, however, is expected to be the value that was accessed with
the
index operator. Thus, when calling the statement
s{i] = ’q’; // hopefully the index i is not too large
how can we write application code so that we test whether the i index is too large for s?
The basic problem is the same in both cases: error situations occur, which are found, but
cannot be resolved because the context from which the error originates is unknown. Furthermor
e,
4.7 Exception Handling for Classes 235
the error cannot be reported back to the caller, because return values are either not defined at all,
or are used for other things.
Here, the concept of exception handling helps. At any given place in the code, errors can be
detected and reported to the caller as exceptions. The error can then be intercepted and dealt with
meaningfully. If the error cannot be handled, it is not just simply ignored, but instead leads to a
controlled program exit, rather than an undefined behavior or an unexpected program abortion.
#ifndef FRACTION_HPP
#define FRACTION_HPP
namespace CPPBook {
class Fraction {
private:
int numer;
int denom;
public:
/* new: error class
ff
class DenomIsZero {
T;
/* multiplication
* - global friend function, so that an automatic
* type conversion of the first operand is possible
+f
friend Fraction operator * (const Fraction&, const Fraction&) ;
// multiplicative assignment
const Fraction& operator *= (const Fraction&) ;
/* comparison
* - global friend function, so that an automatic
* type conversion of the first operand is possible
df
friend bool operator < (const Fraction&, const Fraction&) ;
/* operator *
* - global friend function
* - defined inline
Hi
inline Fraction operator * (const Fraction& a, const Fraction& b)
{
/* simply multiply denomiator and numerator
* - no reducing yet
#7,
return Fraction(a.numer * b.numer, a.denom * b.denom) ;
#endif //FRACTION_HPP
public:
class DenomIsZero {
1
‘>
We are dealing here with the shortest declaration possible for classes: an empty class. The only
role of an object ofthis class is to exist. No data or operations are required.
In the implementation, if the denominator is zero, a corresponding error object is now created:
// classes/frac8. cpp
// KKK BEGIN namespace CPPBook 28 282 2 2 2 2 sk 2s2S28OS2 2k2k 8 2898 OR2828 OS 8 OKOKOKOK
namespace CPPBook {
Fraction::Fraction(int n, int d)
{
/* initialize numerator and denominator as passed
* - 0 is not allowed as a denominator
* - move negative sign from the denominator to the numerator
ay
if (d == 0) {
// new: throw exception with error object for 0 as denominator
throw DenomIsZero() ;
}
alee, (Gch <<, Toph
numer = -n;
denom i | Q
}
else {
numer
denom ih p
a
/* operator *=
ef,
const Fraction& Fraction::operator *= (const Fraction& f)
{
[PetaVea > Ox at y’
*this = *this * f;
/* operator <
* - global friend function
#/
bool operator < (const Fraction& a, const Fraction& b)
a
// since the numerator cannot be negative, the following is sufficient:
return a.numer * b.denom < b.numer * a.denom;
4.7 Exception Handling for Classes 239)
/* printOn
* - output fraction on stream strm
/* scanFrom
* - read fraction from stream strm
// read numerator
Strnao> el:
// read error?
if) (!sstrm)et
return;
i fa (de<e0) sit
numer = -n;
denom = -d;
}
else {
numer = n;
denom = d;
}
The code for the constructor is modified here accordingly. If the error occurs, an error object is
created and is thrown into the environment of the program:
Fraction: :Fraction(int n, int d)
{
/* initialize numerator and denominator as passed
* - 0 is not allowed as a denominator
*- move negative sign from the denominator to the numerator
e/,
if (d == 0) {
// new: throw exception with an error object for 0 as denominator
throw DenomIsZero();
}
At run time, the throw statement becomes equivalent to a sequence of return statements
that are
called in all nested blocks and functions in which the program is situated at that time. All
scopes
of the program are terminated immediately and the error propagated up until it is either
caught
and handled or the program quits.
Note that this is not a jump to the next superior catch that deals with the error, but
rather an
‘ordered retreat’. Destructors are called for the local objects created within all
blocks. Objects
explicitly created with new, however, remain valid and, if necessary,
must be removed by the
4.7 Exception Handling for Classes 241
catch area. This process is also known as stack unwinding. The program stack is unwound until
an appropriate exception is defined.
Even if the exception is not handled and leads to a program exit, the program is not just simply
aborted, but is left in an orderly manner. In contrast to exit (), all destructors of local objects
are called (exit () only calls destructors of static objects). As a local object can represent an
opened file or a running database query (which might be flushed and closed by the destructor),
this is an important distinction.
Exception Handling
The handling of the exception occurs in the application program that directly or indirectly caused
the error. The following program shows how this may look like:
// classes/ftest8.cpp
// include header files for the classes that are being used
#include "frac.hpp"
int main()
{
CPPBook::Fraction x; // fraction variable
return EXIT_FAILURE;
}
x = CPPBook: :Fraction(n,d) ;
std:scout’ << “input was: "<<! x) <<"std:irendl;
}
catch (const CPPBook: :Fraction: :DenomIsZerok) {
/* exit program with an appropriate error message
¥/.
std::cerr << "input error: numerator can not be zero"
<< std::endl;
return EXIT_FAILURE;
For the area enclosed by try, special error handling is installed, defined by the following catch
statement:
tryet
soe fay, Gls
std::cout << "numerator: ";
x = CPPBook: :Fraction(n,d);
std::cout << "input was: " << x << std::endl;
d;
catch (const CPPBook: :Fraction::DenomIsZerok) {
i
If any given exception occurs in the try block, the block is exited immediately. If this is an
exception of the Fraction: :DenomIsZero type, then the statements in the necessary catch
block are executed. After the statements in the catch block are processed, the program continues
at the next statement after the catch block (thus the try block, where the exception was thrown,
in not re-entered).
If, in the constructor, zero is given as the denominator d, then the statement
x = CPPBook: :Fraction(n,d);
Causes an exception to be thrown. As a consequence, both the constructor and the try block
are
left immediately. The following output statement inside the try block
std::cout << "input was: " << x << std::endl;
is therefore not executed.
4.7 Exception Handling for Classes 243
With every other exception, due to lack of an appropriate catch clause, even the main()
function is exited.
The exception in the catch block is passed as a constant reference, for the same reason as
references are used for function parameters. By not using references, copies of the passed (ex-
ception) object are made. By using a reference, the unnecessary copying of the exception object
is avoided.
An interesting question is whether we should throw an exception inside the Fraction class, if
we get zero as the denominator when reading a fraction from a stream:
if (d == 0) {
/| throw exception with an error object for 0 as denominator
throw DenomIsZero() ;
}
Instead of setting an I/O flag, exception handling would also be used in this case. However,
I/O is a good example of the difference between exceptions and errors. Input errors are very
common and therefore, by definition, are not exceptions. It is therefore normal to set appropriate
status flags when incorrectly formatted streams are encountered (see also the implementation
on page 208ff.). In this way, we can distinguish other input errors from the specific error of
inputting a zero as the denominator. However, this complicates the interface. The boundaries
between exceptions and errors are fuzzy, and, in this respect, this kind of decision is always a
question of design.
throws an exception of the const char* type, which we then can process as follows:
// catch string exceptions
catch (const char* s) {
// print string as error message
std::cerr << s << std::endl;
}
If exception classes have members, these are the attributes of the exception. In this way, excep-
tions and errors can be parametrized. Constructors may be provided to initialize these members.
If dynamic members are used, a destructor can also be defined. It is called after an exception
handling, when the exception object gets destroyed.
244 4: Class Programming
The use of an invalid index in an array is a classic example of the use of parameters in
exception objects. If a string is accessed with an invalid index, the value of the index is usually
of interest and should be passed to the caller. An example of this is presented and explained in
Section 6.2.7 on page 387.
diate Gh
newString = new std::string("cup"); // explicitly
create string
// and modify
M;
catcheG. aa) rH
/* for any exception, release the explicitly created string
* and rethrow the exception to the caller of this function
* for additional handling
WE
delete newString;
throw;
.
}
The naked throw statement (i.e. ‘throw; ’, without the detail of a class) is responsible
for throw-
ing the error back into the program, so that it is handled from the caller (or outer block).
This sort of ‘rethrow’ is also required to implement an auxiliary function
for the general
handling of various exceptions. This is discussed in Section 3.6.6 on page 101.
4.7 Exception Handling for Classes 245
Unexpected Exceptions
The exception specification only describes what exceptions can occur from the caller’s point
of view. Internally, other exceptions can occur and be dealt with. However, if an excep-
tion occurs inside the function that is neither handled nor contained in the exception specifi-
cation, the std: :unexpected() function is called. This function is predefined so that it calls
std: :terminate() (which usually calls std: : abort (), see Section 3.6.5 on page 100).
An alternative function for handling unexpected exceptions can be defined using std::
set_unexpected(). The function cannot have any parameters or a return value. The currently
defined function for unexpected exceptions is returned when calling std: :set_unexpected().
If an exception specification therefore covers the class std: : bad_exception, every exception
that does not belong to any of the listed types is replaced within the function by an exception of
the type std: :bad_exception.
Again, the Fraction class can be used as an example of an exception class hierarchy. Various
errors can occur when using fractions. In order to be able to deal with each, a corresponding
hierarchy is defined (see Figure 4.5).
Fraction: :FractionError
Among the common types of FractionError, we have the special cases DenomIsZero and
ReadError. Thus three classes are declared in which the two special cases are derived from the
common one.
In order to do this, the declaration of the Fraction class has the following structure:
// classes/frac10.hpp
class Fraction {
private:
int numer;
int denom;
4.7 Exception Handling for Classes 247
public:
/* error classes:
* new. : common-case
- l
class ith two deri
with derived classes
a/
class FractionError {
$5
class DenomIsZero: public FractionError {
5
class ReadError : public FractionError {
I;
Fs;
In the case of an error, the corresponding special exception objects are thrown in the dot-C file:
// classes/frac10.cpp
denom = -d;
is
else {
numer = n;
denom = d;
A;
/* scanFrom
* - read fraction from stream strm
// read numerator
strm >> n;
// read error?
if (! strm) {
// throw exception with error object for read error
throw ReadError();
THAIS
numer = -n;
denom = -d;
I;
else {
numer = n;
denom = d;
To handle any kind of error of the Fraction class within the application program in main(), we
use a Catch clause that refers to the common base class:
// classes/ftest10.cpp
int main()
{
try {
CPPBook: :Fraction x;
% = readPraction();
}
catch (const CPPBook::Fraction::FractionError&) {
// exitmain() with error message and error status
std::cerr << "Exception through error in class fraction"
<<Sstd: end):
return EXIT_FAILURE;
}
}
In the special event of reading a fraction, an input error caused by scanFrom() can be treated
individually, so that a new read attempt is forced:
// classes/ftest10read.
cpp
do {
error = false; // no error
yet
This has the advantage that different errors can be handled with a single catch statement.
Furthermore, not all exceptions need be declared in the exception specification of a function that
can return different exceptions. So, instead of
void doSomething() throw (NotANumber, ZeroDivision, Overflow);
the statement of the common base class is sufficient:
void doSomething() throw (MathematicalError) ;
Error Messages
A common basic class for all exception classes should define a member for an error message,
which then belongs to all error objects and can be processed if the error is not handled in another,
more individual, way:
namespace CPPBook {
class Error {
public:
std::string message; // default error message
i;
}
The error message could, for example, then be written to the standard error channel in main():
int main()
{
Cry
is
}
In the standard exceptions, what () takes on this role (see Section 3.6.4 on page 99).
Zoe 4: Class Programming
of an exception. Thus, if good performance is a design goal you can’t provide perfect exception
handling in all cases.
The atomic guarantee is sometimes even impossible to give. For example, if you want to
provide a function that removes an element from a collection and returns this element, it might
happen that the copying of the return element throws an exception. Thus, the operation fails but
the number of elements was decremented. For this reason vectors and other standard containers
provide no operation that removes and returns an element (see also Section 7.3.1 on page 438).
Note that all these guarantees are based on the requirement that destructors never throw.
When destructors throw you can’t guarantee anything.
4.7.11 Summary
or.
mgt wei ilip
-
-
inn, GOL
)
Inheritance and Polymorphism
After looking at programming with classes and data encapsulation, this chapter introduces two
other essential features of object-oriented programming: inheritance and polymorphism.
Inheritance allows properties of a class to be derived from another class. This means that new
classes need only implement supplementary aspects. Properties that have been implemented in
another class can be derived, and do not need to be implemented again. As well as reducing the
size of the code, this also helps to ensure consistency because identical properties do not appear
in several places, and therefore only need to be checked or modified once.
Polymorphism makes it possible to work with common terms (a way of abstraction used in
everyday life)'. Various objects can be combined and managed under a common term, without
their different properties being lost.
Inheritance is the typical way polymorphism is implemented is C++. A class has to be defined
for the common term, from which other classes are derived. If an object is used as its common
term, it can be accessed with the type of the class that has been defined for the common term.
However, the fact that the object has a special type is not lost. At run time, a function call
automatically determines the actual class of an object and the corresponding function for this
class is called automatically.
There are various terms used for inheritance, partly because every object-oriented programming
language introduces its own vocabulary. In the following, we will only use C++ terms, but will
also briefly introduce other terms.
If a class takes on the properties of another class, this process is called inheritance. A new
class inherits or derives the properties from an existing class.
' Generic term is probably a better phrase than common term. However, as generic has a Special meaning
in C++, I decided to use common term here.
256 5: Inheritance and Polymorphism
The class from which properties are inherited is called the base class (also superclass, parent
class). The class that inherits is called the derived class (also subclass, child class).
Because a derived class can also be a base class itself, this may create a whole class hierarchy,
as shown in Figure 5.1.
The class Vehicle describes the properties of vehicles. The class Car inherits these prop-
erties and extends them. The class SportsCar inherits the properties from Car (therefore the
extended properties of Vehicle) and extends them further still. Other branches can be intro-
duced accordingly. The result is a class hierarchy for vehicles, in which Vehicle is the common
term.
Inheritance is indicated with the so-called is-a relationship: A sports car is a car. A car is a
vehicle.
As shown in the figure, UML uses an isosceles triangle to denote inheritance, with the top
pointing to the base class.
Multiple inheritance is very useful, but can lead to complications. For this reason, we will
start with single inheritance and will delay looking at the special features of multiple inheritance
until Section 5.4 on page 329.
258 5: Inheritance and Polymorphism
Before the Fraction class can be derived as a base class, the implementation introduced in
Chapter 4 must be slightly modified. Previous versions of the class Fraction did not support
inheritance, and therefore, without these changes, derivation would not be possible.
We need to make two remarks at this point:
e Ifthe previous versions of the Fraction class were part of a closed class library, they should,
of course, already contain the modifications if this class is provided for inheritance.
e Because of the problems mentioned above, further modifications have to be carried out. In
this respect, this version is not yet an effective and suitable implementation of the Fraction
class.
A version of the Fraction class that could also be used as a base class for other classes requires
the following modified class declaration:
// inherit/frac91.hpp
#ifndef FRACTION_HPP
#define FRACTION_HPP
namespace CPPBook {
class Fraction {
protected:
int numer;
int denom;
5.1 Single Inheritance 250
public:
/* error class
ie
class DenomIsZero {
dae
/* multiplication
* - global friend function so that an automatic
* type conversion of the first operand is possible
*/
friend Fraction operator * (const Fraction&, const Fraction&);
// multiplicative assignment
const Fraction& operator *= (const Fraction&) ;
/* comparison
* - global friend function so that an automatic
* type conversion of the first operand is possible
*/
friend bool operator < (const Fraction&, const Fraction&) ;
/* operator *
* - global friend function
* - inline defined
tf
inline Fraction operator * (const Fraction& a, const Fraction& b)
ic
/* simply multiply numerator and denominator
* _ this saves time
260 5: Inheritance and Polymorphism
sil
return Fraction(a.numer * b.numer, a.denom * b.denom);
#endif // FRACTION_HPP
The decisive difference from the previous declaration of the Fraction class is that now the
protected access keyword, instead of private, precedes the members numer and denom:
namespace CPPBook {
class Fraction {
protected:
int numer;
int denom;
5.1 Single Inheritance 261
Using protected, access to members by the user of the class is prevented in the same way as
with private. However, derived classes are granted access. With private, even derived classes
would be refused access.
Because inheritance is a basic concept of C++, the class designer should support this by
using protected, instead of private, for access of members. Because we need access to
these members in order to reduce them, a derivation with the semantics we’d like to have would
otherwise not be possible. Without having access to the internal values of the fraction, we would
only be able to add new properties that can be implemented using the public interface of the base
class.
Now that the Fraction class has been prepared for its role as a base class, we can derive the
RFraction class (see Figure 5.2). The name ‘RFraction’ stands for ‘reducible fraction’. This
means that the objects of this class are fractions that, in principle at least, can be reduced.
Fraction
New Operations
Reimplemented Operations
In addition to the new operations, some of the existing operations must be redefined for use with
the RFraction class:
e The constructors must be redefined, because, in principle, they are not inherited.
e The operator *= and the input function scanFrom() must be redefined, because their imple-
mentation cannot be derived from the class Fraction and must be overridden (the reasons
for this will be explained later).
New Members
In addition to the members numer and denon, which are inherited from the base class Fraction,
the new Boolean member reducible is introduced. This determines whether the fraction is
reducible, or whether it has already been reduced. Of course, this could be recalculated whenever
needed, but having the information available in a member saves time because the property is only
calculated when there is a change in the value of the fraction.
UML Notation
Figure 5.3 shows the UML notation of RFraction class and the relevant parts of the base class
Fraction. Note the hash sign in front of all data members, which signifies protected member
access.
# denom: int
+ operator * () RFraction()
+ operator < () isReducible
+ printOn() reduce()
+ toDouble() | + ged()
operator *=
scanFrom ()
As usual, the class declaration is found in a separate header file, which has the following struc-
ture:
// inherit/rfrac1.hpp
#ifndef RFRACTION_HPP
#define RFRACTION_HPP
/* class RFraction
* - derived from Fraction
* - access to inherited members limited (public remains public)
oy
class RFraction : public Fraction {
protected:
bool reducible; // true: fraction is reducible
public:
/* default constructor, and one- and two-parameter constructor
* - parameters are passed to the Fraction constructor
“ffi
RFraction(int mn ="0, int d°="1)"Fraction(n,d) {
reducible = (gcd() > 1);
// reduce fraction
void reduce();
264 5: Inheritance and Polymorphism
// test reducililty
bool isReducible() const {
return reducible;
} // oh ok oh ok END namespace CPPBook 2 26 ooo ok 2k2K oh of okokob okOK KK OK okokokok ok okok KKK RK KK
#endif // RFRACTION_HPP
First, the header file of the base class must be included because the declaration of the derived
class uses it:
#include "frac.hpp"
is
A derived class is declared with a colon, followed by an optional access keyword and the name
of the base class. It does not matter whether or not the base class itself is a derived class.
The syntax for the declaration of a derived class reads as follows?:
The optional access keyword indicates whether and to what extent access to inherited members
is restricted:
e With public, there are no additional limitations. Public members of the base class are also
public in the derived class, protected members remain protected and private members
remain private.
e With protected, restrictions apply. All public members of the base class become members
that can no longer be accessed by the user; however, derived classes do have access. There-
fore, public becomes protected, protected remains protected and private remains
private.
e With private, all members of the base class become private members. Neither the user nor
derived classes have access to these members.
In other words, the access keyword for base classes specifies the minimum encapsulation derived
members get from the perspective of the derived class.
* As we will see in Section 5.4.1 on page 332, multiple base classes can also be specified.
5.1 Single Inheritance 265
The default value for the optional access keyword is private, but you should always explic-
itly specify it in order to improve readability (many compilers will output a warning message if
no access keyword is entered for the base class).
The RFraction class does not limit access to derived members of the Fraction class any
further. This means that the inherited public member functions such as printOn(), and the
multiplication and comparison operators, can also be called from RFraction:
namespace CPPBook {
class Fraction {
public:
void printOn(std::ostream&) const;
¥3
I;
}
void foo()
{
CPPBook::RFraction rf;
Constructors play a special role in inheritance. In effect, they cannot be inherited. All construc-
tors for objects of a derived class must be redefined. As for any class, if no constructors are
defined, only the default constructor and the copy constructor are provided.
Although constructors of base classes are not inherited, they do play a part in the initialization
of an object. Constructors are called in a top-down fashion. When an object of a derived class
is created, the constructor of the base class is called, which only initializes the part of the object
that is derived from the base class. Afterwards, the constructor of the derived class is called,
which initializes any newly added members. It can also be used to modify initializations of
the constructor of the base class, which might no longer be useful from the point of view of
the derived class. In our example, during the creation of an object of the RFraction class, a
Fraction constructor is called first, followed by an RFraction constructor.
Note that the arguments for the construction of the object are not automatically passed to the
constructor of the base class. Provided that nothing else is specified, the default constructor of
266 5: Inheritance and Polymorphism
the base class is called instead. This is because the parameters for the constructor of the derived
class may have very different semantics from those of the base class. There could also be a
different number of parameters, or different types.
Initialization lists also play a role here. They offer the possibility of passing arguments to the
constructor of the base class. These arguments might be the parameters of the constructor of the
derived class, or any other data.
The following demonstrates the declaration of the first constructor of the RFraction class:
class RFraction : public Fraction {
public:
RFraction(int n = 0, int d = 1): Fraction(n,d) {
reducible = (gcd() > 1);
}5
The constructor is declared with the same parameters and default arguments that are used in the
Fraction class. Therefore, a numerator and a denominator can be passed as an integer. If this
is not the case, 0 and 1 are used as default values. These parameters are then passed on to the
constructor of the base class Fraction, where they are used for the initialization of numer and
denom.
The statements of the RFract ion constructor are executed after the call to the Fraction con-
structor. Thus numer and denom are already initialized when the statements of the RFraction
Constructor are executed. In this case, we use the values of numer and denom to process the
initial value of reducible.
e The initializer list of the constructor of RFraction defines that the int/int constructor
of
the Fraction class has to be called with the parameters (or default values) passed to it:
5.1 Single Inheritance 267
yt;
e Therefore, the int/int constructor of the Fraction class is called with the values 91 and 39,
which initializes the part of the object inherited from Fraction:
e Finally, the statements of the RFraction constructor are executed, which initialize the mem-
ber reducible by acall of gcd():
reducible: true
The fact that the statements in the base constructor are called before those in the constructor
of the derived class is important. Only this way, the derived class can evaluate the initialized
members of the base class. This also gives the opportunity to modify the initializations of the
base class. However, note that you should never use this ability to change the semantic meaning
of inherited members (see Section 5.5 on page 344 for some design pitfalls regarding this).
The whole mechanism is recursive. If a base class is a derived class itself, the constructor of
its base class is called first. Again, the initializer list of the base class indicates what parameters
are passed to the constructor of its base class. Thus, an initializer list can only submit arguments
to its direct base class.
If constructors have to be called for base classes as well as for members, then the constructors
of the base classes are called first.
Destructors (the counterpart to the constructors), on the other hand, are called bottom-up. The
statements of the destructor of a derived class are executed first, and then those of the base class.
268 5: Inheritance and Polymorphism
If destructors have to be called for base classes and also for data members, then the destructors
for the members are called first.
Thus destructors are always called in the opposite order to that of constructors.
// 8 OKoo BEGIN namespace CPPBook 38 ER2882B8 282g282g2S2g2kBs2k9K2s3kok2 oeok2 ak2 ie2sig2 akokokok
namespace CPPBook {
/* ecd@)
* - greatest common divisor of numerator and denominator
Hal
unsigned RFraction::gcd() const
{
if (numer == 0) {
return denom;
/* reduce()
gi!
5.1 Single Inheritance 269
numer /= divisor;
denom /= divisor;
/* operator *=
* - reimplemented
ae
const RFraction& RFraction: :operator*= (const RFraction& f)
{
// as with the base class:
numer *= f.numer;
denom *= f.denom;
// still reduced?
LimGlmeducipile) my
reducible = (gcd() > 1);
}
return *this;
}
/* scanFrom()
7
void RFraction::scanFrom(std::istream& strm)
{
Fraction::scanFrom(strm); // call scanFrom() ofthe base class
First, the newly introduced member function for the calculation of the GCD is implemented.
If the numerator is 0, the GCD is the denominator (the fraction : therefore has the GCD 7).
Otherwise, the first number that divides the numerator and the denominator without remainder
is searched in a loop (the modulo operator % returns the remainder after the division). If no
other divisor is found, the loop will end with 1 as the GCD. For the initial value of the loop,
the standard function std: :min(), defined in <algorithm>, which returns the minimum of the
two values, and the standard function std: :abs(), defined in <cstdlib>, which returns the
absolute value of an integer, are used?.
The other new function, reduce (), divides the numerator and denominator by the calculated
GCD, as long as the fraction is reducible.
Derived member functions and operators may have to be reimplemented if their implementation
in the base class is no longer valid. In this example, this is the case for the *= operator and
the input function scanFrom(). The process of replacing an implementation of a base class by
another in a derived class is called overriding.
In both cases, without a reimplementation, the RFraction may get an inconsistent status
after these operations. This is because the two functions manipulate numer and denom. Their
values, however, determine the value of reducible. Because their implementation in the base
class does not change reducible (reducible is not yet known there), this member may not
have a valid value.
In fact, using the *= operator, the RFraction for which this operation is called may change
from a non-reducible to a reducible status. This is, for example, the case if the non-reducible
object g is multiplied by 2. Without a reimplementation, reducible would remain false, even
though the value has changed to 24.
With scanFrom(), the value of the RFraction is modified so that it gets an arbitrary new
value. Without overriding the implementation, no adjustment of the reducible member would
be carried out. This could also lead to reducible not being set correctly. The information about
whether the RFraction is reducible, therefore, has to be redetermined every time a new value is
read.
With a reimplementation, there are two options:
e Acomplete reimplementation.
e A call of the implementation of the base class, with corrections.
As usual, a decision has to be made between consistency and possible run-time advantages. A
complete reimplementation, as has been done with the *= operator, saves running time, but may
need to be altered if there are changes in the base class. With the second option, as used with
scanFrom(), additional procedural steps need only be reimplemented for the derived classes.
> There are, undoubtedly, better algorithms for the calculation of a GCD.
5.1 Single Inheritance 20
/| header files
#include <iostream>
#include "rfrac.hpp"
int main()
{
// declare reducible fraction
CPPBook: :RFraction x(91,39);
// outputx
SLO COUbE<axs
std::cout << (x.isReducible() ? " (reducible)"
: " (mon reducible)") << std::endl;
// reduce x
x.reduce();
// outputx
std::cout << x;
std::cout << (x.isReducible() ? " (reducible)"
" (mon reducible)") << std::endl;
// multiply x by 3
x *= 3;
// outputx
Sitece
COU ane
std::cout << (x.isReducible() ? " (reducible)"
: " (non reducible)") << std::endl;
First, the object x of the RFraction class is created using the declaration
CPPBook::RFraction x(91,39);
we need to note the following: although the operator function is only defined for a Fraction
as its second operand, an RFraction can also be passed. This example shows that an object of
type RFraction can be used in place of a Fraction object.
This is a direct consequence of the is-a relationship. Because the RFraction class is derived
from Fraction, an RFraction is a Fraction, and can be used as such at any time. By means
of inheritance, in principle, an automatic type conversion of an object of a derived class
to an
object of its base class is defined. With the conversion, the object is only processed with
the
properties that objects of the base class possess. In particular, it only has the data members
of the
base class. The members that were added due to inheritance are simply left out, or are ignored.
In this case, this is not a problem, as only numerators and denominators are used
in the
output of an RFraction. There are, however, cases in which this automatic type conversion
can
be problematic. We will look at relevant pitfalls in Section 5.5 on page 344.
5.1 Single Inheritance 213
The multiplication inherited from the Fraction class still returns a Fraction. However, a
Fraction is not an RFraction (the is-a relationship only applies the other way around). There-
fore, a Fraction cannot be assigned to an RFraction.
One way to eliminate this problem is to reimplement the multiplication operator. The problem
can, however, also be solved by defining a type conversion from a Fraction to an RFraction,
which is made possible by implementing another constructor:
class RFraction : public Fraction {
public:
/* constructor for the type conversion of Fraction to RFraction
* - parameter is passed to the copy constructor of Fraction
zi,
RFraction (const Fraction& f) : Fraction(f) {
reducible = (gcd() > 1);
};
As usual, when creating an RFraction, a constructor of the Fraction class is called first,
followed by the statements of the constructor of the RFraction class. Because the Fraction
constructor is called for the Fraction that was passed to the RFraction constructor, the copy
constructor of the Fraction base class is called.
Because the constructor has only one parameter, it defines an automatic type conversion of
Fraction to RFraction. As a consequence, a Fraction, which is the result of a multiplication,
can be assigned to an RFraction:
CPPBook: :RFraction rf;
Without this implicit type conversion, the parameter of the *= operator must be declared as
Fraction:
class RFraction : public Fraction {
};
It is rarely useful to have a constructor that can convert an object of the base class into an object
of a derived class. This should only be available if there is a reasonable implicit initialization
for all additional attributes. Regarding this, this is a special example because whether or not the
fraction is reducible depends directly on the members that are already available to Fractions
(i.e. numerator and denominator)*.
Often, attributes are added whose values are independent of previous attributes. If we had
introduced, for example, a class ColoredFraction, where a fraction is assigned a certain color
as an additional attribute, a type conversion from a Fraction would not be very useful. The
color of a fraction is a property that has no relation to previous properties. One can actually
adopt a default color. However, it is usually more useful to disallow conversions of a Fraction
to a ColoredFraction, so that, in a program that uses colored fractions, uncolored fractions are
not used as colored fractions by mistake or accident.
Note that each automatic type conversion increases the danger of an unintentional behavior,
because the compiler can no longer recognize whether an object is incorrectly used as an object
of another class or not. Again, a function for explicit type conversion can be useful here. A
function might, for example, require a Fraction and a color as its arguments. Ultimately, it is
a design decision, which you have to balance by taking into account the risk of an unintentional
type conversion and the advantage of a simplified type conversion.
5.1.8 Summary
e Classes can be connected to one another by inheritance.
e A derived class takes on (inherits or derives) all properties of the base class and typically
supplements these with new properties.
e Objects of derived classes include the members of the base class and all newly added
mem-
bers.
e The characteristic of inheritance is the is-a relationship: An object of a derived
class is an
object of the base class (with additional properties).
e An object of a derived class can be used as an object of the base class at any time. It then
reduces itself to the properties of the base class.
e Constructors are not inherited, but are called top-down. Destructors are called bottom-up.
e Using initializer lists, parameters can be passed to the constructors of a base class. Without
an initializer list the default constructor of the base class is called.
e You can, and sometimes have to, override derived operations.
276 5: Inheritance and Polymorphism
public:
// multiplicative assignment
const Fraction& operator *= (const Fraction&);
PS
In the derived class RFraction, we have:
class RFraction : public Fraction {
public:
// multiplicative assignment (reimplemented)
const RFraction& operator*= (const RFraction&) ;
1p
However, this reimplementation can cause problems, which should be identified and,
if possible,
eliminated.
5.2 Virtual Functions DAY
// inherit/rftest2. cpp
// header files
#include <iostream>
vinclude “rirac.hpp"
#include "frac.hpp"
int main()
{
// declare RFraction
CPPBook: :RFraction x(7,3);
// output x
Std 1Coute<qex.
Sstd-<cout << (x isReducible() ? “(reducible)”
" (non reducible)") << std::endl;
// output x
Stine Ole 50°
std::cout << (x.isReducible() ? " (reducible)"
‘(non reducible)" )e<< std-- endl:
}
Two statements of the program are problematic. In both cases, we have that objects of a derived
class can also be used as objects of the base class.
278 5: Inheritance and Polymorphism
In the first case, the problem arises from the manipulation of the RFraction x being carried out
via a pointer that is declared as a pointer to objects of the base class:
CPPBook::RFraction x(7,3);
CPPBook::Fraction* xp = &x;
CPPBook::Fraction f(3,7);
The fact that a pointer of the type Fraction* points to an RFraction is absolutely fine. It is a
consequence of the is-a relationship, which signifies inheritance:
e AnRFraction is a Fraction.
e AnRFraction can therefore be used as a Fraction at any time.
In this case, the RFraction x is used as a Fraction, to which Xp points.
If, however, the x is now manipulated via the fraction pointer xp, the member function of the
class Fraction, instead of the class RFraction, is called. This is because, during compilation,
the compiler binds this call as call of the *= operation for Fractions, because it is a pointer to
a Fraction. This implementation of *=, however, only knows the members numer and denom
and does not adapt reducible.
We are therefore faced with an inconsistency in the existing program. The fraction 7/3 is
multiplied by 3/7, but the reducible member keeps its old value, even though it is incorrect.
This becomes clear with the following output statement:
21/21 (non reducible)
A similar problem, although not so easy to detect, is the input of the RFraction x:
CPPBook: :RFraction x(7,3);
This is also an application of the is-a relationship. The RFraction x is a fraction and can
therefore be used for the initialization of the fraction reference f£. Because f is a second name
for a Fraction, the member function scanFrom() of the class Fraction is called to read the
new value of f. This reads numer and denon, but does not set the member reducible, because
it is not known to Fractions. The RFraction thus maintains the old status in the member
reducible, despite the fact that a new (possibly reducible) value has been input.
Due to the usage of the type Fraction, the reimplemented function for reading objects of
the class RFraction is not called when using the >> operator. Only a direct call of scanFrom()
would work.
To keep the information of the original type of objects when they are being manipulated with
function calls, the calls must not be bound statically. Instead, code has to get generated that, at
run time, the correct function is called, depending on the actual type of the object. This behavior
is described as dynamic binding or late binding?.
Dynamic binding is possible in C++. To do this, the virtual keyword is used. If a member
function is declared as a virtual function, then, with the use of pointers and references, at run
time it is decided what function is actually called. In this way, the appropriate function is called.
> Rather confusingly, the term dynamic binding is also used to describe the mechanism of shared libraries.
However, the two concepts are not related.
280 5: Inheritance and Polymorphism
Thus the functions that we reimplemented in the derived class RFract ion need to be declared
in the base class Fraction as virtual. As a base class usually does not which functions are
reimplemented by derived classes, in general all functions of the base class that can be overridden
by derived classes should be declared as virtual. Only by doing this is a class properly prepared
for inheritance.
If the functions of the base class are not declared as virtual, the problems that were described
earlier arise. For this reason, with inheritance, one should stick to the following rules:
e Non-virtual functions should not be overridden.
e If this is necessary for the implementation of a derived class, it should not be derived.
The declaration of the base class Fraction therefore reads as follows:
// inherit/frac92.hpp
#ifndef FRACTION_HPP
#define FRACTION_HPP
// OBE BEGIN namespace CPPBook 28 IE282S3 28as2S2k2s3 2s2 2k2 oe2kfe2 2g2 2s 2 2k2kok0kok2kok2kok
namespace CPPBook {
class Fraction {
protected:
int numer;
int denom;
public:
/* error class
+7
class DenomIsZero {
13
/* multiplication
* - global friend function, so that an automatic
* type conversion of the first operand is possible
Hs
5.2 Virtual Functions 281
/* multiplicative assignment
* - new: virtual
7
virtual const Fractionk operator *= (const Fraction&);
/* comparison
* - global friend function, so that an automatic
* type conversion of the first operand is possible
*/
friend bool operator < (const Fraction&, const Fraction&);
/* operator *
* - global friend function
* - inline defined
*/
inline Fraction operator * (const Fraction& a, const Fraction& b)
{
/* simply multiply numerator and denominator
* - this saves time
“Gh
return Fraction(a.numer * b.numer, a.denom * b.denom);
#endif //FRACTION_HPP
All functions that can be reimplemented in derived classes, for any reason, can be declared
as virtual. Only constructors and friend functions, which, in principle, cannot be virtual, are
excluded from this rule.
Now when reading a reducible fraction, everything works as expected. The scanFrom() imple-
mentation of the derived class RFraction is used, because the operator>>() function knows
that an RFraction is actually being read:
namespace CPPBook {
class Fraction {
public:
virtual void scanFrom(std: :istream&) ;
in
inline
std::istream& operator >> (std::istream& strm, Fraction& f)
{
f.scanFrom(strm); // because of virtual: correct scanFrom() is called
return strm; // return stream for chaining
5.2 Virtual Functions 283
public:
virtual void scanFrom(std::istream&) ;
i
}
int main()
1
CPPBook::RFraction x (7,3);
}
If the overloaded operator function >> for fractions calls the member function scanFrom() for
its parameter f, as f is a reference and scanFrom() is declared as being virtual, at run time, it
is decided what class f actually belongs to. If it is an object of a derived class, the implementation
of scanFrom() of this class is automatically called, as long as it has one.
The function scanFrom() does not necessarily need to be declared as being virtual in the
derived class. Because the function in the base class is virtual, it is automatically virtual in the
derived class as well.
Note that, by declaring a function as being virtual, a call to it can last considerably longer. For
pointers and references, instead of a direct function call, code has to be generated to determine
what function has to be called at run time.
Particularly striking is the difference with inline functions. Because, in the case of pointers
and references, the function that is called is decided at run time, the function call cannot be
replaced by the statement in the function body during compilation. Thus the run-time advantage
of inline functions does not apply to pointers and references if virtual functions are used.
For this reason, in practice, it can be useful to equip classes with just a few, or even no, virtual
functions. The classes are then not very suitable for inheritance. However, the performance is
significantly better. This is, again, a design decision.
284 5: Inheritance and Polymorphism
public:
virtual const Fraction& operator *= (const Fraction&) ;
}3
public:
virtual const RFractionk operator*= (const RFraction&) ;
+;
}
int main()
{
CPPBook::RFraction x(7,3);
CPPBook::Fraction f(3,7);
CPPBook: :Fraction* xp = &x;
}
In the base class, the parameter of the operator *= has the class Fract ion,
but in the derived
class it has the class RFraction. Because of this, the implementation of the
derived class is not
a true substitution for the implementation of the base class, but just an addition.
The new implementation is called for objects of the derived class. For pointers
and references
of the Fraction class, however, it is still the case that only the implemen
tation of the base class
exists. It is not overridden in the derived class. Although the functions to
be called are decided at
run time, the implementation of the base class Fraction is also used for
RFractions, because
there is no reimplementation of the operation defined in the base class.
5.2 Virtual Functions 285
When overriding inherited functions, you should therefore adhere to the following rule:
e A virtual function of a base class is only really overridden in a derived class if the number
and type of the parameters are identical.
In order to deal with the problem, the *= operator in the derived class needs to be declared with
the same parameters and the same return type as those of the base class:
namespace CPPBook {
class RFraction : public Fraction {
public:
virtual const Fraction& operator*= (const Fraction) ;
1
}
As with the overloading of functions in general, a differentiation of the return type only is not
allowed.
There is, however, one exception to this rule. If a virtual function of a base class returns a
pointer or reference of this base class, with a new implementation in a derived class, a pointer or
reference of this derived class may be returned instead. However, a pointer must remain a pointer
and a reference must remain a reference.
Thus the following reimplementation is also possible:
namespace CPPBook {
class RFraction : public Fraction {
public:
virtual const RFraction& operator*= (const Fraction&) ;
Using the solution introduced here (that the parameter of a member function is an object of the
base class) immediately creates another problem: with the implementation of the operator, it is
no longer possible to access the non-public members of the parameter:
const Fractionk RFraction::operator*= (const Fraction& f)
{
numer *= f.numer; // ERROR: no access to f .numer
denom *= f.denom; // ERROR: no access to £ .denom
286 5: Inheritance and Polymorphism
The operator is implemented for the class RFraction, but uses an object of a different class as
a parameter. It does not matter whether the class is a base class and whether there is private or
protected access to its members. From the RFraction class’s point of view, Fractions are
objects of a different type, for which only public access is granted. Thus we have another rule:
e For objects of the base class that are submitted as parameters, there is only access to public
members in derived classes.
For this reason, access to the members of the parameter can only be achieved via a public in-
terface. Because no member functions for the querying of numerators and denominators exist
(commercial classes would, of course, provide these), in this case, the implementation of the base
class has to be called, which multiplies the RFraction with the submitted parameter. Possible
inconsistencies that may occur can then be removed:
const Fraction& RFraction: :operator*= (const Fraction f)
{
/* new: calling the implementation of the base class
* - no access to non-public members of £ exists
t/
Fraction: :operator*= (f);
// still reduced?
if (!reducible) {
reducible = (gcd() > 1);
return *this;
There is another problem that can arise with inheritance. It concerns explicit memory
manage-
ment.
If an object of a derived class is created using new, its type is clear:
// create object of the class RFraction
CPPBook: :RFraction* kbp = new CPPBook: :RFraction;
However, when deleting the object using the delete keyword, this clarity may not exist.
Because
an object of a derived class can be used as an object of a base class, it can
also be released as
such:
CPPBook: :Fraction* bp;
The same problem that appears with reimplemented functions in derived classes also appears
here: the delete operator leads to the call of all destructors that may be available. As long
as no dynamic binding takes place, the fact that an object of a derived class is actually being
released is lost, and only the destructor of the class Fraction is called. Therefore, even if a
destructor is implemented for RFractions, this is not called. This means that if the destructor of
a derived class, for example, releases memory allocated for additional members, this would not
be released.
For this not to arise, a virtual destructor has to be declared in the base class. This applies
even if the base class does not actually need a destructor (the default destructor is not virtual).
We therefore have yet another rule for inheritance:
e A class is only generally suitable for inheritance if a virtual destructor is declared.
In our example, this means that the base class Fraction must have a (albeit empty) virtual
destructor:
namespace CPPBook {
class Fraction {
public:
virtual ~Fraction() {
}
The base class Fraction must meet the requirements that makes it suitable for inheritance:
e All functions that can be overridden must be declared as virtual.
e® A virtual destructor must be defined.
This therefore produces the following header file for the base class Fraction:
// inherit/frac93.hpp
#ifndef FRACTION_HPP
#define FRACTION_HPP
namespace CPPBook {
class Fraction {
protected:
int numer;
int denom;
public:
/* error class
ye
class DenomIsZero {
ie
/* multiplication
* - global friend function, so that an automatic
* type conversion of the first operand is possible
+/
friend Fraction operator * (const Fraction&, const Fraction&) ;
/* multiplicative assignment
* - new: virtual
i
virtual const Fraction& operator *= (const Fraction);
/* comparison
* - global friend function, so that an automatic
* type conversion of the first operand is possible
ie
friend bool operator < (const Fractionk, const Fraction&) ;
/* operator *
* - global friend function
* - inline defined
Ee
inline Fraction operator * (const Fraction& a, const Fraction& b)
a
/* simply multiply numerator and denominator
* - this saves time
a |
return Fraction(a.numer * b.numer, a.denom * b.denom);
#endif //FRACTION_HPP
In the derived class, the following points need to be taken into account:
e The derived class may only override functions that were defined in the base class as being
virtual. While doing this, the parameters and the return type must match.
e If the derived class is to be suitable for further inheritance, every additional function that can
be overridden must also be declared as being virtual.
This produces the following header file for the derived class RFraction:
// inherit/rfrac3.hpp
#ifndef RFRACTION_HPP
#define RFRACTION_HPP
namespace CPPBook {
/* class RFraction
* - derived from Fraction
* - new no access to inherited members
* (public remains public)
* - suitable for further inheritance
Ne
class RFraction : public Fraction {
protected:
bool reducible; // true: fraction is reducible
public:
/* default constructor, and one- and two-parameter constructor
* - parameters are passed to the Fraction constructor
5.2 Virtual Functions 291
e/
RFraction (int n = 0, int d = 1) : Fraction(n,d) {
reducible = (gcd() > 1);
// reduce fraction
virtual void reduce();
// test reducibility
virtual bool isReducible() const {
return reducible;
33
} // oh KKK END namespace CPPBook oie oie Kf okokok oh okoi ok ok okokoR of oh OK of of ofof ok of okok ok OKOk okokOK
#endif // RFRACTION_HPP
In the dot-C file of the derived class, the new and overridden functions are implemented. One
must make sure that only public access is granted to parameters that have the type of the base
class:
// inherit/rfrac3.cpp
namespace CPPBook {
292 5: Inheritance and Polymorphism
/* gcd()
* - greatest common divisor of numerator and denominator
4e
unsigned RFraction::gcd() const
{
if (numer == 0) {
return denom;
/* reduce()
#/
void RFraction: :reduce()
x
// if reducible, divide numerator and denominator by CGD
if (reducible) {
int divisor = gcd();
numer /= divisor;
denom /= divisor;
/* operator *=
* - reumplemented for overwriting with types of the base class
a
const RFraction& RFraction::operator*= (const Fraction& f)
a!
/* call implementation of the case class
* - no access to non-public members of £ exists
5.2 Virtual Functions 293
of
Fraction: :operator*= (f);
// still reduced ?
if (!reducible) {
reducible = (gcd() > 1);
}
return *xthis;
i:
/* scanFrom()
“),
void RFraction::scanFrom(std::istream& strm)
if
Fraction: :scanFrom (strm) ; // call scanFrom() ofthe base class
The default arguments of functions are used during compilation. They are therefore always static.
If virtual functions in the base class have a different default value than in the implementation
of the derived class, the correct function is called, but the incorrect default value is submitted.
Let us assume, for example, that a function init () is defined for Fractions and has 0 as
a default argument. The derived class RFraction overrides the function, but assigns 1 as the
default argument:
class Fraction {
We have already seen that, when overriding an inherited function, the parameter types should
be the same, because otherwise an overloading occurs instead of a true overriding. However,
overloading a function in a derived class may be intentional. An inherited function can be sup-
plemented with a function of the same name, but with additional parameters. In this case, it is
important to know that although the function of the base class is not overridden, it can no longer
be called by objects of the derived class. The overloaded implementation hides the derived one.
Let us look at the following example. The base class Fraction defines the member function
init(), with an int as a parameter. The derived class RFraction also defines a function
init (), but has a Fraction as its parameter:
class Fraction {
x. ainitta); // OK
In derived classes, overloading therefore always means overlapping. This property was intro-
duced in C++ for safety, so that an inadvertently incorrect parameter type could be recognized as
an error more easily.
If the function of the base class is still to be usable, it must be redefined:
class Fraction {
void init(int);
35
ty
The implementation of the class RFraction may still cause problems. It is still the case still
that, for RFractions, the inherited implementations of the base class may be called, instead of
the reimplemented versions. However, this requires explicit type conversions and cannot happen
by accident.
By using the scope operator, it can be explicitly required that, for all objects of the derived
class, implementations of the base class are called:
// inherit/rftest4. cpp
/| header files
#include <iostream>
#include "rfrac.hpp"
int main()
i
// declare RFraction
CPPBook::RFraction x(7,3);
/* multiply x by 3
* BUT: use operator of the base class fraction
A
x.CPPBook: :Fraction::operator *= (3);
296 5: Inheritance and Polymorphism
// output x
Stdsicoutme<<aex
std::cout << (x.isReducible() ? " (reducible)"
"(non reducible)") << std::endl;
As the output of this program demonstrates, this can lead to inconsistencies (the fraction x is
reducible after the multiplication, but returns that it is not):
21/3 (non reducible)
This kind of call is possible because, according to the declaration of the class RFract ion, it was
specified that public functions of the base class remain public. This still applies, even if they are
overridden and virtual is used.
We can now argue as to whether this presents a problem or not. Because it must be explicitly
indicated that the implementation of the base class is to be used, this kind of inconsistency
cannot occur by mistake. Therefore, if this is what is required, one should be aware of the
consequences. On the other hand, classes were introduced to provide a well-defined interface so
that inconsistencies cannot be produced.
This problem can be avoided by using the access keywords private or protected for the
base class when declaring the derived class:
class RFraction : protected Fraction {
I;
The outcome of this is that all member functions of the base class are no longer public. They
cannot be called by the user of the class unless they are reimplemented.
In the example of the RFraction class, this would, for example, mean that the I/O operations
have to be declared (and implemented) as public functions once more. However, because we
now need to reimplement each derived function again, it would seem that there is no benefit
from
using inheritance anymore. The same functionality could be provided by a separate RFraction
class, which only uses an object of type Fraction internally.
However, members can be inherited with different scope limitations. To do this, a class
has to
be derived as protected or private. The scope limitation can then be suspended
for individual
members. This happens via so-called pure access declarations.
Using pure access declarations, the declaration of the RFraction class looks as follows:
// inherit/rfrac5.hpp
#ifndef RFRACTION_HPP
#define RFRACTION_HPP
namespace CPPBook {
/* class RFraction
* - derived from Fraction
*- new. no access to inherited members (public becomes protected)
* - therefore, the is-a relationship is not longer valid
ae
class RFraction : protected Fraction {
protected:
bool reducible; // true: fraction is reducible
public:
/* default constructor, and one- and two-parameter constructor
* - parameters are passed to the Fraction constructor
*/
RFraction(int n = 0, int d = 1) : Fraction(n,d) {
reducible = (gcd() > 1);
// multiplicative assignment
virtual const RFraction& operator*= (const Fraction&) ;
// reduce fraction
virtual void reduce();
// test reducibility
virtual bool isReducible() const {
return reducible;
298 5: Inheritance and Polymorphism
#endif // RFRACTION_HPP
Using this implementation, the multiplicative assignment of the base class can no longer be called
for objects of the derived class. I/O operations and conversions to a floating-point value are still
able to be called by means of a pure access declaration:
namespace CPPBook {
class RFraction : protected Fraction {
public:
// new: pure access declaration for operations that remain public
Fraction: :printOn;
Fraction: :asDouble;
Ine
}
The pure access declaration ensures that printOn() and asDouble() stay public, although,
in general, derived public members become protected. Note that there are no parentheses after
the derived members in a pure access declaration.
Non-public inheritance reduces the general concepts of inheritance to that of a pure reuse of
code. In particular, the is-a relationship does not apply anymore. Thus you can no longer use an
object of a derived class as an object of the base class:
CPPBook: :RFraction x(91,39);
5.2.9 Summary
e Function calls are automatically bound statically. For objects, pointers, and references, the
member function is called that is defined for the class with which the object, pointer, or
reference was declared.
e The virtual keyword changes the static binding for pointers and references into a dynamic
or late binding. At run time, code is generated that calls the function that corresponds to the
actual class of the object.
e Using virtual functions, it is possible to override functions of base classes, without problems
arising from the use of pointers or references of the base class.
e Aclass that is suitable for inheritance should fulfil the following requirements:
- All functions that can be overridden by derived classes must be declared as virtual func-
tions.
— A virtual destructor must be defined.
e When implementing a derived class, attention should be paid to the following points:
- Overriding non-virtual functions in the base class can cause many problems and should
normally be avoided.
— Functions of the base class are only overridden if the parameters and the return type match.
As an exception for return types, a pointer or reference of the base class may become a
pointer or reference of the derived class, respectively.
— Overridden functions should have the same default arguments.
— Using the scope operator, overridden virtual functions can still be called.
— Only public access is granted to objects of a base class that are submitted as a parameter.
e Using non-public inheritance, the is-a relationship can be avoided. In this case, the only
consequence of the inheritance is the reuse of code.
300 5: Inheritance and Polymorphism
5.3. Polymorphism
This section introduces the concept of polymorphism and its application in C++.
A special polymorphic quality of C++, which is discussed in this section, is the fact that the
class-specific differences of objects can be lost at times. This reinforces the basic notion that
object-oriented languages are superior to conventional languages, as they support the means of
the abstraction of common terms°®: different objects can be seen under the same common term at
times, and can also be manipulated. However, during this manipulation, the differences are not
lost.
There is a wealth of examples of the uses of common terms:
e Cars, buses, lorries and bikes are all grouped under the common term ‘vehicle’. As a vehicle,
they can be situated at a particular position and can drive to a different position.
e Integers, floating-point values, fractions and complex numbers are all regarded as ‘numeric
values’, which can be added and multiplied.
e Circles, rectangles, lines, etc., are ‘geometric objects’, which are found in a figure, have a
color and can be drawn.
e Customers, employees, managers, heads of departments, etc., are different types of ‘people’,
who have a name and various other properties.
This means that in programs in which different kind of objects are grouped together under
one
common term, no consideration has to be given to their actual class:
e A function for vehicles thus need only use the type Vehicle. With this, it can automatica
lly
manage cars, buses, lorries, etc.
e A graphics editor, only using the type GeometricObject, can automatically manage,
output
and manipulate circles, rectangles, lines, etc.
6 F :
Generic term is probably a better phrase for common term. However, as generic: has A ; ‘
a special meaning in
C++, I decided to use common term here.
5.3 Polymorphism 301
Due to polymorphism, the differences still play a part, because the operations of the objects that
are managed are interpreted differently:
e Ifa vehicle drives (i.e. the member function drive() is called), the appropriate function is
automatically called, depending on the actual type of the object. Thus, if the vehicle is a lorry,
the ‘drive’ defined for lorries is executed (i.e. drive() of the class Lorry); if it is a car, the
‘drive’ defined for cars is automatically executed.
e Ifa graphical object is output, the appropriate operation is automatically called, depending
on what kind of object it is. If it is a circle, the operation for laying out a circle is called; if it
is a rectangle, the layout operation defined for rectangles is used.
With the implementation of the application code for the use of common terms, we do not even
have to know what types are combined under the common term:
e If boats are introduced as a new kind of vehicle, the function for vehicles can automatically
let boats drive as well.
e If triangles or text elements are introduced as a new kind of geometric object, the graphics
editor can manage these automatically.
Polymorphism therefore makes it possible for different objects to be processed under a common
term in non-homogeneous collections. Then, depending on the actual type of the object, the
correct function is automatically called.
In C++, on the other hand, we have type binding. At compile time, the compiler checks whether
a function call is allowed. This concept is preserved with polymorphism. A class that represents
a common term, has to determine what functions can be called for objects under the common
term. However, these functions do not have to be implemented yet.
If an object of a class is created that can be managed under the common term, it is verified
that, for all operations that are declared under the common term, an implementation exists. This
implementation may be provided by the base class for the common term or the derived class. The
compiler thus ensures that every function call is possible at run time. In contrast to Smalltalk,
’ To be precise, in object-oriented terminology, this means that objects in Smalltalk are sent a ‘message’,
which is dealt with by an appropriately defined ‘method’.
302 5: Inheritance and Polymorphism
the C++ run-time system does not produce a message when a function call cannot be performed
due to the fact that it is called for an object that does not have an appropriate member function.
This kind of polymorphism can be implemented in C++ with two different language features:
inheritance and templates. The usual form of polymorphism with inheritance is introduced later
on in this section. The implementation of polymorphism with templates is dealt with in Sec-
tion 7.5.3 on page 457.
Base classes for common terms are typically so-called abstract classes. Abstract classes are
classes in which the very act of creating a concrete object is nonsensical.
C++ supports abstract classes in that it ensures that objects of an abstract class cannot be
created (not ali object-oriented languages provide this ability).
The counterpart to an abstract class (i.e. a class in which it makes sense, and is possible, to
create concrete objects) is called a concrete class.
reference point accordingly. A Circle additionally has a radius and implements a function for
laying out. The function for moving is inherited. A Line has a second point (the first point is
used as the reference point) and reimplements the function for moving, as well as the function
for drawing.
move (Coord)
draw ()
move (Coord)
draw ()
Figure 5.4 clarifies the resulting class hierarchy, using the UML notation. The italic font of
the base class GeoObj denotes that it is an abstract class. The operation draw() is also italicized
in the base class: this means that it is an abstract operation, and must therefore be implemented
by concrete classes.
We now introduce the auxiliary class Coord. It is simply provided to manage two X and Y
coordinates as a common object. It is not a geometric object (the corresponding geometric object
would be the point), but a pair of values that can be used absolutely (as a position) or relatively
(as an offset).
The construction and the operations are so simple that the class can be implemented entirely
in a header file:
304 5: Inheritance and Polymorphism
// inherit/coord.hpp
#ifndef COORD_HPP
#define COORD_HPP
namespace Geo {
/* class Coord
* - auxiliary class for geometric objects
* - not suitable for inheritance
Si
class Coord {
private:
aioli 38 // X coordinate
int. ys /| Y coordinate
public:
// default constructor, and two-parameter constructor
Coord Or: x (Oy =y (0) 4 // default values:0
i;
Coord(int newx, int newy) : x(newx), y(newy) {
a)
/* operator +
* - add X and Y coordinates
af
inline Coord Coord::operator + (const Coord& p) const
{
return Coord(xtp.x,y+p.y);
/* unary operator -
* - negateX and Ycoordinates
5.3 Polymorphism 305
mi
inline Coord Coord::operator - () const
£
return Coord(-x,-y);
/* operator +=
* - add offset to X and Y coordinates
fs
inline void Coord::operator += (const Coord& p)
{
x += p.xs
Se De
L
/* printOn()
* - output coordinates as a pair of values
+f
inline void Coord: :printOn(std::ostream& strm) const
il
Se rm) Kylee <Sce eK<a) , SG <P
}
/* operator <<
* - conversion for standard output operator
ge
inline std::ostreamk operator<< (std::ostream&k strm, const Coord& p)
{
p.printOn(strm) ;
return strm;
} //namespace Geo
#endif // COORD_HPP
The class should be self-explanatory. However, it is worth mentioning that only no parameters
or two parameters can be passed to the constructor. If no parameters are passed, both the X and
Y coordinates are initialized with 0.
306 5: Inheritance and Polymorphism
#ifndef GEOOBJ_HPP
#define GEOOBJ_HPP
namespace Geo {
public:
/| move geometric object according to passed relative offset
virtual void move(const Coord& offset) {
refpoint += offset;
}
5.3 Polymorphism 307
// virtual destructor
virtual ~Geodbj() {
}
a
} //namespace Geo
#endif //GEOOBJ_HPP
First, the members for the reference point and the constructor of the class are defined as non-
public:
class GeoObj {
protected:
Coord refpoint;
}3
The fact that the constructor is non-public makes it clear that GeoObj is an abstract class. As a
result, no concrete objects of the class can be created. Because the constructor has a parameter
with which the reference point is initialized, there is no default constructor. The outcome of
this is that, in their constructors, directly derived classes always have to submit a coordinate (by
means of initializer lists) for the initialization of the reference point.
Next, the public functions that are defined for all geometric objects are declared. These
include the virtual destructor, which makes the class suitable for inheritance.
In addition to this, the movement of an object is both declared and implemented:
class GeoObj {
public:
virtual void move(const Coord& offset) {
refpoint += offset;
308 5: Inheritance and Polymorphism
The implementation simply moves the reference point by adding the passed offset. This is suffi-
cient for objects whose absolute position is only defined via the reference point. Objects that use
multiple coordinates to define their position must override this implementation.
public:
virtual void draw() const = 0;
};
At this point, something unusual happens: instead of an implementation, the function is assigned
the value 0. This means that the function for the class is declared, but is not implemented. The
implementation therefore has to be carried out by the classes derived from it.
This kind of function is known as a pure virtual function. Pure virtual functions fulfil an
important purpose: they declare that a particular function can be called for a class that is used as
a common term, without actually defining it. In object-oriented modeling, this kind of operation
is described as an abstract operation.
As long as a class contains a pure virtual function, it is defined incompletely. No objects
of the class can be created. Because of this, the class automatically becomes an abstract class.
This is also true for derived classes. The compiler makes sure that pure virtual functions
are
implemented in derived classes. (An exception to this is if the derived class is itself an abstract
class.) The creation of an object of a derived class is only allowed if all pure virtual functions
have an implementation.
Pure virtual functions can nevertheless have a default implementation in base classes.
This is
discussed in Section 5.3.7 on page 326.
Abstract classes can also be defined if there are no pure virtual functions available.
In order
to do so, the constructors must simply be declared as private or protected,
as we have done
here.
# GeoOb
3 (Coord) + Circle (Coord, unsigned)
+ draw()
#ifndef CIRCLE_HPP
#define CIRCLE_HPP
namespace Geo {
/* class Circle
* - derived from GeoObj
* - a circle consists of:
* — - a center point (reference point, inherited)
* - a radius (new)
/
class Circle : public Geo0bj {
protected:
unsigned radius; //radius
public:
// constructor for center point and radius
Circle(const Coord& m, unsigned r)
310 5: Inheritance and Polymorphism
GeoObj(m), radius(r) {
// virtual destructor
virtual “~Circle() {
iF
ie
/* drawing
* - defined inline
"/
inline void Circle::draw() const
ct
std::cout << "Circle around center point " << refpoint
<< " with radiys " << radius << std::endl;
a:
} //namespace Geo
#endif // CIRCLE_HPP
The constructor requires a center point and a radius as parameters for the initialization. The
center point is submitted to the constructor of the base class Geo0bj via the initializer list, where
it is used for the initialization of the reference point. Using the radius as the second parameter,
the corresponding member is initialized.
The class implements the drawing function draw(). Because of this, the class no longer
contains a pure virtual function and can therefore be used as a concrete class for the creation
of
objects. The function itself simulates the drawing by outputting a corresponding text.
The function move () is derived from the base class, because only the center point defines
the
position of the circle and therefore a movement of this point is sufficient.
In principle, the class Line has the same contents as the class Circle. However,
a line consists
of two absolute coordinates: the start point and the end point. In addition to the
reference point,
which is taken as the start point, a second point is therefore needed (see Figure 5.6).
5.3 Polymorphism cal
:GeoObj
# refpoint: Coord
+ draw() + draw ()
namespace Geo {
/* class Line
* - derived from Geo0bj
* - q line consists of:
* — - a start point (refernce point, inherited)
* —- an end point (new)
<¢/
class Line : public Geo0bj {
protected:
Coord p2; // second point, end point
public:
// constructor for start and end points
Line(const Coord& a, const Coord& b)
312 5: Inheritance and Polymorphism
: GeoObj(a), p2(b) {
// virtual destructor
virtual ~Line() {
}
Ai
/* output
* - defined inline
*/
inline void Line::draw() const
A
std::cout << "Line from " << refpoint
<< tO <p? << std- send.
}
/* move
* - reimplemented, inline
+/
inline void Line::move(const Coord& offset)
x
refpoint += offset; // represents GeoObj : :move (offset) ;
p2 += offset;
}
} //namespace Geo
#endif //LINE_HPP
In this case, the constructor requires two coordinates as parameters for the initializat
ion. The first
coordinate is passed to the constructor of the base class GeoOb j via the initializer
list, where it is
used for the initialization of the reference point. The second coordinate is used
to initialize the
additional member for the end point of the line.
5.3 Polymorphism 313
The drawing function draw(), which outputs a descriptive text, is implemented in a similar
way to that of the Circle class.
The function move() is reimplemented, because the start point and the end point both have
absolute coordinates. Thus the implementation of the base class, which only moves the single
reference point, is not enough. One could also have implemented the second point as an offset
to the first point, in which case move() could be derived from the base class. However, then
appropriate transformations between a relative offset and the absolute coordinates would have to
be implemented in other functions, such as, for example, draw().
Application Example
A small sample program will now clarify the use of polymorphism. In the example, circles
and lines are seen and manipulated as common geometric objects. However, when moving and
drawing the objects, the implementation of the actual class is used:
// inherit/geotest1.cpp
// forward declaration
void printGeo0bj (const Geo: :Geo0bj&) ;
int main()
{
Geo: :Line 11(Geo::Coord(1,2), Geo: :Coord(3,4));
Geo; Line 12(Geo;: Coord
7, 7) ,uGeo; . Coord (0.0)
Geow Ginehesc (Geom Coond (seo) nals
coll[i]->move(Geo: :Coord(3,-3));
After the creation of two Lines and a Circle, these are used as geometric objects in an array of
pointers to the type GeoObj. The variable col1 therefore represents an inhomogeneous collection
of geometric objects.
Every element that is in the collection is then drawn and moved in a loop. Because vir-
tual functions are used, at run time, the real class of the geometric object is evaluated and the
corresponding function is called.
In the same way, the correct function is called when a reference is used, as occurs with
the
function printGeoObj().
The output of the program is as follows:
Line from (1,2). toit3s4)
Circle around center point (3,3) with radius 11
Line from (7,7) to (0,0)
Line from (4,-1) to (6,1)
Circle around center point (6,0) with radius 11
Line from (10,4) to (3,-3)
Polymorphism is only possible if the identity of an object is not lost. This means
that an object
can only be seen as an object of the class GeoObj by means of pointers and references
. A variable
of the type GeoObj is, and remains, of the Geo0bj type, even if a Circle ora
Line is assigned.
In this case, no variable of the Geo0bj type can be declared, because it is
an abstract class
(i.e. it has a pure virtual function and no public constructor). However, we can also
conceive base
classes, from which it also makes sense to create objects. In this case, an object
loses its identity
if, for example, it becomes an object of the base class through assignment
or a type conversion.
Polymorphism can then no longer take place, as it only works with pointers and
references.
5.3 Polymorphism B15
In order for this not to happen by mistake, when using polymorphism, one should make
sure that the base classes are always abstract. This is always possible simply by declaring all
constructors as non-public.
Geo: :GeoObj
refpoint: Coord
move (Coord)
draw ()
| Georscizete |
radius: unsigned
ios gia
draw ()
The class GeoGroup also implements the function draw() and inherits the function move().
A vector (see Section 3.5.1 on page 70) is added as a new data member to manage the elements of
the group (i.e. the geometric objects). Additional member functions provide the ability to insert
and remove an element. Of course, in practice, other functions would be provided, but this is
enough to demonstrate the principle.
316 5: Inheritance and Polymorphism
As usual, the header file of the class GeoGroup contains the class declaration:
// inherit/geogroup.hpp
#ifndef GEOGROUP_HPP
#define GEOGROUP_HPP
namespace Geo {
/* class GeoGroup
* - derived from Geo0bj
* - a GeoGroup consists of:
* — - a reference point (inherited)
* -a collection of geometric elements (new)
of
class GeoGroup : public Geo0bj {
protected:
std: : vector<Geo0bj*> elems; // collection of pointers to GeoObjs
public:
// constructor with optional reference point
GeoGroup(const Coord& p = Coord(0,0)) : GeoObj(p) {
}
// insert element
virtual void add(Geo0bj&) ;
// remove element
virtual bool remove (Geo0bj&) ;
// virtual destructor
virtual ~GeoGroup() {
5.3 Polymorphism olf
I;
} //namespace Geo
#endif //GEOGROUP_HPP
is.
The vector manages pointers to the geometric objects that comprise the group. The use of point-
ers is necessary in order to maintain the actual type of the element. It would not be possible to
use GeoObj as an element type, because it is an abstract class. The use of references is also not
possible, as, for references, the objects they represent must be established at initialization time.
In this case, elements are inserted and removed at run time.
The constructor is passed a parameter, which is used by the constructor of the base class to
initialize the reference point:
class GeoGroup : public GeoObj {
public:
// constructor with optional reference point
GeoGroup(const Coord& p = Coord(0,0)) : GeoObj(p) {
}
3
As a default argument, it contains the origin (the coordinate (0,0)). The reference point is used
by the GeoGroup as an offset to the elements of the group. The coordinates of the elements are
relative and refer to the reference point of the GeoGroup.
In the dot-C file of the class GeoGroup, the functions for inserting and removing elements, as
well as the function for drawing all elements, are defined:
// inherit/geogroup. cpp
#include "geogroup.hpp"
#include <algorithm>
namespace Geo {
318 5: Inheritance and Polymorphism
/* add
* - insert element
Nh
void GeoGroup: :add(Geo0bj& obj)
4
// keep address of the passed geometric object
elems.push_back (&ob}) ;
iF
/* draw
* - draw all elements, taking the reference points into account
a7
void GeoGroup::draw() const
£
for (unsigned i=0; i<elems.size(); ++i) {
elems[i]->move(refpoint); — // add offset for the reference point
elems[i]->draw(); // draw element
elems[i]->move(-refpoint); // subtract offset
i
}
/* remove
* - delete element
ef,
bool GeoGroup:
:remove (Geo0bj& obj)
af
// find first element with this address and remove it
// return whether an object was found and removed
std: : vector<Geo0bj*>::iterator pos;
pos = std::find(elems.begin() ,elems.end() ,&obj) ;
if (pos != elems.end()) {
elems.erase(pos) ;
return true;
J;
else {
return false;
}
‘,
} //namespace Geo
5.3. Polymorphism B19
The function for inserting an element simply stores the address of the added element in an internal
collection:
void GeoGroup: :add(Geo0bj& obj)
{
// keep address of the passed geometric object
elems.push_back(&obj) ;
J
Because only references and pointers are used for the submitted geometric object, the real class
of this object is still known.
Use of this information is then made when drawing the elements:
void GeoGroup::draw() const
{
for (unsigned i=0; i<elems.size(); ++i) {
elems[i]->move(refpoint); // add offset for the reference point
elems[i]->draw(); // draw element
elems[i]->move(-refpoint); // substract offset
}
y
Using a for loop, all elements of the GeoGroup are drawn by calling the draw() function for
each one. As virtual functions are used, the correct function is automatically called: if the
element is a Circle, Circle: :draw() is called; if the element is a Line, Line: :draw() is
called; if the element is a group of geometric objects (which can also be a geometric object in a
group), GeoGroup: :draw() is called, which, in turn, calls the correct draw() function for all
the elements that it contains.
Before and after drawing, the object is manipulated in order to process the offset of the whole
geometric group. The offset is then added to the coordinates before drawing, and subtracted after
drawing, so that the position of the element remains stable. In practice, one would definitely
provide the draw() function with the offset as a parameter instead. However, this example
shows how, by depending on the actual type of the element, the suitable move () implementation
is automatically called.
The class is finished off with the function for removing an element:
bool GeoGroup: :remove(Geo0bj& obj)
{
// find first element with this address and remove it
// return whether an object was found and removed
std: :vector<Geo0bj*>::iterator pos;
pos = std::find(elems.begin() ,elems.end() , obj) ;
if (pos != elems.end()) {
elems.erase(pos) ;
return true;
320
e 5: Inheritance and Polymorphism
eee ee eee
else {
return false;
}
In this case, the position of the passed geometric object is searched for with the help of the
find() algorithm. Note that the addresses are compared, which means that only identical objects
are found. If the object is found, it is removed from the collection using erase, and true is
returned. If the object is not found, the function returns false.
Application Example
The first application program for geometric objects (see page 313) can now be rewritten to in-
clude the use of groups of geometric objects:
// inherit/geotest2.
cpp
// header file for I/O
#include <iostream>
int main()
{
Geo: :Line 11(Geo: :Coord(1,2), Geo: :Coord(3,4));
Geo: :Line 12(Geo: :Coord(7,7), Geo: :Coord(0,0));
Geo: :Circle c(Geo::Coord(3,3), 11);
Geo: :GeoGroup g;
calls the draw() function of the class GeoGroup, which loops through calls of the correct draw ()
functions for the elements contained within.
The program has the following output:
ane rOmm Gla?) tO Com)
Circle around center point (3,3) with radius 11
iisimemtcOmm Giaiie) mat Om CORO)
Note that the interface of the GeoGroup hides the internal use of pointers. Thus the application
programmer need only pass the objects that need to get inserted or removed.
Because a GeoGroup is itself a geometric object, it can also be inserted into a GeoGroup:
GeoGroup g2;
all geometric objects. By doing so, the class can also manage objects of any other class, as long
as it is derived from GeoObj.
Therefore, if new geometric objects, such as triangles or text, are introduced, we only need to
make sure that the corresponding classes are derived from Geo0bj. The GeoGroup class remains
unchanged and does not need to be recompiled. This is a very important advantage. As long
as the requirements on the common term do not change, the system can be extended without
altering the existing implementations for the common term. Complex processes such as moving,
outputting and grouping geometric objects need only be implemented once.
Using the dynamic_cast operator, we can convert an object back into its
actual type:
5.3 Polymorphism 323
Ay
else {
// obj is not a GeoGroup
t
324 5: Inheritance and Polymorphism
If we only want to find out if an object is of a certain type, the following is enough:
void typequery(const Geo: :Geo0bj& obj)
{
if (dynamic_cast<GeoGroup*>(obj) != NULL) {
// obj is a GeoGroup
}
else {
// obj is not a GeoGroup
}
}
By using the typeid operator, we can even determine the type at run time. The following
example shows how this can be done:
#include <typeinfo>
// compare type
if (typeid(t) == typeid(Geo::GeoGroup)) {
iY
iy
For an object or a class, typeid returns a description object that has the type std:
:type_info
(defined in <typeinfo>), on which the following operations can be called:
e The name() function returns the name of the class as a C-string. The way
this string looks is
implementation specific.
e The == and != operators return whether two types are the same. Because
an object or a class
can be passed to the typeid operator as an argument, we can find out whether
the type of
two objects is the same, and can also determine whether an object has a particular
type.
e Using before (), a type can also be compared to another for the purpose
of sorting.
If an argument for typeid is a pointer, and if this pointer has the value
0 or NULL, then an
exception of the type std: :bad_typeid is thrown. This can be dealt
with as described in
Section 3.6 on page 93.
5.3 Polymorphism 325
RTTI must be used with care because, particularly when using typeid, code becomes dependent
on concrete types. The following code segment is an example of very poor design:
int area(const Geo::Geo0bj& obj) // very bad example
{
if (typeid(obj) == typeid(Geo::Circle) ||
typeid(obj) == typeid(Geo::Rectangle)) {
// calculate area
i:
else {
return 0;
}
}
With this kind of coding, the whole advantage of programming with common terms is nullified,
because the area() function must be verified and adapted with the introduction of every new
kind of geometric object.
It would be better to have a design that introduces another abstract class GeoArea, under the
class GeoObj, from which all concrete area objects are derived. In this case, the code does not
need to be rewritten:
int area(const Geo::Geo0bj& obj) // somewhat better example
GeoArea* fp = dynamic_cast<GeoArea*>(&obj)
if (fp != NULL) {
// calculate area
return fp->calcArea() ;
}
else {
return 0;
}
i
However, care has to be taken that geometric objects do not exist for which it makes sense to
calculate an area, but that are not derived from GeoArea.
A design that allows the area of all objects to be calculated is best:
int area(const Geo::Geo0bj& obj) //OK
{
return obj.calcArea() ; // inquire about the area of every object
}
For this, a default implementation can be provided in Geo: : Geo0bj that returns 0 for all objects
that do not have an area:
326 5: Inheritance and Polymorphism
class Geo0bj {
public:
virtual int calcArea() const {
return 0;
Le
To make sure that this default implementation is not derived without further thinking, a default
implementation can be provided even when the function is declared as being pure virtual:
class Geo0bj {
public:
virtual int calcArea() const = 0; // pure virtual
3;
16
provides a particular service and the objects that use this service. If the agreement is defined as
an abstract base class, both sides can be developed and modified independently of each other °.
This kind of interface can, for example, be used as follows:
class Printable {
public:
virtual void print() = 0;
+5
On the one hand, we can now implement any class that implements this interface:
class XYZ : public Printable {
pubic:
virtual void print() {
33
On the other hand, we can write functions that call appropriate member functions for all objects
that satisfy this agreement (and are therefore derived from Printable):
void foo(const Printable& obj)
af
obj.print();
}
These abstract base classes are also simply known as ABCs. In principle, it is a good guideline
to only implement base classes as abstract base classes.
5.3.9 Summary
e Polymorphism describes the ability of identical operation calls leading to different operations
or behavior for different object.
e C++ supports polymorphism through
— members of different classes having the same name,
— the overloading of functions, and
— virtual functions.
e Using polymorphism, the means of abstraction with a common term can be implemented.
8 Java programmers are familiar with this concept, which is offered to them via the keywords interface
and implements. In C++, however, there are no special keywords: You simply implement an abstract base
class with only pure virtual functions.
328 5: Inheritance and Polymorphism
+
e Abstract classes are classes that cannot be instantiated. They can be used to combine common
properties of different classes in order to be able to work with common terms. The counterpart
to this, a class from which objects can be created, is a concrete class.
e Pure virtual functions are functions that are declared but not implemented. Instead, they are
implemented in derived classes. They are defined through an assignment of 0.
e Default implementations are possible for pure virtual functions.
e The operators typeid and dynamic_cast are provided for the query of types at run time and
for the downcast from a common term to the actual class respectively. As a rule of thumb,
you should try to avoid using them, because they often result in poorly designed code (closed
hard-coded selections instead of open polymorphism).
5.4 Multiple Inheritance B29
Using this example, we will look at the problems that can arise with multiple inheritance. These
are usually name conflicts due to the fact that members of different base classes can have the
same names.
First we look at the base classes. They have deliberately been kept simple for this example.
For the purposes of this example, a Car contains just one member, which stores the distance
(in kilometers) it has traveled. This can be initialized during the creation of an object of the Car
class; if not, it is initialized with the default value 0. A Car can travel a certain distance, and the
number of kilometers a Car traveled can be output:
330 5: Inheritance and Polymorphism
// inherit/car.hpp
#ifndef CAR_HPP
#define CAR_HPP
namespace CPPBook {
/* Car class
* - suitable for inheritance
WD
class Car {
protected:
int km; // kilometers traveled
public:
// default constructor, and one-parameter constructor
Car(int d = 0) : km(d) { // initialize distance traveled
}
// virtual destructor
Virtual Car e+
}
hi
} //namespace CPPBook
#endif // CAR_HPP
5.4 Multiple Inheritance 331
In principle, the same applies to the Boat class, although distances are measured in sea miles
rather than kilometers:
// inherit/boat.hpp
#ifndef BOAT_HPP
#define BOAT_HPP
namespace CPPBook {
/* Boat class
* - suitable for inheritance
+/
class Boat {
protected:
int sm; // sea miles traveled
public:
// default constructor, and one-parameter constructor
Boat(int d = 0) : sm(d) { // initialize distance traveled
}
di //namespace CPPBook
#endif // BOAT_HPP
B32 5: Inheritance and Polymorphism
The class for amphibious vehicles, Amph, is derived from the classes Car and Boat as follows:
// inherit/amph.hpp
#ifndef AMPH_HPP
#define AMPH_HPP
namespace CPPBook {
/* Amph class
* - derived from Car and Boat
* - suitable for further derivation
ef.
class Amph : public Car, public Boat {
public:
/* default constructor, and one- and two-parameter constructor
* - Car constructor is called with first parameter
* - Boat constructor is called with second parameter
2/
Amph(int k = 0, int s = 0) : Car(k), Boat(s) {
// thus there is nothing more to do
// virtual destructor
virtual ~Amph() {
a;
+;
} //namespace CPPBook
#endif // AMPH_HPP
5.4 Multiple Inheritance 333
As can be seen, the names of the base classes are given after the Amph class declaration,
separated
by acolon:
class Amph : public Car, public Boat {
‘2
For each base class, it is indicated to what extent access to the inherited members is limited (see
Section 5.1.3 on page 262).
Because the Amph class is derived from the two base classes Car and Boat, the properties of
the class Car, as well as the properties of the class Boat, are inherited. An amphibious vehi-
cle therefore has the members km and sm. These can be output using, for example, the newly
implemented member function printTraveled(), as shown in the following program:
// inherit/amphtest.cpp
int main()
{
/* create amphibious vehicle and initialize
* with 7 kilometers and 42 sea miles
zy,
CPPBook: :Amph a(7,42);
The declaration
CPPBook: :Amph a(7,42);
creates an object of type Amph. The constructors of the base classes are called in the order that
they are declared; the order given in the initializer list is immaterial. In fact, the creation is done
in the following steps:
e First of all, the memory for the object is allocated:
334 5: Inheritance and Polymorphism
e Then the one-parameter constructor of the class Car is called, which initializes the part of the
object that is inherited from Car:
e Afterwards, the one-parameter constructor of the class Boat is called, which initializes the
part of the object that is inherited from Boat:
e Because the constructor of the class Amph has no statements in the body, and no new members
have been added either, the object is fully initialized.
If constructors have to be called for base classes as well as for members, the constructors of the
base classes and called first, and then the constructors of the new members.
The statement
a.printTraveled() ;
calls the member function printTraveled(), which is newly implemented in the Amph class
and outputs the total distance traveled.
Ambiguities
Notice that a call to travel() is not possible for amphibious vehicles. Because this member
function is inherited from both the Car class and the Boat class, it is not clear what one should
be called:
CPPBook::Amph a(7,42);
I;
Of course, it is also possible to implement the functions directly. However, the necessary modifi-
cations of implementation of the base class may generate inconsistencies. Here, as so often, the
advantage of maintaining consistency has to be weighed up against the disadvantage of increas-
ing running time.
If the amphibious vehicle is seen as a Car or a Boat, then it is obvious which travel () is
meant and the call is therefore possible without a qualification:
CPPBook: :Amph a(7,42);
If a class has several base classes, these can be directly or indirectly derived from the same base
class. If this happens, there is the question of whether the multiply inherited members of the base
class are available once or more than once.
Let us assume, for example, that the classes Car and Boat are both derived from the class
Vehicle (see Figure 5.9). If the Vehicle class defines a member yearOfManufacture, both
a Car and a Boat also have a year of manufacture. In this case, an amphibious vehicle should
only have one year of manufacture, even though it derives from both Car and Boat. On the
other hand, if the Vehicle class defines another member maxspeed (for the maximum speed),
it may well be useful to have two copies of this member for amphibious vehicles—one for the
maximum speed as a Car and once for the maximum speed as a Boat.
C++ provides for both possibilities: multiply inherited members can be used as one, or as
distinct members.
336 5: Inheritance and Polymorphism
By default, members from the same base classes inherited via different classes are distinct mem-
bers in C++. In the following definition, an amphibious vehicle has two copies of maxspeed:
// inherit/vehiclehier.hpp
namespace CPPBook {
class Vehicle {
protected:
int maxspeed; // maximum speed
so
int
} //namespace CPPBook
5.4 Multiple Inheritance 337
Because maxspeed is inherited from Car as well as from Boat, it can only be accessed for
amphibious vehicles using the scope operator:
void Amph: :f()
{
maxspeed = 100; // ERROR: ambiguous
Car::maxspeed = 100; // OK: maxspeed of Car
Boat: :maxspeed = 70; // OK: maxspeed of Boat
Vehicle::maxspeed = 100; = /// ERROR: ambiguous
}
In order to make a member that is inherited from a base class via different classes unique, the
classes that derive directly from the common base class have to define this base class as a virtual
base class. This is done using the virtual keyword in the specification of the base class:
class Vehicle {
protected:
int yearOfManufacture; // year of manufacture
te
tee
rs
I3
By using the virtual keyword for the base class Vehicle, if the derived classes come together
again later in a common derived class, the members ofthe class Vehicle are only available once.
Note that the virtual keyword can be placed before or after the access keyword.
Because yearOfManufacture is inherited virtually from Car as well as from Boat, the
member is only available once in Amph. Access without a scope operator is now clear and there-
fore possible:
void Amph: :f()
{
yearOfManufacture = 1983; /1 OK
338 5: Inheritance and Polymorphism
Car::yearOfManufacture = 1983; // OK
Boat: : year0fManufacture = 1983; // OK
Vehicle: :yearOfManufacture = 1983; //OK
h
It is decisive for the common use of the members of the class that the directly derived classes
declare their base class as virtual. The keyword virtual must therefore be given for Car and
Boat, as has happened here. Whether Amph is derived virtually from Boat and Car is irrelevant.
But what happens if an amphibious vehicle’s yearOfManufacture should be available just once,
but maxspeed available twice? Here, we need an auxiliary class in order to separate the members
that are to be used as one unique member from the ones that are to be used multiple times.
This is depicted in Figure 5.10. Because all the classes that derived directly from class
VehicleVirtual (here, this is just the class VehicleNonVirtual) use virtual inheritance, the
members of VehicleVirtual are available only once. Because the classes derived directly from
VehicleNonVirtual are non-virtual, its members are available twice.
VehicleVirtual |
yearOfManufacture
VehicleNonVirtual
part of Amph
With simple inheritance, the members of the derived class, which reside at the end of the
storage of the object, are just ignored. The address of the objects is the same. This also happens
with multiple inheritance, as long as only the partial objects that reside at the end of the storage
are left out. This typically happens, for example, with our amphibious vehicle if it is seen as a
car (see Figure 5.12).
However, if the amphibious vehicle is seen as a boat, just ignoring the car part of the object
does not work because this part resides between the parts for Vehicle and Boat.
For this reason, a ‘shadow object’ is created, which has the correct layout for Boats. The
parts of the object are actually only internal references to the original object (see Figure 5.13).
As a result, the object gets a different address.
The addresses even remain different if the Car and the Boat are seen as a Vehicle. The data
of the Vehicle is then once a part of the original object and once a part of the shadow object.
As a result, the view of the same object with the same type can result in different addresses.
340 5: Inheritance and Polymorphism
part of Boat
part of Amph
part of Amph
#include "vehiclehier.hpp"
#include <iostream>
int main()
f,
CPPBook:
: Amph a;
ifCansiCan)s
fBoat (a);
:;
The function fVehicle(), which is called in two different ways, will (with almost all compilers)
output two different addresses for the same object a.
Instead of using references, we can also carry out the test with pointers:
// inherit/ident2.
cpp
#include "vehiclehier.hpp"
#include <iostream>
int main()
{
using std::cout;
using std::endl;
CPPBook::Amph a;
// address of a
couts<< "fase << (voids)
ka <o "\n" << endl:
It is not even guaranteed that just two different addresses are output. The fact that an object
always has the same address is not a language specification of C++, but is instead based on the
fact that compilers, for as long as possible, usually manage objects using the same address.
In order to recognize identical objects in any case, there is only one possibility: appropriate
mechanisms have to be self-implemented. Each object needs to have both a unique ID as a
member and a member function for comparing this ID. Section 6.5.1 on page 414 demonstrates
how this can be implemented.
It could be argued that this ability might be useful, although I have yet to see any conclusive
evidence of this. Such a situation would mean that an object of type B is, on the one hand, an
object of type A, but, on the other hand, is also an object of the same type A.
In practice, if you experience this kind of problem with inheritance, you should check whether
your design is sound, and that it does not mean that an object of type B contains two objects of
type A, i.e. has two objects of type A as members.
If you ever happen upon a case where it is useful to directly derive the same base class twice,
you should do two things:
5.4 Multiple Inheritance 343
e Write to me so that I can use your example in the next edition of the book°.
e Introduce dummy classes so that the path to the same base class is unique (see Figure 5.15).
5.4.5 Summary
e C++ allows multiple inheritance: derived classes can have multiple base classes.
e If name conflicts for the access to members arises from this, the scope operator should be
used.
e Base classes can be used multiple times by means of multiple inheritance. Using virtual base
classes, the members defined in the common base class are then only available once.
e With multiple inheritance, the same object can have different addresses. This is possible even
though it is seen as having the same type. In order to remove any doubt as to the identity of
an object, members that allow the definition and querying of a unique ID must be introduced.
° The only meaningful example that I can think of is the class SplitPersonality, derived twice from the
class Person.
344 5: Inheritance and Polymorphism
In object-oriented languages, there are two kinds of abstraction: inheritance and containment.
The relationships behind these are shown in Table 5.1.
Containment Inheritance
relationship has_a iS_a
part_of kind_of
containment inheritance
composition generalization
language feature |.member, reference | derived class
The design error is that a specialized condition is established for the inherited members,
which means that the height and width must always be the same.
The following rule for inheritance therefore holds:
e An object of a derived class must be able to represent every value an object of a base class
can represent.
Unfortunately, the inheritance relationship is often described as specialization (i.e. ‘a rectangle
is a special kind of a geometric object’). However, this is not meant in a limiting sense, but in
a sense to become more concrete. I therefore prefer to describe the inheritance relationship as a
concretion.
ie
#5
The decisive point is that the class FractionWithWholeNumber adds a new member that gives
a new meaning to the existing members of the class Fraction. If the object is initialized
with 3, the numer and denom members have a different internal state for Fractions and
FractionWithWholeNumbers. In FractionWithWholeNumber, the Fraction 42 would be
4
represented as 3+ (see Figure 5.16).
Note that the inheritance relationship means that a derived object can always be used
as an
object of the base class. In this case, the object is reduced to the members that already
exist in the
5.5 Design Pitfalls of Inheritance
347
base class. Therefore, 3+ would be implemented as +, which could lead to misleading results”.
The design error is that the inherited members are supplemented with new members, through
which their semantics change.
Therefore, another rule for inheritance is the following:
e The status that the members of a base class have to represent a certain value, must not be
different for a derived class to represent the same value.
Again, a class should not be derived in this case, so that this kind of error can be avoided.
This is clearly a has-a relationship. The name of the class, Fract ionWithWholeNumber,
expresses that two objects, a fraction and a whole number, are combined to make one object.
The class would therefore be better declared as follows:
class FractionWithWholeNumber {
private:
Fraction fraction;
int number;
Fs
However, in this example, there is another possible implementation. Inheritance can be used,
without giving inherited members different values for the same status represented by the object.
In order to do so, the implementation must be changed so that, when passed the value 3, the class
FractionWithWholeNumber initializes the member numer with the value 13 and the member
denom with the value 4. The only difference would then be that the object is output in a different
form (i.e. in two parts: a whole number and a fraction). Therefore, only the output operation is
rewritten. This way, inheritance causes no problems.
}3
In this case, a derivation with the class SignedFraction would be incorrect:
class SignedFraction : public Fraction {
private:
bool isNegative;
Ea
'” Do not assume that this could be a useful type conversion. Automatic type conversion should always be
obvious.
348 5: Inheritance and Polymorphism
The information content of a fraction is supplemented with the information that can change the
meaning of the inherited members. This is because the potential value range is doubled (all
positive and negative fraction values).
If, however, a negative fraction is assigned to a Fraction, - +2 suddenly becomes +3, which
is problematic. I can hear some readers claiming that this would be a nice ‘feature’. The only
question is whether this kind of behavior is so intuitive that it can take place without an explicit
call. It would be somewhat critical if, by mistake, you make a positive balance of half a million
euros out of a negative balance of half a million euros.
The design error is that the inherited members are supplemented by new members that cause
their values to change meaning.
Yet another rule for inheritance is therefore the following:
e The semantics of the members of a base class must not change for a derived class.
This design problem is also detected because an assignment to an object of a base class is not al-
ways useful for every value of an object of a derived class, which exposes this kind of inheritance
as limiting inheritance.
The only meaningful solution here is to implement an independent class with Fraction and
sign as members:
class SignedFraction {
private:
Fraction fraction;
bool isNegative;
te
The problem could also be solved by using a more descriptive naming convention: if the class
Fraction was called UnsignedFraction, it would be obvious that a SignedFraction was
not an UnsignedFraction.
Most newcomers to object-oriented programming tend to use inheritance too often. This is al-
ways problematic because inheritance is a closer binding between types than composition
or
containment. If just classes are only used, nothing happens as long as the public interface
is not
changed. Because derived classes also have access to protected members, modifications
in the
base class are more critical and can easily lead to inconsistencies. In particular, a derivation
of
third-party classes is always critical.
Therefore, I tend to recommend avoiding inheritance, making use of it only if something can-
not be implemented in another way. One should always question whether there is no
containment
implied.
If derivation is done, the semantics of inherited operations and members should
never be
limited or modified. Objects of derived classes should be able to be used
as objects of the
5.5 Design Pitfalls of Inheritance
349
base class, at any time, without limitations. This principle is known as the Liskov substitution
principle’.
Sometimes, it may be useful to implement a containment relationship by means
of private
inheritance (see Section 5.2.8 on page 295). This kind of inheritance is occasionally called
im-
plementation inheritance and clarifies the fact that it is not a matter of a conceptional inheritanc
e
in the sense of the is-a relationship, but a simple way of implementing a class that reuses
code
of another class. This should also be avoided. In the long term, a robust design with as few
dependencies as possible is well worthwhile.
5.5.6 Summary
e Not every is-a relationship you can formulate using your native English language is neces-
sarily a characteristic of inheritance. In particular, (public) inheritance should be avoided
— if not all inherited operations are still useful;
— if inherited operations get a different meaning;
- if inherited get a different meaning; or
— if the properties of members of the base class are limited.
e It must make sense to assign all possible objects of a derived class to an object of the base
class, simply by ignoring the members supplemented in the derived class.
e Avoid inheritance.
'! Due to Barbara Liskov, who formally introduced this principle for the first time in 1988.
; Ce
sail is lea ~ ——
ape
1 fag ne ae a
ety @etes ntietinls mira
Se OEY eFige?aa net&, Xsan 2° ae
We» taaitiee x sien s
plan ett ib wt
a hbigen aeartulia Wiire
enaiie x Wetale
ries 9Ae ee 0 Kempen eyed ae
Daal preg toe; ~~. der
7 7
* ={
“
P : | a
In the previous chapters, classes were introduced that had ‘simple’ members. This means that
they had members such as int, string or vector, which could be copied and assigned without
any problems. However, this is not the case with all types. Especially when using pointers, these
members are particularly difficult to copy or assign to each other. For this reason, one has to
interfere in operations such as copying, assignment and clean-ups. This chapter addresses this
topic under the heading ‘dynamic members’.
Static members are also explained in this chapter. These are members that are only contained
once in the program and are used by all objects/instances of a class.
A custom implementation of a string class and a simple class for representing information
about people are used as sample classes.
552 6: Dynamic and Static Members
#ifndef STRING_HPP
#define STRING_HPP
namespace CPPBook {
class String {
private:
char* buffer; // character sequence as dynamic array
unsigned len; // current number of characters
unsigned size; // size of buffer
public:
// default and C-string constructor
ptring (const char* =").
// comparison of strings
friend bool operator== (const String&, const String&);
friend bool operator!= (const String&, const Strings);
// concatenating strings
friend String operator+ (const String&, const String&) ;
// output to a stream
void printOn(std::ostream&) const;
// number of characters
unsigned length() const {
return len;
private:
/* constructor from length and buffer
* - internal for operator +
Ef
String(unsigned, char*) ;
‘3
/* operator !=
* - implemented as inline conversion to operator ==
We
inline bool operator!= (const String& s1, const String& s2)
1)
return !(si==s2);
#endif //STRING_HPP
Let us first look at the members that describe the internal layout of a string:
class String {
private:
char* buffer; // character sequence as a dynamic array
unsigned len; // actual number of characters
unsigned size; // size of buffer
}
A String object comprises a dynamic pointer buffer, which manages the actual characters
of the string as an array of chars, as well as two members that manage the actual number of
characters in the string and the amount of memory that belongs to it.
The character sequence itself does not directly belong to the object, but is instead managed
as dynamic memory. Figure 6.1 clarifies this. The string s, in its current status, contains the
character string hello, with five characters. These characters are stored in a separate memory
segment for eight characters, to which buffer points.
Alternatively, an array with a fixed size could be declared. However, this has the disadvantage
that the number of characters in the string is noticeably limited and/or a lot of memory
is wasted
for small strings.
The dynamic memory managed via buffer does not automatically belong to the object,
but
must be explicitly created when the object itself is formed. The memory has to
be allocated
6.1 Dynamic Members
B55
where necessary if manipulations are carried out, and freed again when the object is destroyed.
This is possible because the implementation of all operations is left entirely under the control of
the class programmer.
Each constructor must therefore allocate memory explicitly, in order to be able to use this
for character strings. This includes the copy constructor (copy constructors are introduced in
Section 4.3.7 on page 179), which has to be implemented for this reason. The default copy
constructor just copies member-wise, which means that it would just copy the pointer, instead of
creating a new array and actually copying the character sequence of the source string.
For the same reason, the assignment operator must also be implemented. The default assign-
ment operator, which assigns member-wise, would only assign the pointer and not the character
sequence to which the pointer refers.
Finally, the memory that is explicitly created for every object has to be freed when the object
is destroyed. For this reason, the destructors (the counterpart to the constructors) are defined. A
destructor is called when an object is destroyed, and enables it to be ‘cleaned up’. The function
name of the destructor is the class name, with a ~ character preceding it:
class String {
public:
eString()- // destructor
}3
The CPPBook: : String class is implemented using the operators introduced in Section 3.8 on
page 114 for dynamic memory management, new[] and delete[]. Apart from the function
that reads a string from a stream, the dot-C file has the following contents (the input function is
discussed in Section 6.1.7 on page 366):
// dyna/stringla.cpp
namespace CPPBook {
/* copy constructor
ol
String: :String(const String& s)
/* destructor
*/
String: :~String()
4
// release memory allocated with new[]
delete [] buffer;
/* operator =
* - assignment
*/
Stringk String: :operator= (const String& s)
{
// assignment of a string to itself has no effect
if (this == &s) {
return *this; // return string
/* operator ==
* - compares two strings
* - global friend function, so that an automatic
* type conversion of the first operand is possible
i
bool operator== (const String& sl, const Stringk& s2)
{
return si.len == s2.len &&
std: :memcmp(s1.buffer,s2.buffer,si.len) == 0;
}
/* operator +
* - appends two strings
* - global friend function, so that an automatic
* type conversion of the first operand is possible
af,
String operator+ (const Stringk si, const String& s2)
{
// allocate buffer for the concatenated string
char* buffer = new char[si.len+s2.len];
af
String: :String(unsigned 1, char* buf)
4
len = 1; // copy number of characters
size = len; // number of characters determines size of memory
buffer = buf; // copy memory
}
/* output to stream
«7
void String: :printOn(std::ostream& strm) const
a
// simply output character string
strm.write(buffer,len);
} // OBR 8 END namespace CPPBook 38 3898OR2k2k2k2s2 2k2k2s2k2k2 2 2kof2 ok2 ok okof3 2K ok okok2KokOK
buffer:
Next, len and size are initialized. The length of the passed C-string is determined using
strlen():
len = std::strlen(s);
size = len;
memory is allocated that is large enough for the character sequence, and its address is assigned
to the buffer of the String object. The content of this memory is not defined initially either:
buffer:
the individual characters of the character string are copied. The standard memcpy() function
adopted from C copies the number passed to characters of an array of char (or bytes). All len’s
characters are copied from s into the memory, to which buf fer points;
The C-string constructor can be called as a default constructor using the default argument (an
empty string). This creates a String object, where buffer points to a character string without
elements:
buffer:
Here, we take advantage of the fact that an array with no elements can be created using new. In
commercial implementations, however, a special treatment of the empty string would be better,
as the allocation of memory takes some time. In this case, buffer would be initialized with
NULL internally. However, this special case must be handled with each access to the characters
of the string.
Note that NULL cannot be used as a default argument for the string constructor, because the
string functions that are used internally cannot be called with NULL as an argument (calling
strlen (NULL) would cause a fatal error (core dump) on many systems). Thus a special treat-
ment is needed when using NULL as a default value. However, as the standard string classes do
not implement such a test, it is also omitted here.
6.1.4 Destructors
With the String class, the destructor is used to free the explicitly allocated memory when the
String is destroyed. As it is an array of characters, the array syntax of the delete operator has
to be used:
strings: String()
{
// release memory allocated with new []
delete [] buffer;
Y
The destructor is called immediately before the memory for the actual object is freed. For exam-
ple, this is the case if the scope of a locally created object is left:
void f()
void f£()
{
CPPBook: :String names[10]; //ten calls of the default constructor
names[0] = newString("hello") ;
// first assignment of the temporary return value to names [0],
// then destructor for temporary return value
return ret;
// copy constructor for temporary return value
} // end of block: destructor for ret called
362 6: Dynamic and Static Members
if ((a = b) != CPPBook::String("")) {
}
Generally, the typical head of the function of an assignment operator is therefore:
class& class: :operator = (const class& obj)
e As it usually makes no sense to carry out other manipulations with the temporary
return value
inside the same expression, the return value can also be declared as being constant.
The head
of the function of an assignment operator can therefore also look as follows:
6.1 Dynamic Members 363
sp = &s; // Sp points to s
Two strings are the same if the character strings are the same. The memcmp() function can be
used for the comparison (it returns O if two character sequences are equal). However, it is checked
in advance whether the number of characters is the same:
bool operator== (const String& si, const Stringk s2)
{
return si.len == s2.len &&
std: :memcmp(s1.buffer,s2.buffer,si.len) == 0;
364 6: Dynamic and Static Members
The AND operator && evaluates the left and right side only until the first subexpression evaluates
to false. Thus, if the number of characters is different, the call of memcmp () is not performed.
The function is declared as a global friend function in order to enable an automatic type
conversion for the first operand (see also Section 4.6.3 on page 220):
CPPBook: :String s;
The + operator is used for concatenating two strings. It is also defined as a global friend function
in order to enable an automatic type conversion for the first operand:
class String {
public:
// copy characters
std: :memcpy(sum.buffer,s1.buffer,s1.len);
std: :memcpy
(sum. buffer+s1.len,s2.buffer,s2.len);
private:
String(unsigned, char*);
35
Now the + operator can be implemented so that the buffer of the concatenated string can be
created with the correct size, and be initialized at the same time:
String operator+ (const String& si, const String& s2)
{
// allocate buffer for the concatenated string
char* buffer = new char[s1.len+s2.len];
// dyna/string1b.cpp
// 8K 2 ok BEGIN namespace CPPBook HR2B 5 9 24628fe2k24g218fe2s 2 ok9K2k3 2kae2sae2 2k2 ok okokokokok
namespace CPPBook {
ibeCstd:<isspace(a))e{
return;
i
In principle, the input function is implemented so that it reads a string as a word, skipping leading
whitespaces. The input is completed with a whitespace or the end of the input (i.e. end of file,
EOF).
After the length of the string to be read has been set to 0, leading whitespace is skipped:
std::strm >> std::ws;
The std: :ws manipulator takes on this work. The name stands for ‘whitespace’, such as new-
lines, tabs and spaces.
Finally, a loop is run that reads and processes a character:
while (strm.get(c)) { //while character c is successfully read
// process c
y
The get () member function is used to read the next character. The >> operator cannot be used
here, as it skips leading whitespace; thus we could never read a whitespace with it. However, we
need to know whether a whitespace was read, as this would terminate the string input.
368 6: Dynamic and Static Members
The get () function returns the stream that was read from. This is then used as a condition
for whether the loop should carry on running. As explained in Section 4.5.2 on page 201, the
condition is only met if the stream is fine (has neither EOF nor an error state). The loop therefore
runs for as long as an individual character can be successfully read.
A test is carried out in the loop to find out whether a whitespace was read. If this is the case,
it is seen as the end of the input, and the function is exited!:
if (std::isspace(c)) {
return;
d.
If no whitespace is encountered, the new character needs to be inserted into the internal character
array. To do this, we must ensure that there is enough space available for it. It is a common error
to assume that an input can only be 80 characters long. The fact is that an input can have any
length (for example, a data stream only has a separator after 10000 characters). If the memory
space is no longer sufficient, new memory of more than double the previous size is allocated, and
the characters read so far are copied to it:
if @lena>=—size) me
char* tmp = buffer; // pointer to old memory
size = size*2i + 32; // increase size of memory
buffer = new char[size]; // allocat
new memory
e
std::memcpy(buffer,tmp,len); // copy characters
delete [] tmp; // release old memory
}
The new character can now be appended, and the number of characters is incremented accord-
ingly:
buffer[len] = ¢;
++len;
A consequence of the usual optimization with commercial string classes is that most functions
are implemented inline. As header files are readable (they must be, in order to be included),
I
recommend that you take a look at one.
Reference Counting
One optimization possibility, which also demonstrates the powerful abilities of programming
classes in C++, is the use of a technique called reference counting. This is based on the premise
that copying and assigning strings is expensive, occurs very frequently and that strings are hardly
ever manipulated (that is, individual characters are only very rarely changed).
The trick is that a string object itself is only a simple handle that refers to the actual string ob-
ject (the so-called body). The actual data of a string, with the actual character string, is therefore
moved into an auxiliary class:
class StringBody {
private:
char* buffer; // character sequence as a dynamic array
unsigned len; // current number to characters
unsigned size; // size of buffer
unsigned refs; // number of strings that use this body
};
The objects of the StringBody auxiliary class manage the actual character string, and can be
used by several strings that all have the same value. The refs members determines how many
strings share this body.
With every initialization, a body object is created with the actual data and a handle. In the
body, it is initialized that there is exactly one owner (refs is set to 1). The handle itself is a fairly
simple object that only refers to the body:
class String {
private:
StringBody* body; // pointer to the actual string
3;
If a string is now copied, only the handle is copied, and the number of strings that use this body
gets incremented. Two strings then use the same character string. Conversely, if a string is
destroyed, the number of strings that refers to it is decremented appropriately in its body. If this
number is 0, the body object itself is destroyed.
Every read access to a string (for example, a query of its length) is simply passed on to the
body. Only if a string is changed, whose body is shared by more than one string, is a true copy
of the body created and assigned to the string that gets modified.
Because passing of strings (copying and assigning) typically happens more often than changes
to individual characters, we avoid many explicit memory management operations, and thus save
a lot of time.
370 6: Dynamic and Static Members
.
Another typical optimization is the inclusion of special functions for substring processing,
which is also carried out using internal auxiliary classes.
Reference counting can also be implemented using special smart pointers. This is discussed
in Section 9.2.1 on page 536.
No Reference Counting
According to the explanation of the optimization facility with reference counting, we could now
expect every implementation of the std: : string class to be optimized in this way. However,
this is incorrect.
Recently, it turned out that, in multi-threading programs, this kind of optimization with string
classes can be counterproductive. The price of increased complexity, when added to the overhead
of managing locks to handle the shares, is often higher than the benefit gained from avoiding
copying. For this reason, all string implementations have removed this kind of optimization.
Reference counting would only make sense if it was guaranteed that strings were not modified
during their lifetime’.
Instead, other kinds of optimizations have evolved, which are mostly based on the observation
that most strings consist only of a very few characters. The good thing with C++ is that one
can automatically benefit from this kind of knowledge and improvement, because the interface
remains the same.
public:
File(const char*); // const char* constructor
for the filename
SET leQ): // destructor
ing
° The Java approach for handling strings turns out to be better here, as Java defines two types of string:
one
where you cannot manipulate characters and one where you can. However, in Java, strings cannot easily
be compared with the == operator. (I think that I still have to write my own string class or programming
language.)
6.1 Dynamic Members
Sy)
File: :~File()
{
// close file
fclose(fp);
}
In the application program, the file is automatically opened using the declaration of an object,
then closed again when the block is exited:
void f()
{
File d("testprog.cc"); // constructor opens thefile
However, a great deal of care has to be taken with the implementation of these kinds of
classes. Much thought and consideration must go into the implementation of the copy constructor
and the assignment operator. (For example, what would happen during the copying of a file or a
transaction?) In any case, the default implementation usually does not work.
The following can be used as a guideline (often referred to as the rule of three):
e Acclass needs a copy constructor, an assignment operator and a destructor, or none of these.
This means that if a custom implementation is necessary for either a copy constructor, or the
assignment operator, or the destructor, then a custom implementation is usually required for all
of these operations.
If it is not clear whether a copy constructor or an assignment operator is needed, one can
simply prevent copying and assignment. To do this, the copy constructor or the assignment
operator simply have to be declared as being non-public. This is discussed in Section 6.2.5 on
page 383.
6.1.10 Summary
e Classes can have dynamic members. These are members that cannot simply be copied by
assignment (typically pointers).
e Classes with dynamic members need a separate implementation of the copy constructor, the
assignment operator and the destructor.
e A destructor is a function called automatically with the destruction of the object.
e An assignment operator should begin with a test that checks whether an object is being as-
signed to itself.
e An assignment operator should return the object to which something was assigned (that is, it
should return *this).
e Using dynamic members, complex processes can be abstracted so that, when using an object,
they are easy to handle.
e The ability to copy and assign objects of a class can be prevented.
e Acclass usually needs a copy constructor, an assignment operator and a destructor, or none of
these.
6.2 Other Aspects of Dynamic Members
Sy
sl eS Jers
the string s should now have the value ‘grey’ (rather than ‘gray’). In order to do this, the []
operator, which is called in the expression s [2], needs to return an internal part of the string (or
access to an internal part of it), so that the string can be manipulated.
This is only possible if the [ ] operator grants access to the original characters of the string,
rather than returning a copy, so that it can then be manipulated. This is easily done by returning
a reference.
The operator must therefore be declared as follows:
namespace CPPBook {
class String {
private:
char* buffer; // character sequence as a dynamic array
unsigned len; // current number to characters
unsigned size; // size of buffer
public:
char& operator [] (unsigned); // access to a character
374 6: Dynamic and Static Members
te
}
The implementation can then look as follows:
// dyna/stridzvar.cpp
return buffer[idx];
After the test to see whether the passed index is valid, the character at index idx of the internal
member buffer is returned as a reference. The returned character of the function is therefore
not a copy, but the original character of the string, and can also be manipulated.
However, the implementation of the [ ] operator for strings as introduced above is not suitable
for constant strings, as the operator would enable manipulation of the string. An obvious solution
might be to declare it as a constant member function:
namespace CPPBook {
class String {
public:
char& operator [] (unsigned) const;
+s
}
However, this results in the same problem:
const String s = "gray";
This is wrong! The problem here is that the actual object is not being changed. Its buffer
member is only a pointer to the character string, and this pointer remains constant. However,
what the pointer refers to is not constant (which is fine, because internally we have to change the
characters of the string).
However, the operator should be defined for constant objects. It would be quite unreasonable
if it were possible to access a character in a variable string only using the [] operator. The
following should therefore also be possible:
const CPPBook::String s = "gray";
char c = s[0];
The solution is to provide two implementations of the [ ] operator: one for constants and one for
variable strings. This kind of differentiation is possible. A function can be overloaded for both
variables and constant objects (the function for constant objects is only used for variable objects
if there is no separate function for variable objects).
For this reason, the CPPBook: : String class should contain a second declaration for the [ ]
operator:
namespace CPPBook {
class String {
private:
char* buffer; /| character sequence as a dynamic array
unsigned len; // current number to characters
unsigned size; // size of buffer
public:
// operator [] for variables
char& operator [] (unsigned);
33
}
The statements for constant strings in the body of the function are no different from the version
for variable strings:
// dyna/stridzconst.cpp
return buffer[idx];
The only difference is that the character is no longer returned as a reference, but as a copy. For
performance reasons, if the return value is a larger object, a constant reference should be used
instead:
namespace CPPBook {
class String {
public:
const char& operator [] (unsigned) const;
class String {
private:
char* buffer; // character sequence as a dynamic array
unsigned len; // current number to characters
unsigned size; // size of buffer
public:
char* toCharPtr() const;
33
// copy characters
std::memcpy(p, buffer, len);
// and return
return p;
}
However, This solution has several disadvantages:
e On one hand, the explicit creation of memory takes time.
e One the other hand, because extra memory is created, the application program must ensure
that it is also freed: a requirement that will sooner or later lead to a memory leak.
If this kind of implementation is used for a type conversion, at the very least, the function
name should indicate that memory is created explicitly, which then needs to be freed at a later
date (for example, the name asNewCharPtr ()).
It is usually better to return the internal character string and declare the return value so that the
data cannot be changed:
class String {
private:
char* buffer; // character sequence as a dynamic array
unsigned len; // current number to characters
unsigned size; // size of buffer
378 6: Dynamic and Static Members
public:
const char* toCharPtr() const;
3
With classes that contain dynamic members, the automatic type conversion is often used for
testing conditions. Following the traditional C code of
FILE* fp; // pointer to the opened file
toe Ci pet
// read data
* Caution: this does not mean that it is safe to assume that data() also ends with the end-of-string character.
This is only the case with most implementations. Anyone who needs an end-of-string character should
therefore always use c_str().
6.2 Other Aspects of Dynamic Members 379
}
else {
//error:
£p is NULL
}
with a class for opened files, the following is made possible:
CPPBook::File f("hello"); //constructor
opens the file (hopefully)
if eCfet
// read data
;
else {
// error: £ is not OK
M
In C, this technique is based on the fact that NULL often shows an error status with pointers. NULL
is, however, simply the value 0, which, in turn, denotes false. The test
if (fp)
is therefore an abbreviation for
if (fp != NULL)
(It could be argued at this point whether or not this improves the readability of the program.)
Conversion to C++ can now be seen from the same point of view. Conditions in control
constructs can be objects as long as an automatic type conversion into an integral or a pointer
type is defined. If the integral or the pointer type has the value 0, the condition is not met.
Therefore, in order to enable the above if-query, a type conversion into a pointer type has to
be implemented. For a class for opened files, this may look as follows:
namespace CPPBook {
class File {
private:
FILE* fp; // pointer to the opened file
public:
the call
12. (t)
is evaluated as follows:
if ((f.operator FILE*()) != NULL)
However, providing such an automatic type conversion conceals the danger of losing the whole
advantage of a secure interface. With a type conversion defined in this way, there is now a
facility for accessing a private member and then modifying it. For example, the following would
be possible, without an error message:
CPPBook::File f;
FILE* fp;
tio) SS abe
The first improvement would therefore be the conversion of the pointer into the void type:
namespace CPPBook {
class File {
private:
FILE* fp; // pointer to the opened file
public:
operator void* () {
return (void*)fp;
};
}
However, an internal member is still exported to the caller. It is even better to make sure
that no
internal address is exported:
namespace CPPBook {
class File {
private:
FILE* fp; // pointer to the opened file
public:
operator void* () {
return fp != NULL ? reinterpret_cast<void*>(32)
static_cast<void*>(0);
6.2 Other Aspects of Dynamic Members 381
Is
}
In this case, the values 32 and 0 are converted into addresses using the operators for type conver-
sion. While the use of 0 as an address is possible, and is therefore sufficient for the conversion of
static_cast, | has to be converted using the ‘most dramatic’ of all type conversion operators,
the operator reinterpret_cast.
The best thing to do is to avoid functions for automatic type conversion. A member function
for explicit type conversion such as isOK() also works and leads to more-readable user code:
if, (£.4s0KQ))
The standard classes for I/O use the technique that has just been introduced. In the process, the
operator ! is also overloaded in order to enable something like
cl LG ee}
With complex classes, it may be worthwhile, on the initialization of objects, to refrain from cal-
culating every member that could be of interest. An appropriate calculation could be postponed
to the moment when the information is needed for the first time. This kind of programming
technique is described as /azy evaluation.
An example uses the class for opened files. In order to determine how many lines a file has,
the whole file has to be run through, and the number of newlines counted. This takes quite some
time with large files. For this reason, the number of lines is better kept in an internal member and
only determined when the number is requested:
namespace CPPBook {
class File {
private:
FILE* fp; // pointer to the opened file
ant: lines; // number of lines (a value of -1 denotes ‘not yet known’)
public:
// constructor
File(...) : lines(-1) { //number oflines not yet known
public:
// constructor
File(...) : lines (-1) { // number of lines not known atfirst
“ This example assumes that the constancy of a file determines whether write access
is possible. The
standard types for file access uses a different approach, namely it provides different
types for read and write
access.
6.2 Other Aspects of Dynamic
a eae Members
ee 383
i ee a AD
namespace CPPBook {
class File {
private:
FILE* fp; // pointer to the opened file
public:
private:
File(const File&) ;
File& operator= (const File&);
33
}
The declaration of this standard operation as private is enough; no implementation is necessary.
Passing a parameter is then only possible by means of references:
void withCopy(CPPBook:
:File) ;
void withoutCopy(const CPPBook: :File&) ;
void f()
{
CPPBook::File f("prog.dat") ;
CPPBook: :File g("prog.old") ;
withoutCopy(f); //OK
withCopy(f) ; // ERROR: copies notallowed
g =f; // ERROR: assignments not allowed
The best solution is to never give up control. This is also possible when you
return or gain access
to parts of an object. The trick is to maintain control of the return value. Of
course, this should
not lead to more complicated or less intuitive interfaces. Instead, we use so-called
proxy classes.
6.2 Other Aspects of Dynamic Members 385
A proxy can be described as a wrapper that, without changing the interface, gives control over
something that one normally has no control.
For example, in the string class, the index operator can be implemented so that a special type
is returned that behaves like a char, but does not permit all operations chars provide:
class String {
public:
/* proxy class for access to individual characters
fe
class reference {
friend class String; // String has access to private members
private:
char& ch; // internal reference to a character in the string
le
public:
// access to a character in the string
reference operator [] (unsigned);
char operator [] (unsigned) const;
386 6: Dynamic and Static Members
In the String class, the nested class reference is defined. This class represents references to
characters in a String.
This kind of object is returned by the index operator for variables:
String: :reference String::operator [] (unsigned idx)
{
// index not in permitted range?
if (idx >= len) {
throw std::out_of_range("string index out of range");
}
return reference(buffer[idx]) ;
}
The return value is initialized with the corresponding character inside the string, to which we
grant access. Thus the reference member ch will be initialized with this character. In this way,
we maintain control of the operations that are carried out with the return value of the index
operator because we can program what is possible with a string: : reference and what is not.
The use of this reference is shown in the following example:
// dyna/stringtest2. cpp
int main()
<
typedef CPPBook: :String string;
In the statement
char c = firstname[0];
6.2 Other Aspects of Dynamic Members
Se 387
ee ee a PR
the expression firstname [0] yields a string: : reference, which, by means of the conver-
sion function operator char() (see Section 4.6.4 on page 227), can automatically be con-
verted into, and thus assigned to, a char.
With
firstname[0] = lastname[0];
both sides of the assignment provide a string: :reference. The assignment operator then
allows an appropriate assignment.
With
lastname[0] = c;
the second form of the assignment operator is used, which allows a string reference to be assigned
a char.
Copying, and every other operation using the return value of the index operator, is not al-
lowed:
++firstname [0]; // error: ++ not defined for string: :references
According to this pattern, control can always be maintained over nested expressions. You only
have to make sure that you return one of your types to the applications programmer. However,
the automatic type conversion from the proxy to the type it wraps makes sure that any read-only
operation of the returned value is still possible.
The String class (as introduced in Section 6.1 on page 352 and extended in Section 6.2.1 and
Section 6.2.6) could define a special error class in which the error objects have the invalid index
as a member:
// dyna/string3.hpp
namespace CPPBook {
class String {
public:
class reference {
is
388 6: Dynamic and Static Members
public:
By using an initializer list°, the constructor of the CPPBook: : String: :RangeError class makes
sure that, during the creation of an error object, the index member is initialized with the invalid
index passed as parameter i.
In the implementation of the operator [ ] for variables, an appropriate object has to be created
as an exception when an invalid index is found. The index is then passed to this exception:
// dyna/string3.
cpp
return reference(buffer[i]);
}
4
// index not in permitted range?
af (i >= len) >
/| throw exception with invalid index
throw RangeError
(i) ;
}
return buffer[il];
}
If the error now appears, the application program can not only recognize that the error has oc-
curred, but can also retrieve the invalid index. For example, it would be possible to output the
invalid index in an error message:
// dyna/stringtest3.cpp
int main()
sf
Leyt
}
catch (const CPPBook::String::RangeError& error) {
// exit main() with error message and error status
stdy;cerr << "ERROR: invalid. index " << error, index
<< " when accessing a string" << std::endl;
return EXIT_FAILURE;
ib
t
A name must be declared for the object in the catch block, in the same way as with functions,
in order to be able to access the members. In the catch block, the index member in the error
object is accessed directly. For this reason, it is declared as public. It is not necessary to define
the member as private, and define a member function for access, because data encapsulation is
not so important here. The error object is not used to represent a state over some period of time.
Instead, it is used to signal the error and pass the corresponding data. Its life cycle ends with the
end of the error handling.
If you want to pass the string that produced the error as a parameter of the exception object,
you have to consider the following: This string has to get be copied, because it may be a local
object that no longer exists when, during the processing of the exception (stack unwinding), its
scope is left. The string therefore must not be declared as a pointer or a reference, but must be
390 6: Dynamic and Static Members
*
declared as a normal member, of type String, in the exception class. However, the following is
not possible:
namespace CPPBook {
class String {
public:
// error class:
class RangeError {
public:
String value; // ERROR: type String incomplete
es
}
This is an error because String is used as a plain type meanwhile it is declared. Therefore,
String is incomplete when the member value of the nested class is declared. Instead, you have
to forward declare the nested class and define it afterwards:
// dyna/string4.hpp
namespace CPPBook {
class String {
public:
class reference {
ie
// error class:
// - forward declared because it contains a String
class RangeError;
public:
class String::RangeError {
public:
int index; // invalid index
String value; // string for this purpose
6.2 Other Aspects of Dynamic Members BM
Now when an invalid index is found, both the string and the invalid index are passed for the
initialization of the exception object:
// dyna/string4.cpp
return reference(buffer[i]);
return buffer[il];
// dyna/stringtest4.
cpp
int main()
{
tryet
ay
catch (const CPPBook: :String::RangeError& error) {
// exit main() with error message and error status
std::cerr << "ERROR: invalid index " << error. index
<< " when accessing string \"" << error.value
<< UNH << istd send]:
return EXIT_FAILURE;
}
;
As the object that threw an exception cannot be passed as a parameter to the exception without
being copied, the identity of the object can no longer be determined. If this information is
required, either object IDs (see Section 6.5.1 on page 414) or the address of the object
as a
pointer of the const void* type need to be passed. This ID/address can then be compared
with
the ID/address of the known object when dealing with the exception.
6.2.8 Summary
e Functions can be overloaded differently for variable and constant objects.
e Ifa member function provides write access to internal members, it must be ensured
that the
function cannot be called for constants.
e Functions that convert dynamic members into other types can return a copy
or a constant,
with different advantages and disadvantages.
e Automatic type conversions can be defined for conditions in control construct
s that allow an
object to be used directly as a condition. This should be used with care.
By doing this, no
access to internal members should be possible.
e The mutable keyword allows the modification of a member,
even for constant member func-
tions. Semantically, this means that, from an application programmer’s
point of view, the
member has no relevance to the state of an object (thus, even when modifyi
ng it, the object
logically stays constant).
e If copying and assigning is not useful, calling the copy constructor
and assignment operator,
respectively, should be forbidden. To do so, it is sufficient to declare
them as private.
e In nested operations, proxy classes allow control to be maintained.
e Exception objects can have members. If the error-producing object
is passed, a copy of it
must be created.
6.3 Inheritance of Classes with Dynamic Members 393
#ifndef STRING_HPP
#define STRING_HPP
namespace CPPBook {
class String {
public:
class reference {
friend class String; // String has access to private members
private:
char& ch; // internal reference to a character in the string
reference(char& c) : ch(c) { // constructor
}
reference(const reference&) ; // copying forbidden
394 6: Dynamic and Static Members
public:
reference& operator= (char c) { // assignments
chy ="e%
return *this;
M
reference& operator= (const reference& r) {
Che= nach:
return *this;
}
operator char() { // use as Charcreate
a copy
s
return) ch:
ty
//error class:
// - forward declared because it contains a String
class RangeError;
protected:
char* buffer; // character string as dynamic array
unsigned len; // current number of characters
unsigned size; // size ofmemory of buf fer
public:
// default and char* constructor
String(const char* = "");
// comparison of strings
friend bool operator== (const String&, const Strings);
friend bool operator!= (const String&, const String&) ;
// output to a stream
virtual void printOn(std::ostream&) const;
6.3 Inheritance of Classes with Dynamic Members 295
// number of characters
// note: cannot be overlooked during derivation
unsigned length() const {
return len;
private:
/* constructor from length and buffer
* - internally for operator +
ai
String(unsigned, char*) ;
33
class String::RangeError {
public:
int index; // invalid index
String value; // string for this purpose
<
s.scanFrom(strm); // read string from stream
396 6: Dynamic and Static Members
Y
/* operator !=
* - implemented inline as conversion to operator ==
“ld
inline bool operator!= (const String& si, const String& s2) {
return !/(si==s2))-
} // 38K oo END namespace CPPBook 388HR28OS 2828iS2482 ak2k2k2 2k2k2 2k2k2K2 ok OK okKOK okoeOKOK
#endif //STRING_HPP
It is worth noting that the member function length() is not declared as virtual. This is a design
decision in favour of an improved running time (being virtual, inline processing is not possible
for references and pointers; see Section 5.2.2 on page 283). The consequence of this decision
is that there are problems if a derived class overrides this function and an object of the derived
Class is then used in the base class. Or, in other words, this member function is not provided
for overriding. As the query of the number of characters cannot be implemented differently, this
limitation is probably acceptable. (You might argue that the meaning of the buffer member can
be changed; however, this would violate the basic rule that members in derived classes should
keep their meaning; see Section 5.5.3 on page 347.)
The header file of the class ColString derived from String is as follows:
6.3 Inheritance of Classes with Dynamic Members 397
// dyna/colstring1.hpp
#ifndef COLSTRING_HPP
#define COLSTRING_HPP
/* class ColString
* - derivedfrom String
z/
class ColString : public String {
protected:
String col; // colour ofthe string
pubilere:
// default, String and String/String constructor
ColString(const Stringk s = "", const Stringk c = "black")
Siolrang(s)Lucor(cjnt
//comparison
of ColStrings
friend bool operator== (const ColString& sl,
const ColString& s2) {
return static_cast<const Stringk>(s1)
== static_cast<const Stringk&>(s2)
&& si.col == s2.col;
398 6: Dynamic and Static Members
.
ine
#endif // COLSTRING_HPP
First, the new member for color is defined. As the color symbol is used for the member func-
tions that set and query the color, the member has the name col (meaningful names in interfaces
are more important)°:
class ColString : public String {
protected:
String col; the string
// color of
3
The constructor is defined next. Up to two strings can be passed as parameters. The first string
is the initial character sequence (default: empty string) and the second string is the initial color
(default: ‘black’):
class ColString : public String {
public:
// default, String and String/String constructor
ColString(const Stringz s = "", const Stringk c = "black")
wotring(s),ccol.(c) 4
im;
In the initializer list, the first string s is passed as the initial character sequence of the String
base class. The second parameter is used to initialize the col member.
The color() member function is overloaded, so that it can be used for setting as well as
querying the color. This is a common technique in C++. If no parameter is passed, it provides
the current color; if a parameter is passed, this is the new color:
° There is no general solution for the dilemma of name conflicts between a simple member and
the member
functions that set and query it. An underscore may be placed in front of the member
name, it may be
abbreviated, and often the member functions begin with prefixes such as get and set (that
is why these
functions are also called getters and setters).
6.3 Inheritance of Classes with Dynamic Members 399
public:
const String& color() { // query color
return col;
iF
void color(const String& newColor) { //setcolor
col = newColor;
Ls
The member functions for setting and querying the color are not defined as virtual in this case.
This means that they are not suitable for overriding during derivation. Once again, this is a design
decision in favour of an improved running time. As setting and querying the color cannot be
implemented differently, and the semantics of members should never change in derived classes,
this limitation is acceptable once more. However, when the color is modified, this means that
it is no longer possible to carry out other actions in the derived classes (unless no pointers or
references to base classes are used, which is almost impossible to ensure).
Functions for I/O are, as in the base class, defined as virtual, so that the correct I/O function
is called:
class ColString : public String f
public:
virtual void printOn(std::ostreamk) const;
virtual void scanFrom(std::istream) ;
¥3
public:
friend bool operator== (const ColString& s1,
const ColString& s2) {
return static_cast<const Stringk>(s1)
== static_cast<const String&>(s2)
&& si.col == s2.col;
I;
Using the stat ic_cast<> operator, the parameters s1 and s2 are explicitly converted into the
const String& type. By doing so, it is guaranteed that the equality operator is called for the
type of the String base class. The additional test for equality is added for color, which leads to
a further call of the equality operator of the String class, because the col member has this type.
There is now an equality operation for two Strings and two ColStrings. But what happens
if a String is compared with a ColString? In this case, there are two possibilities:
e The ColString is implicitly converted into a String. This is possible because each object
of a derived class can, in principle, be used as an object of the base class, and therefore an
automatic type conversion of ColString into String is defined.
e The String is converted into a ColString. This is possible because there is a constructor
for the ColString class to which a String can be passed as a parameter. This constructor
automatically defines an appropriate type conversion.
The expression may appear ambiguous, which you might would cause problems when compiling
the program. However, in C++, an automatic type conversion predefined by the language has a
higher priority than user-defined functions, such as constructors and conversion functions (see
also Section 10.3 on page 573). Therefore, the comparison operator for two strings is called.
Friend functions are, in principle, not virtual. Calls of a friend function are always bound
to
the type the parameter has at compile time (even for pointers and references). This means
that
the comparison operator for the String type is called if two ColStrings are compared
using
pointers or references to Strings.
Anyone who wants the correct function to be called for the function at run time should
define
the operator as an member function. An additional global operation can then be defined,
which
compares an object of the const char» type witha String or with aColString.
Another pos-
sibility would be to implement the operator as a global function, which is not a friend
function,
but calls an internal auxiliary function instead (another example demonstrating this
follows).
As friend functions cannot be virtual, the test for inequality must also be
reimplemented,
although it is implemented just as in the base class. It simply yields the negated
result of the test
for equality:
6.3 Inheritance of Classes with Dynamic Members 401
pubsirer
friend bool operator!= (const ColString& si,
const ColString& s2) {
return ! (si==s2);
3;
Because the implementation of the base class is not a virtual function, its test for inequality
always calls the test for equality for objects of type String. This would then also be valid for
use with ColStrings. Color would therefore no longer play a role. For this reason, the operator
must be reimplemented.
It is worth noting that friend functions are a problem when used with inheritance. They are not
inherited, but can be called for objects of the derived class. The objects then become objects of
the base class, which, during the call of auxiliary functions in the friend functions, can lead to
the wrong functions being called.
It is also not permitted for an operation to be defined as an member function as well as a
friend function:
class String {
public:
bool operator== (const String& s2);
friend bool operator== (const String& sl,
const String& s2); //ERROR
t3
Therefore, as long as a class is being used with inheritance, friend functions should, in principle,
be omitted. The automatic type conversion for the first operands can be made possible by using
additional global auxiliary functions instead.
Another possibility is the technique used with I/O operators. These are defined as global
functions, whose only purpose is to call corresponding virtual function that do the actual work.
In the base class, this would look as follows:
class String {
public:
virtual bool equals(const String& s2); // compare Strings
402 6: Dynamic and Static Members
Y
public:
virtual bool equals(const ColString& s2) {
return String: :equals(s2) && col == s2.col;
iy
x3
namespace CPPBook {
/* output to stream
ts,
void ColString: :printOn(std::ostream& strm) const
i
// output character sequence with colour in brackets
String: :printOn(strm) ;
Strme << Or Cine w<< col. <<a jar
} // o8 EK END namespace CPPBook 318HGOASSISfg248SSfeoft2ka 2kOK9 2kota of2 2k2 okok2 ok oi2k OKOKokokok
The function for input reads two strings: one for the content and one for the color. A more
complicated implementation could interpret the color as an optional input by evaluating whether
the opening and closing parentheses exist.
int main()
{
CPPBook::ColString c("hello") ; // ColString with default colour
CPPBook: :ColString r("red", Wsereyal)) < // ColString with colour red
FarGl?
© erie << << W UW wed se Ke Chel? ©endl; // output ColStrings
ell] 2 Gl? e
std::cout << "modified string: UR<<SCe<<ustd=,endl?
404 6: Dynamic and Static Members
public:
ColString(const ColString& s) // copy constructor
ptring (es). col(sscol) ={
A
Je
The implementation of a custom assignment operator is similar. First, the assignment operator
of the base class is called, and then all new members are assigned:
ColString& ColString::operator= (const ColString& s) {
// assignment of aColString to itself has no effect
if (this == &s) {
return *this; // return ColString
6.3.7 Summary
e Friend functions can never be virtual.
e For this reason, friend functions are problematic when used with inheritance and should be
avoided.
e Ifa destructor in the base class is virtual, it will have virtual destructors in all derived classes.
e Ifacopy constructor, destructor or assignment operator are necessary in a base class, this does
not mean that they are also necessary in a class derived from it. The default implementations
of these operations in derived classes automatically call the self-defined implementations of
the base class.
406 6: Dynamic and Static Members
oY
If objects of a class contain objects of another class, on initialization, several constructors are
called. In addition to the constructor of the parent object, the objects that are members also have
to be initialized.
The following applies:
e Before the statements in the body of a constructor are processed, constructors for the members
of the object are called.
This order makes sense because only the constructor of the whole object knows the meaning of
the sub-objects, and must therefore have a facility for evaluating or correcting the initial status of
the sub-objects whenever necessary.
Furthermore, it is specified that the order of the calls of the constructors for the members
corresponds to the order in which these members are declared inside the class structure. The
order is not defined by the initializer list. However, this order should not matter, as otherwise
we would end up with totally unreadable programs. It would mean that the behavior of a class,
or even a program, would change because the order of the declarations was modified. The order
is only defined to ensure that the destructors are always called in reverse order, which may be
important in implementing custom memory management, for example.
// dyna/person1.hpp
#ifndef PERSON_HPP
#define PERSON_HPP
namespace CPPBook {
class Person {
private:
std::string fname; // first name of the Person
std::string lname; // last name of the Person
public:
// constructor for last name and optional first name
Person(const std::stringk, const std::stringk = "");
// query of properties
const std::string& firstname() const { // return first name
return fname;
}
const std::string& lastname() const { // return last name
return lname;
}
// comparison
bool operator == (const Personk p) const {
return fname == p.fname && lname == p.lname;
t
bool operator != (const Person& p) const {
return fname != p.fname || lname != p.liname;
if
ie
} // oR END namespace CPPBook 263 3 3K9 2 a 2sa 2sae2k2 of2koe2Khe2 2k9k2sie2 ok2 2K OB2Kok
#endif //PERSON_HPP
408 6: Dynamic
e eee EDand Static
EAE Members
ECEUCES
.
Note that the Person class is not a class with dynamic members, even though it contains sub-
objects that have dynamic members. The various problems of dynamic members are successfully
solved and hidden by the sub-objects. It is ensured that the compiler-generated assignment op-
erator, Copy constructor and destructor work correctly there. The string class can now be used
like the type int. If a Person is assigned to another Person, the default implementation of the
assignment operator, which assigns member-wise, can be used. The same applies when creating
copies. Also, when an object of the Person class is destroyed, the destructors or the sub-object
are automatically called.
The constructor is defined in the dot-C file, and initializes the person using the parameters
passed:
// dyna/person1.cpp
/* constructor for last name and first name
* - default for first name: empty string
i
Person: :Person(const std::string& ln, const std:: string& fn)
: Iname(1n), fname(fn) // initialize first and last names with passed parameters
1
// nothing else to do
e Then, according to the initializer list, the last name and the
first name are initialized with
the arguments passed to the constructor. First the parameter fn
is used to initialize fname.
Because both are strings, the copy constructor of the string class
is used to initialize the fname
member with fn (see also Section 6.1.3 on page 360):
6.4 Classes Containing Classes 409
The fact that fname is initialized first is due to the fact that the fname member is declared in
the Person class before the Iname member. The order of initializations in the initializer list
does not matter.
e Then, the copy constructor of the string class is called for the lname member, as the 1n string
is passed for initialization:
buffer:
len:
size:
buffer:
len:
size:
e Finally, the statements of the constructor of the Person class are processed. As the members
are already completely initialized, there is nothing more to do (which is not untypical, by any
means).
Note that an initializer list is not part of the declaration, but part of the implementation of a
function.
Because no more statements have to be processed after the initialization, in practice, the
constructor is usually defined in the header file as an inline function:
namespace CPPBook {
class Person {
public:
Person(const std::string& In, const std::string& fn = LEN)
Iname(1n), fname(fn) {
410
a
e a
e 6: Dynamic and Static Members
ee ee
We can use this example to demonstrate why the use of initializer lists is important. Imagine that
we had implemented the constructor of Person so that no initializer lists were used:
/* constructor of last name and first name
* - bad: without initializer list
7];
Person: :Person(const std::string& ln, const std: :string& fn)
t
lname = ln;
fname = fn;
a}
If, in this case, a person is created using
CPPBook: :Person nico("Josuttis", "Nicolai");
the following happens:
e Again, memory is created for the object first:
buffer:
len:
size:
buffer:
len:
size:
¢ Then the default constructor of the string class is called for the fname member
(as no argu-
ment is submitted for the initialization). This initializes the first name with an empty
string
(see Section 6.1.2 on page 358):
buffer:
len:
size:
buffer:
len:
size:
6.4 Classes Containing Classes 411
e After this, the lname member is initialized with an empty string, using the default constructor
of the string class:
buffer:
len:
size:
buffer:
len:
size:
e Finally, the statements in the body of the constructor of the Person class are processed,
which assign the correct values to the fname and 1name members:
buffer:
len:
size:
In order to do so, the assignment operator of the string class is called, which frees the memory
for the recently initialized empty strings again, as well as allocating and initializing new
memory (see Section 6.1.5 on page 362).
This example highlights some of the disadvantages of not using an initializer list. Both members
are given a default value by the default constructor, which is incorrect, and has to be replaced im-
mediately with an assignment. In our example, this means that memory is created unnecessarily
and then freed immediately, because the initial memory is not large enough to initialize with the
passed names. This means there is a considerable run-time disadvantage (memory management
is time consuming). Such a disadvantage gets worse the more (complex) members you have in a
class.
Furthermore, such a kind of faulty initialization not only takes a lot of time, but may also
cause irreparable damage. For example, consider a class for opened files that opens an incorrect
default file.
It may also happen that no default constructor exists for a class. This is always the case if at
least one argument is needed for initialization. These kinds of object can then no longer be used
as sub-objects in another class.
412 6: Dynamic and Static Members
.
For example, the Person class has no default constructor, as, according to its specification, it
makes no sense to create a Person without at least initializing the last name. Without initializer
lists, the class could no longer be used in other classes (e.g. think of a Project class that has a
project leader member as a Person).
By using initializer lists, it is also possible to declare the Iname member as a constant, as it is
initialized by the constructor and not changed later:
// dyna/person2.hpp
#ifndef PERSON_HPP
#define PERSON_HPP
namespace CPPBook {
class Person {
private:
const std::string lname; // last name of the Person (new: constant)
std::string fname; // first name of the Person
public:
// constructor for last name and optional first name
Person(const std::string& ln, const std: :string&’ fn = "")
Iname(1n), fname(fn) {
L
// query of properties
const std::string& lastname() const { // return last name
return lname;
if
const std::string& firstname() const { // return last name
return fname;
he
#endif //PERSON_HPP
6.4 Classes Containing Classes 413
A word of caution: declaring members as constants means that they really cannot be modified as
long as the object exists. This means, in particular, that no assignment is possible with the default
assignment operator, which assigns new values to all members. In this respect, the declaration of
the last name for the Person class might not be useful. A typical example of constant members
are object IDs (see Section 6.5.1 on page 414).
In the same way, it is also possible that a class may have references as members. These must
also be initialized using an initializer list. However, you must make sure that the object for which
the reference stands does not exist for less time than the object that has the reference as a member.
In this respect, members should actually be used as references, if the lifetime of the referenced
object is controlled by the object that has the reference as a member, or if the referenced object
stays valid until the end of the program.
6.4.3 Summary
e Members of a class can have any type. For the implementation of the class, it does not matter
whether it is a type with dynamic members.
e By default, the default constructor is called for sub-objects of classes. If a default constructor
is not defined or cannot be called for the base class, no object of the derived class can be
created.
e Initialization lists make it possible for constructors to pass arguments to sub-objects, which
means that, instead of the default constructor, another corresponding constructor is called.
e Initialization lists are preferred over assignments.
e Classes can have constants and references as members. These have to be initialized in the
constructor, using an initializer list.
414 6: Dynamic and Static Members
The management of objects that represent people requires two properties of classes that are often
needed:
e An explicit ID for the objects.
e Information about the number of existing objects.
Both properties can only be implemented using variables that do not belong to a concrete object,
but to a class in general. These so-called class variables’ are global variables that belong to the
scope of the class.
In C++, class variables can be implemented by declaring static class members. Static class
members are declared in the class structure using the keyword static. The variables belong to
the scope of the class, and have to be accessed externally via the scope operator (which
is, of
course, only possible if they are declared as public members).
In contrast to non-static members, these are variables that are only created once for the whole
life cycle of the program, and not for each individual object of the class. However, each
object
of the class has access to them.
The following specification adds two static class members to the Person class introduced
in
the previous section: maxPID as a counter to assign explicit person IDs; and numPerso
ns as a
variable that counts the number of existing Persons:
// dyna/person3.hpp
#ifndef PERSON_HPP
#define PERSON_HPP
namespace CPPBook {
class Person {
’ The name class variable has its origins in the Smalltalk programming
language.
6.5 Static Members and Auxiliary Types 415
public:
// constructor from last name and optional first name
Person(const std::stringk, const std::string& = "");
// new: destructor
~Person();
// new: assignment
Person& operator = (const Person&);
// query of properties
const std::string& lastname() const { //return
last name
return lname;
}
const std::string& firstname() const { // return first name
return fname;
}
long id() const { // new: returnID
return pid;
416 6: Dynamic and Static Members
ine
#endif //PERSON_HPP
Both maxPID and numPersons are declared as static members, so that they exist only once
and are shared by all objects of the class:
class Person {
private:
static long maxPID; // highest ID of all Persons
static long numPersons; // current number of all Persons
13
Static members can be accessed not only in ‘ordinary’ member functions, but also in special
static member functions (also called class methods in object-oriented terminology):
namespace CPPBook {
class Person {
public:
// return current number of all Persons
static long number() {
return numPersons;
}
};
}
In principle, this can be considered as a global function that belongs to the scope
of the class
CPPBook: : Person. It has access to the private static members of the Person class
and, in con-
trast to non-static member functions, it has the advantage of being able to be called independen
tly,
without any concrete objects of this class.
The following application program demonstrates this:
6.5 Static Members and Auxiliary Types 417
// dyna/ptest3.cpp
#include <iostream>
#include "person.hpp"
int main()
‘!
std::cout << "number of people: " << CPPBook: :Person: :number ()
<< std::endl;
The number of Persons is determined here by calling th static number () function of the scope of
CPPBook: : Person. No concrete object of the class is used. The number can also be determined
in this way without a Person existing at all.
In the object-oriented sense, static members mean that classes themselves have attributes and
operations. A static member function might be regarded as a method of a class. The call of the
static member function is then a message to this class, to which the class method reacts.
If a concrete object of the Person class exists, the call via this object is also possible:
CPPBook: :Person nico("Josuttis","Nicolai") ;
Inside a class structure, static class members are only ever declared. However, like any static
variable, they also have to be defined (and initialized) once. The definition must happen outside
of the class structure in a dot-C file (which is typically the dot-C file of the class). An initialization
within the class structure is wrong.
Let us consider the implementation of the Person class:
// dyna/person3.
cpp
// include header file of the class
#include "person.hpp"
namespace CPPBook OK
oh 2 OK26 2 2 OK 6 okoRook 2BOKoi OBOB OROKOO OK OB 2K
// oh KK BEGIN
418 6: Dynamic and Static Members
namespace CPPBook {
/* new: destructor
=f
Person:
: ~Person()
{
--numPersons; // reduce number of existing Persons
/* new: assignment
we
Person& Person: :operator = (const Person& p)
f
if (this == &p) {
return *this;
6.5 Static Members and Auxiliary Types 419
return *this;
The variable of the corresponding scope is accessed via the scope operator:
namespace CPPBook {
long Person: :maxPID = 0;
long Person: :numPersons = 0;
Z,
As usual, each static class member must be initialized only once in the program.
The constructor initializes the members of the Person class via the initializer list:
Person: :Person(const std::stringk ln, const std::string& fn)
lname(1n), fname(fn), pid(++maxPID)
The lname member is initialized with the first parameter, 1n, and the fname member is initial-
ized with the second parameter, fn. For the initialization of the newly added member pid, the
incremented class member with the highest person ID, maxPID, is used. As the pid member is
constant, this has to be initialized using an initializer list; a later assignment would not be pos-
sible. In the body of the function, the counter for the number of existing Persons is increased
accordingly.
To keep the number of existing Persons correct, the counter must also be reduced when an
object of the Person class is destroyed. For this reason, a corresponding destructor is defined:
Person: :~Person()
{
--numPersons; _ // reduce number of existing Persons
The destructor is called for every object that exists and is subsequently destroyed. Thus the
counter has to get incremented for each object created. In particular, we must not forget to define
the copy constructor too, so that a new object ID is provided and the Person counter is increased
if an object of the class is created as a copy of an existing object:
420 6: Dynamic and Static Members
return *this;
ii
As the ID was declared as a constant, an assignment would be impossible without providing this
implementation. This is because the default assignment operator would also (try to) assign the
ID, which would cause an error.
#ifndef PERSON_HPP
#define PERSON_HPP
6.5 Static Members and Auxiliary Types 421
namespace CPPBook {
class Person {
/* static class members
“ig
private:
static long maxPID; // highest ID of all Persons
static long numPersons; // current number ofall Persons
public:
// current number of
all Persons
static long number() {
return numPersons;
public:
// new: special enumeration type for the salutation
enum Salutation { Mr, Mrs, Ms, empty };
public:
// constructor for last name and optional first name
Person(const std::string&, const std: :string& = IS)
// copy constructor
Person(const Personk) ;
// destructor
~Person() ;
// assignment
422 6: Dynamic and Static Members
// query of properties
const std::stringk lastname() const { // return
last name
return lname;
}
const std::string& firstname() const { // return
first name
return fname;
A;
long id(Q) const { //returnID
return pid;
M;
const Salutation& salutation() const { // new: return salutation
return salut;
UF
// compare
friend bool operator ==-(const Person& p1, const Personk p2) {
return p1.fname == pi.fname && p2.1lname == p2.1name;
i
friend bool operator != (const Person& p1, const Personk& p2) {
return !(p1l==p2);
z:
y%
#endif //PERSON_HPP
The Salutation enumeration type is simply defined and used in the class declaration. This has
the advantage that, for the symbols that are used, Salutation, Mr, Mrs, Ms and empty, there
is no danger of global name conflicts. However, the public declaration makes it possible to use
these symbols outside the class. This happens with the scope operator, as shown in the following
application example:
// dyna/ptest4.cpp
#include <iostream>
#include "person.hpp"
int main()
i
6.5 Static Members and Auxiliary Types 423
if(niconsalutabion == nosalubatdon) ma
stds-cout << "salutation of Nico was not set” << std::endl;
As the declaration of noSalutation shows, the qualification with the scope operator is neces-
sary when using the type as well as when using a value, as both the type and the value belong to
the scope of the Person class.
In this way, separate types can also be created in classes using typedef.
}3
the trick of
This facility was introduced fairly late in the standardization of C++. Until then,
see the following code:
using an enumeration type was used. You may therefore occasionally
class MyClass {
private:
enum { MAXNUM = 100 }; //OK
int elems [MAXNUM] ; //OK
3;
symbolic name for which a
The value for an enumeration type is not a constant object, but just a
certain value can be set with a declaration.
424 6: Dynamic
i eeand Static eee,
Members
rr
The members of a nested class are accessed by means of a nested qualification using the
scope
operator. For example, the fo00() function must be defined as follows:
void MyClass: :AuxiliaryClass: :foo()
‘i
}
Again, this is particularly useful with complex classes in order to avoid ‘polluting
’ the global
scope with auxiliary classes. This style avoids name conflicts and supports
data encapsulation
(the auxiliary classes can also be declared as private).
Typical examples of nested classes are error classes introduced with exception
handling (see
Section 4.7 on page 234),
It is even possible to declare classes locally, within the scope of a function.
This is rarely
used. However, it is clear that a structure in C++ is simply a class that has
public members by
default. This therefore means that local structures can be declared within
functions, which makes
more sense.
class MyClass {
jaiellenL
ane ¢
/* declare nested auxiliary class
*/
class AuxiliaryClass;
tr
class MyClass::AuxiliaryClass {
private:
Has oe
public:
void foo();
t3
Sometimes such a forward declaration is the only way to implement a nested class. This is, when
the type of the outer class is used to declare a member in the nested class. See Section 6.2.7 on
page 390 for an example.
6.5.5 Summary
are
e Classes can have static members. These are objects that only exist once per class, and
in the
shared by all objects of the class. They represent global objects that are only visible
scope of the class.
e Static class members must be defined (and initialized) once, outside of the class definition.
the class.
e Static member functions allow access to static members, without using an object of
scope of a class.
They represent global functions that are limited to the namespace and
be used for
e Static class constants can be defined using enumeration types. They can also
declaring other members.
as enumeration
e Classes form a separate scope in which it is possible to define types, such
types or auxiliary classes (nested classes).
the assignment operator
e For object IDs, all constructors (including the copy constructor) and
The member for the ID should be declared as a constant.
of a class have to be defined.
construc tor) and the destructor of a
e For object counters, all constructors (including the copy
class have to be defined.
ae sists |
m
aL “
sence ws q
aetage ie " ecw
hina
Toa “— J -
titrba
aini he
7 se) 5 —_ eti ‘Gems sn etmio fer, mm
This chapter introduces the concept of templates. Templates can be used to parametrize source
code for different types. A minimum function or a collection class can therefore be implemented
before the type of the parameter or element has been determined. However, this code is not
generated for arbitrary types; if the type of the parameter or element is defined, the usual type
checking for all operations apply.
We first introduce function templates, followed by class templates. We concludes with notes
on working with templates, including some special design techniques.
More information on templates can be found in Nicolai M. Josuttis and David Vandevoorde’s
book C++ Templates — The Complete Guide, published by Addison-Wesley, ISBN 0-201-73484-
2 (see [VandevoordeJosuttisTemplates]}). Inside this book you will find a similar introduction to
templates, supplemented by a comprehensive reference, a wide range of coding techniques, and
some advanced applications for templates.
428 7: Templates
7.1.1 Terminology
The terms used to describe templates are not clearly defined. For example, a function for which
a type is parametrized is sometimes called a function template and sometimes called a template
function. However, as the latter term is a bit confusing (it might be a template of a function as
well as a function out of a template), the term function template should be used. Similarly, a class
for which types are parametrized should be called a class template instead of a template class.
The process of creating a regular class, function or member function from a template by
substituting actual values for its arguments is called template instantiation. Unfortunately, the
word instantiate is also used in object-oriented terminology for the creation of objects of a class.
Therefore, in C++, the meaning of the word instantiate always depends on the exact context.
7.2 Function Templates 429
The first line defines T as a type parameter. The keyword typename is used, which establishes
that the following symbol is a type. This keyword was introduced in C++ relatively late. Before
this, the class keyword was used:
template <class T>
Semantically, there is no difference between these two words. Therefore, when using the key-
word class here, the type does not necessarily have to be a class. In both cases, you can use any
type (fundamental type, class, etc.), as long as it provides the operations that the template uses.
In this case, type T has to support the < operator because a and b use this for comparison.
The use of the symbol T for a template type is not required, but is quite common. In the fol-
lowing declaration, T can be used as a type in a parameter declaration and denotes the parameter
type passed during the call:
// tmpl/maz1.hpp
The statements within the function are no different from those of other functions. In this case,
the maximum of two values of type T is processed using the comparison operator and returned
as a result. As has already been mentioned in Chapter 4.4, creating copies when passing the
parameter and the return value is prevented through the use of constant references (const T&).
430 7: Templates
~
int main()
{
int a, b; //two variables of the datatype int
std::string s, t; //twovariables of the type std::string
At the moment, as the max () function is called for two objects of one type, the template becomes
real code. This means that the compiler uses the template definition and instantiates it by using
int or std::string in place of T. This can be considered as the creation of the real code for
int and std::string. A template is therefore not compiled as code that can deal with any
type, but is only used to produce code for different types in different application cases. If max ()
is called for seven different types, seven function are compiled.
The process with which the code to be compiled is generated from the template code is called
instantiation or more clearly (because this term is also used in object-oriented terminology for
the creation of objects of a class) template instantiation.
Of course, a template instantiation is only possible if all of the operations that are used in the
function template are also defined for the type of the parameter. Thus, in order to be able to call
the maximum function for objects of the std: : string class, the comparison operator < has to
be defined for strings.
Note that, in contrast to macros, templates are not a straight text replacement. The call
max(x++, z *= 2);
is not very useful, but it works. The maximum of the two expressions that were passed as
arguments is returned, and each expression is evaluated just once (i.e. x is only incremented
once).
' The call of max() for strings is explicitly qualified for the global namespace, because a std: :max()
is also defined in the standard library. Because strings belong to std, this function is also found without
qualification, which can cause ambiguity (see Koenig lookup on page 177).
7.2 Function Templates 431
rahe “GLS
long 1;
Explicit Qualification
what type a tem-
An explicit qualification can be used when calling templates to determine for
plate is to be used:
max<long>(i,1) // OK, calls max () with T as long
type as template param-
In this case, the function template max<>() is instantiated for the long
passed arguments can be used
eter T. Now, as with ordinary functions, it is checked whether the
on from int to long.
as Longs, which happens to be the case, as there is an implicit type conversi
432 7: Templates
Alternatively, the template can also be defined so that it allows parameters of different types:
template <typename T1, typename T2>
inline T1 max(const T1&, const T2&); //parameter can have different types
{
ietsmbhen, E < |e) ff |) 8 fie
shee, 51,8
long 1;
{
std::cout << "max<>() for T" << std::endl;
iaesnuben bY
<< |e & ley
B te
af
std::cout << "max<>() for Tx" << std::endl;
return *a < *b ? b: a;
int main()
{
int a=7; // two variables of datatype int
int b=11;
std::cout << max(a,b) << std::endl; //max()
for two ints
Function templates can have any internal variables of the template type. For example, a function
template that mixes up the values of two parameters can be implemented as follows (compare
with the implementation of the function swap () on page 183):
template <typename T>
void swap (T& a, T& b)
@!
T tmp(a);
a=b;
b = tmp;
i
The local variables can also be static. In this case, a static variable is created for every type for
which the function is called.
7.2.7 Summary
e Templates are stencils for code that is compiled after the actual type has been set.
e The generation of compilable code from template code is called (template) instantiation in
C++.
e Templates can have multiple template parameters.
e Function templates can be overloaded.
436 7: Templates
// tmpl/stack1.hpp
#include <vector>
#include <stdexcept>
// TRB RK BEGIN namespace CPPBook BEOES8 2H3 ORfg2SSk2483 2k 2 2k2kfe2 oyeokoieokoeoko 2kok2kokakoeokok
namespace CPPBook {
public:
Stack(); // constructor
void push(const T&); // store new top element
void pop(); // remove top element
T top() const; // return top element
+5
// constructor
template <typename T>
Stack<T>: :Stack()
nh
// nothing more to do
7.3 Class Templates 437
template<typename T>
void Stack<T>: :pop()
{
if (elems.empty()) {
throw std: :out_of_range("Stack<>::pop(): empty stack");
}
elems.pop_back() ; //remove
top element
}
} // ww EMT) namespace CPPBook 246 2G 3S2 RS 9 Ae2 2s2k2S2 2s 2S2 is2gHSIS2S is2SOS283SSO OB
As with function templates, the template code is always preceded by an statement in which the
T is manifested as the type parameter (of course, several type parameters can also be defined for
class templates):
template <typename T>
class Stack {
dog
Again, the class keyword can be used as an alternative to typename here:
template <class T>
class Stack {
+3
438 7: Templates
Inside the class, the type T can then be used like any other type for the declaration of class mem-
bers and member functions. In this example, the elements of the stack are managed internally
using a vector for elements of type T (the template is therefore programmed using another tem-
plate), the function push() uses a constant reference T as a parameter, and top() returns an
object of type T.
The type of the class is Stack<T>, in which T is a template parameter. Therefore, this must
be used whenever the type needs to be entered. Only for the entry of the class name
class Stack {
};
(and for the name of the constructors and destructor) is Stack used.
If parameters and return values need to be entered, this would look as follows (which shows
an example of the declaration of copy constructor and assignment operator) ?:
template <typename T>
class Stack {
When defining the member functions for a class template, it also has to be specified that they are
(part of) a template. As the example of push() shows, the entire type Stack<T> has to be used
for the qualification:
template <typename T>
void Stack<T>::push(const T& elem)
{
elems.push_back(elem) ; // store copy
}
The functions delegate the work to the corresponding functions of the vector that is
used inter-
nally to manage the elements. Doing this, the top element is always the element at
the end of the
vector.
Note that pop() removes the top element but doesn’t return it. This behavior
corresponds
with pop_back () for vectors. The reason for this behavior is exception safety (see
Section 4.7.10
on page 252). It is impossible to implement a version of pop() that returns the
removed element
and gives the optimal exception safety. Consider a version that returns the
removed top element:
* In the standard, however, there are some rules that determine when an
entry of Stack instead Stack<T> is
sufficient inside a class declaration. However, as a rule of thumb, you should
always use Stack<T> where
the type of the class is required
7.3, Class Templates 439
template<typename T>
TtStack<T>sspop()
::
if (elems.empty()) {
throw std::out_of_range("Stack<>::pop(): empty stack") ;
a;
T elem = elems.back(); // keep copy of top element
elems.pop_back() ; // remove top element
return elem; // return copy of old top element
}
The problem is that the exception may be thrown by the copy constructor of the elements, when
the element is returned. However, there is no chance to leave the stack in the original state
because the number of elements is reduced already. Thus, you have to decide whether to give
only the basic exception safety guarantee or not to return the element °,
Note also that the member functions pop_back() and back() (the latter returns the last
element) have an undefined behavior if the vector is empty (see pages 521 and 523). This is
tested for both functions, and an exception of type std: : out_of __range is thrown if necessary
(see Section 4.7.9 on page 252):
template<typename T>
T Stack<T>::top() const
1
if (elems.empty()) {
throw std: :out_of_range("Stack<>::top(): empty stack");
}
return elems.back() ; // return top element as a copy
EF
Of course, the functions can also be implemented within the class declaration:
template <typename T>
class Stack {
¥;
is discussed as Item 10 in
> This topic was first discussed by Tom Cargill in [CargillExceptionSafety] and
[SutterExceptional]).
440 7: Templates
int main()
i!
Cry 1
CPPBook: :Stack<int> intStack; // stack for integers
CPPBook: :Stack<std::string> stringStack; // stack for strings
provide operations for all member functions, as long as only those functions that are supported
are called. An example of this would be a class in which some member functions are sorted with
the operator <. As long as these member functions are not called, the type passed as a template
parameter does not need to have the < operator defined.
In this case, code is generated for two classes, int and std: : string. Ifa class template has
static members, two different static members are created.
For every type used, a class template forms a separate type that can be used anywhere:
void foo(const CPPBook::Stack<int>& s) // parameters is int stack
3!
CPPBook: :Stack<int> istack[10]; //istack isan array of ten int stacks
}
In practice, a typedef statement is frequently used in order to make the use of class templates
more readable (and more flexible):
typedef CPPBook: :Stack<int> IntStack;
z
The template arguments can be any type. For example, float pointers, or even a stack of stacks
for ints:
CPPBook: :Stack<float*> floatPtrStack; // stack of float pointers
CPPBook: :Stack<CPPBook: :Stack<int> > intStackStack; // stack of int stacks
The important thing is that all operations called for these types are provided.
Note that two consecutive closing template brackets need to be separated from each other by
a space. Otherwise, it would denote the >> operator, which would produce a syntax error at this
point:
// ERROR: >> not allowed
CPPBook: :Stack<CPPBook: :Stack<int>> intStackStack;
For an explicit specialization, template<> has to be written in front of the class declaration,
and the template type specified has to be entered after the class name:
template<>
class Stack<std::string> {
33
Every member function must begin with template<>, and T must be replaced by the specified
template type:
template<>
void Stack<std::string>::push(const std::string& elem)
{
elems.push_back(elem) ; // store copy
}
The following is a complete example of a specialization of the Stack<> class template for the
std::string type:
// tmpl/stack2.hpp
#include <deque>
#include <string>
#include <stdexcept>
// 38K BEGIN namespace CPPBook 3S9K2sfs2 28fe9 28ae2824kfeaftoftfe2K2kfe2 2kakai9KK of2K2 oeakeoeak
namespace CPPBook {
template<>
class Stack<std::string> {
private:
std: :deque<std::string> elems; // elements
public:
Stack() { //constructor
}
void push(const std::string&) ; // store new top element
void pop(); // remove top element
std::string top() const; //return top element
1%
} // wu ENT) namespace CPPBook of363 ok2 Kki 2 fe 2s2 2K2h2koeoe2k2s feik2soieis2k2k2 2s28oi OK2Kok
In this case, for strings, the internal vector for the element is replaced by a deque. This has no
decisive drawback, but it shows that the implementation of a class template for a special type can
look completely different.
The numeric limits defined in the standard library are another example of the use of template
specializations (see Section 9.1.4 on page 531).
Partial Specializations
Templates can only be partially specialized (partial specialization). For the class template
template <typename T1, typename T2>
class MyClass {
I3
the following partial specialization can be given:
// partial specialization: both types are equal
template <typename T>
class MyClass<T,T> {
+5
}3
Fs
These templates are activated as follows:
MyClass<int,float> mif; //MyClass<T1,T2>
MyClass<float,float> mff; //MyClass<T,T>
MyClass<float,int> mfi; //MyClass<T, int>
MyClass<int*,float*> mp; //MyClass<T1* ,T2*>
If several partial specializations are all equally suitable, the call is ambiguous:
MyClass<int,int> m; // ERROR: matches MyClass<T ,T>
// and MyClass<T, int>
MyClass<int*,int*> m; // ERROR: matches MyClass<T,T>
// ,T2*>
and MyClass<T1*
The last ambiguity would be resolved if a partial specialization for pointers of the same type
had
been defined:
template <typename T>
class MyClass<T*,T*> {
oe
// tmpl/stack3.hpp
#include <vector>
#include <stdexcept>
namespace CPPBook {
public:
Stack(); // constructor
void push(const T&); // store new top element
void pop(); // remove top element
T top() const; //return top element
ki
// constructor
template <typename T, typename CONT>
Stack<T,CONT>: :Stack()
4
// nothing more to do
}
retu rn elems.back() ; // return top element
This stack can be used like the previous stacks. In addition, you could specify a different con-
tainer for the elements:
// tmpl/stest3.cpp
#include <iostream>
#include <deque>
#include <cstdlib>
#include "stack3.hpp"
int main()
{
try {
// stack for integers
CPPBook: :Stack<int> intStack;
// stack for floating-point values
CPPBook: :Stack<double, std: : deque<double> > dblStack;
Using
CPPBook: :Stack<double,std: :deque<double> >
a stack for floating-point values is declared that uses a deque internally as a container.
7.3.5 Summary
e Classes can also be implemented as templates for types that have not yet been defined.
e Using class templates, container classes that manage other objects can be parametrized for
the type of the managed elements.
e For class templates, only the functions that are actually used are instantiated.
e Implementations of class templates can be specialized for certain types. Partial specialization
is also possible.
e It is possible to define default values for template parameters of class templates.
448 7: Templates
#include <stdexcept>
namespace CPPBook {
public:
Stack(); // constructor
void push(const T&); // store new top element
void pop(); // remove top element
T top() const; // return top element
An
// constructor
template <typename T, int MAXSIZE>
Stack<T,MAXSIZE>: :Stack()
: numElems (0) // no elements
<
// nothing more to do
sf
if (numElems == MAXSIZE) {
throw std::out_of_range("Stack<>::push(): stack is full");
}
elems[numElems] = elem; // enter element
++numElems; // increase number of elements
}
The second parameter of the stack template, MAXSIZE, is used to determine the size of the stack.
This is not only used to declare the array, but also, in push (), to determine whether the stack is
already full.
When using this stack template, both the type of the element being managed and the size of
the stack have to be specified:
// tmpl/stest4.cpp
#include <iostream>
#include <string>
#include <cstdlib>
#include "stack4.hpp"
450
fe
e ee e 7: Templates
pales
int main()
{
try {
CPPBook: :Stack<int , 20> int20Stack; // stack for 20 ints
CPPBook: :Stack<int, 40> int40Stack; // stack for 40 ints
CPPBook: :Stack<std::string,40> stringStack; // stack for 40 strings
It must be taken into account that, in this case, int20Stack and int40Sta
ck have different
types, and cannot be assigned to each other or used mutually.
Default values for the template parameters can also be defined:
template <typename T = int, int MAXSIZE = 100>
class Stack {
Te
I consider this not to be useful in this case. Default values should intuitivel
y be correct. Neither
int as the element type nor a maximum size of 100 are intuitive. In this respect,
it is better if the
specification of both is left to the application programmer.
7.4 Non-Type Template Parameters 451
t3
The use of string literals also leads to problems:
template <typename T, const char* name>
class MyClass {
be
ie
The pointer s may be a global pointer, but what it points to is still not global.
The solution to the problem is as follows:
template <typename T, const char* name>
class MyClass {
or
452 7: Templates
void foo() {
MyClass<int,s> x; //OK
}
The global array s is initialized with the character string ‘hello’, whereby s is the global char-
acter string ‘hello’.
7.4.3 Summary
e Templates can have parameters that are values rather than types.
e These values cannot be floating-point values or objects, and cannot be local.
e The use of string literals as template arguments is only possible with restrictions.
7.5 Additional Aspects of Templates 453
t3
In this case, typename is used in order to determine that SubType is a type that is defined in the
class T. Therefore, ptr is declared as a pointer to this SubType.
Without typename, it would be assumed that SubType is a static value (variable or object)
of the class T. Thus
T::SubType * ptr
#include <iostream>
In this function template, an STL container of the type T is expected as a parameter. The output
of the elements of this container is achieved using a local iterator. This iterator has an auxiliary
type that is defined by the container. Its declaration must be carried out using the typename
keyword:
typename T::const_iterator pos; //const_iterator is an auxiliary type ofT
public:
void push(const T&); —_// store new top element
void pop(); // remove top element
TatopOy const: // return top element
bool empty() const { // return whether the stack is empty
return elems.empty();
// tmpl/stackS5assign.hpp
template <typename T>
template <typename T2>
Stack<T>& Stack<T>: :operator= (const Stack<T2>& op2)
{
if ((void*)this == (void*)&op2) { // assignment to itself?
return *this;
¥
First we look at the syntax for the definition of templates that are defined in class templates. A
template with parameter T2 is defined in the template with parameter T:
template <typename T>
template <typename T2>
With this implementation, one would think it sufficient simply to access all elements of the
submitted stack op2 directly, and to copy them into the separate stack. However, note again
that templates instantiated for different types are themselves different types. For this reason,
Stack<T2> is a different type than that for which this function was implemented. Therefore,
op2 can only be accessed using the public interface. The only chance of accessing the elements
via the public interface is using the member function top(). However, each element has to
become a top element, then. Thus, a copy of op2 must first be made, so that the elements are
taken from that copy by calling pop(). Because pop() returns the last element pushed onto the
stack, we have to use a container that supports the insertion of elements at the other end of the
collection. For this reason, we use a deque, which provides push_front () to put an element on
the other side of the collection.
456 7: Templates
*
Note that this function does not turn off type checking. Stacks still cannot be assigned to each
other if their element types cannot be assigned to each other. If, for example, an attempt is made
to assign a stack of strings to a stack of ints, an error message is produced for the line
elems.push_front(tmp.top()); // insert copy at front
because tmp.top() returns a string, which cannot be used as an int.
Note also that a template version of an assignment operator does not cover the default as-
signment operator. The default assignment operator is still generated, and is called with an
assignment between two stacks with the same element type.
However, one can change the implementation so that a vector can also be used as a container
for the elements. To do this, the declaration is changed as follows:
template <typename T, typename CONT = std::deque<T> >
class Stack {
private:
CONT elems; // elements
public:
Stack() ; // constructor
void push(const T&); —// store new top element
void pop(); // remove top element
T top() const; // return top element
bool empty() const { // return whether the stack is empty
return elems.empty();
vStack.push(42) ;
vStack.push(7);
std::cout << vStack.top() << std::endl;
vStack.pop();
As long as an attempt is not made to assign a stack with elements of
another type, this program
is correct.
7.5 Additional Aspects of Templates 457
Dynamic Polymorphism
If inheritance is used for the implementation of polymorphism, then an (abstract) base class
defines the common interface (the common term) that is used by a number of concrete types (see
Section 5.3 on page 300).
If one has, for example, a group of geometric classes, an abstract class GeoObj (as introduced
in Section 5.3.3 on page 302) can be given, from which all possible concrete classes are derived
(see Figure 7.1).
position()
Point Circle
In order to program with the common term, references or pointers to the objects of the base
class are used. This may look as follows:
458 7: Templates
// tmpl/dynapoly.
cpp
obj.draw();
int main()
{
Line 1;
Circle mc hmGlemico
The functions are compiled for the GeoObj type. The functions that are called depends on what
arguments are passed at run time. For example, in mydraw(), it is decided during run time
what draw() must be called for the geometric object. If a circle is passed, Circle: :draw() is
called; if a line is passed, Line: :draw() is called. Accordingly, in distance(), it is decided
during run time what member function position() must be called for a geometric object. A
non-homogeneous collection of geometric objects can also be declared using a pointer to the type
GeoObj (alternatively, the use of asmart pointer is recommended, see Section 9.2.1 on page 536).
Static Polymorphism
Instead of inheritance, templates can also be used for polymorphism. In this case, there is no
base class that the common interface explicitly defines. Instead, the necessary properties are
implicitly defined as operations of a template parameter.
The previous sample program can then be rewritten as follows:
// tmpl/staticpoly.cpp
int main()
{
Jinsays) AL.¢
Cirelere:
Circlemclmmco)
For the first two functions, draw() and distance(), the type GeoObj becomes a template
parameter. By using two different template parameters, the transfer of two different kinds of
geometric objects is made possible in distance():
distance(l,c); // distance<Line,Circle>(Geo0bj1&, Geo0bj 2&)
A non-homogeneous collection is no longer possible with templates. However, the types
of the
elements in the collection no longer have to be declared as pointers:
Std::vector<Line> coll; // OK: homogenous collection
This can have significant advantages.
programming languages offer different combinations (for example, Smalltalk supports unbound
dynamic polymorphism).
The advantages of dynamic polymorphism are as follows:
e It makes non-homogeneous collections possible.
e It requires less code (the functions are only compiled once for the type Geo0b}).
e Polymorphic operations can be provided as object code (template code must be provided as
source code).
e The error handling of the compiler is better (see Section 7.6.2 on page 468).
The advantages of static polymorphism are as follows:
e It leads to an improved run-time behavior (better optimizations are possible as there are no
virtual functions). Experience shows that a factor of between 2 and 10 is possible.
e It is unbound (classes are not dependent on any other code). Because of this, the use of
fundamental types is possible.
e It avoids the use of pointers.
e Concrete types do not need to provide the complete interface (templates only need the oper-
ations that are called).
With regards to type safety, both forms have their advantages and disadvantages. With dynamic
polymorphism, the common term has to be given explicitly. With static polymorphism, objects
of a class can be used as geometric objects, only because the class offers the corresponding
operations. On the other hand, the type safety of homogeneous sets is not guaranteed with
dynamic polymorphism. In order to ensure there are only lines in a collection of geometric
objects, for example, appropriate queries have to be programmed, which take place during run
time.
Taking these considerations into account, in practice, due to possibly improved run-time be-
havior, one should think about using static polymorphism, as long as the parameters are known
at compile time and no non-homogeneous collections are required.
7.5.4 Summary
e If, during the use of a template parameter, an auxiliary type that has been defined for the
template is accessed, this has to be qualified using the typename keyword.
e Inner classes and member functions can also be used as templates. In doing so, implicit type
conversions can be made possible for operations of class templates. This does not disable
type checking.
e Template versions of the assignment operators do not hide the default assignment operator.
e Polymorphism can be implemented using templates. This has its advantages and disadvan-
tages.
462 7: Templates
Explicit Instantiation
One way of avoiding multiple compilations of the same template code is offered by
the technique
of explicit instantiation.
In this case, the templates are actually only declared in header files:
// tmpl/expl1.hpp
#ifndef EXPL_HPP
#define EXPL_HPP
// oh 8 2Kok BEGIN namespace CPPBook 38FIR2 28 OIE ie28fe2sfe2sfe2 oe2Koe2 2sieOKfeSigoeoeafk a 2 okokok
namespace CPPBook {
#endif //EXPL_HPP
The definition of templates is in a separate header file, which binds the header file with the
declarations:
// tmpl/expldef1.hpp
#ifndef EXPLDEF_HPP
#define EXPLDEF_HPP
#include "expl.hpp"
#include <stdexcept>
namespace CPPBook {
// constructor
template <typename T>
Stack<T>: :Stack()
{
// nothing more to do
template<typename T>
void Stack<T>: :pop()
{
if (elems.empty()) {
throw std: :out_of_range("Stack<>::pop(): empty stack");
t
elems.pop_back() ; //removetop element
J
#endif // EXPLDEF_HPP
// tmpl/expltest1.cpp
#include <iostream>
#include <string>
#include <cstdlib>
#include "expl.hpp"
int main()
sf
try {
CPPBook: :Stack<int> intStack; // stack for integers
CPPBook: :Stack<std: :string> stringStack; // stack for strings
#include <string>
#include "expldef.hpp"
You can identify such a explicit instantiation by the fact that the opening brackets do not directly
follow the template keyword. As can be seen, one can explicitly instantiate individual functions
and whole classes. In the case of classes, all member functions are instantiated.
With classes, one can also explicitly instantiate individual functions. Our example application
program could therefore also be called with the following explicit instantiations:
// tmpl/expldef2.
cpp
#include <string>
#include "expldef.hpp"
Figure 7.2 demonstrates the organization of the source code for the max() template.
Using the procedure of explicit instantiation, significant time can occasionally be saved. It
is therefore worthwhile dividing template code between two separate header files (one for the
declaration and one for the definition). If one does not want to explicitly instantiate, one simply
has to include the header file with the definitions instead of the header file with the declarations.
Thus this method creates considerably more flexibility without having to put up with significant
disadvantages.
If one wants to retain the advantage of inline functions, these have to implemented in the
header file with the declarations in the same way as with other classes.
Another option for dealing with templates is defined as the template compilation model in the
standard. This establishes that a template can be qualified via the export keyword. By doing so,
it is automatically exported into a template database or into a template repository.
7.6 Templates in Practice
SS
a a ee 467
#endif
#include "max.hpp"
#include "max.hpp"
max (x,y) ;
| #include "maxdef.hpp"
If this kind of template is implemented, the repository is used to clarify whether the template
was already compiled for the types, or, if this is not the case, whether it is automatically compiled
and bound to the running program.
This method is very much implementation specific. Until now, there were almost no com-
pilers that supported this language feature. As a result, in practice, this cannot be used at the
moment.
468 7: Templates
As can be seen, even finding the line that causes an error can be a problem ?.
Now, you only have to understand the problem. With a lot of practice and background knowl-
edge, one may find that something with basic_string it searched for, but only something with
int is available. If you cannot find this out, there is at least the possibility of seeing the place in
the code that has triggered the error and hopefully coming to the conclusion that int in the line
must simply be replaced by std: : string.
The error message in the example also highlights another problem. Templates can themselves
lead to warnings or errors. This is due to the fact that, because of the many replacements, very
long internal symbol names are produced. Symbols with up to 10000 characters are possible.
Not all compilers and linkers can cope with such long symbols. Some of them simply output a
warning, which can safely be ignored.
7.6.3 Summary
* Unfortunately, there are C++ compilers that do not even provide information about which line led to the
error. You should complain about this! However, there are also exemplary compilers that only output an
error message for the affected line in the code.
-
a -
- a ae ie
= <r Pw A
= : aS <i
4 7 7 -
7 is -
u
an
a a =i.@ “ rs
has o
tae =
Gari ' - r —— a
7 oG as hye ¥ s
Bs 4 ? a s 7
7 ‘
a : 7 im
eS
- : Z » mis " - ‘ we : al 4 be ‘ a /* i Fic an .
aa elec
, 7 wheal ) : : ’
ie ee ee a
ppt petri tie Desh a c
ate - a
a) cn e\. do
em
a Sr tafe er r
i seat NEP tein
ROM OS | wilt.
st a oak Se
yon 1 oy sien ee Wiest Volek ateWire y
- ia : aa co al
j ' %.<4i\
lt Oued ; remit see
7
ear estat’ |
Se ve Om, 2 Se gey, pe ryheommerae
The Standard I/O Library in Detail
Now that the standard techniques for inputting and outputting with stream classes have been
introduced (see Sections 3.1.4 and 4.5), this chapter will look at the details of the standard I/O
library. From this, one should get an impression of the full extent of the possibilities that this
library offers.
Formatted access to files and strings is also part of this chapter.
However, note that this book is unable to deal with all the features of the I/O library. I will
concentrate instead on the details that are important for daily I/O use. For other aspects, I suggest
that you refer to texts devoted to the standard library or the I/O library; for example, my book The
C++ Standard Library (see [JosuttisStdLib]) or the book Standard C+ + IOStreams and Locales
from Langer and Kreft (see [LangerKreftIO)).
472 8: The Standard I/O Library in Detail
The class hierarchy for the I/O classes is shown in Figure 8.1. For all classes that are provided as
templates, the top line of the box shows the template type, while the bottom line shows the type
for the instantiations for types char and wchar_t.
streambuf / wstreambuf
basic _ios<>
ios / wios
basic iostream<>
iostream / wiostream
characters. The other stream classes look after the formatting of the data (for example, the
conversion of the number 42 into the character sequence ‘4’ and ‘2’).
e The class templates basic _istream<> and basic ostreamc> are derived virtually
from basic_ios<> and define objects that can be used for reading or writing. The classes
that are most often used in the Anglo-American language, istream and ostream, are the
instantiations for type char.
e The class template basic iostream<> is derived from basic_istream<>, as well as
from basic_ostream<>, and defines objects that can be used for writing as well as for
reading.
Additional classes are defined for access to files and strings, which are looked at in Sections 8.2
and 8.3.
As has already been explained in Section 4.5.1 on page 196, a number of instances of these
classes are predefined. They are listed in Table 8.1. In addition to the objects for streams that
handle ‘ordinary characters’ of type char (cin, cout, etc.), the corresponding objects for the
standard channels of wide-character streams are listed.
Meaning
cin istream standard input channel (typically the keyboard)
cout ostream | standard output channel (typically the screen), buffered
cerr ostream | standard error output channel (typically the screen), unbuffered
clog ostream | standard history output channel (typically the screen), buffered
wcein wistream | standard input channel (typically the keyboard)
weout wostream | standard output channel (typically the screen), buffered
wceerr wostream | standard error output channel (typically the screen), unbuffered
wclog wostream | standard history output channel (typically the screen), buffered
The I/O stream library was designed so that it strictly separates responsibilities. The classes de-
rived from basic_ios ‘only’ take care of formatting the data For the actual reading and writing
of individual characters, the stream buffer classes are used, which are derived from basic_ios.
Stream buffer classes therefore play an important role if one defines I/O for new devices,
redirects streams to other devices or wants to use the character strings in different ways (for
example, if one wants to automatically convert all lowercase letters to uppercase letters). Stream
buffer classes also take care of the synchronization of simultaneous accesses of several streams
to the same device.
' The formatting is not carried out by the classes themselves, but they delegate special classes, which take
into account issues related to internationalization.
474 8: The Standard I/O Library in Detail
Y
In order to enable I/O for special devices, one should therefore not implement classes derived
from basic_ios. Instead, a special stream buffer class has to be implemented and used by the
stream objects derived from basic_ios.
Header Files
The definitions of the stream classes are divided into different header files:
e <iosfwd>
This contains the forward declarations of the stream classes.
e <streambuf>
This contains the definition of the base class for the stream buffer, basic_streambuf<>.
e <istream>
This contains the definitions of the classes that just support input (basic_istream<>), and
for the classes that support both input and output (basic_iostream<>).
e <ostream>
This contains the definitions of the classes that just support output (basic_ostream<>).
e <iostream>
This contains the definitions of the global stream objects, such as cin and cout.
This division may appear a little strange. In fact, it is sufficient, when using streams for I/O,
to only include the header file <istream>. Only if the global stream objects are really needed,
should <iostream> be included. This design has historical reasons. With the use of the global
stream objects in a module, special code is necessary that makes sure that these objects are
initialized. This code is included by means of <iostream>. Because execution of this code
adds a certain amount of run-time overhead, one should refrain from including <iostream> if
possible.
Provided that only the type of the stream class is required in declarations, only <iosfwd>
should be included. If the operations for reading and writing are required, <istream> and
<ostream> should be included. Only if the global stream objects are used, should <iostream>
be included.
For special stream features, such as parametrized manipulators, file access or string streams,
there are other header files (<iomanip>, <fstream>, <sstream> and <strstream>). These
are looked at later, with an introduction to the corresponding techniques.
Status Bits
The different bit constants are listed in Table 8.2. They are defined as flags (with type iostate)
in the class std: : ios_base, and are managed in this class by an internal member.
Talking about ios: : goodbit as a flag it is a bit confusing, because it typically has the value 0
and actually only shows whether another flag is set.
failbit and badbit are defined as follows:
e failbit is set if an operation cannot be executed correctly, but, in principle, the stream is
OK. Typical of this are format errors when reading. If, for example, an integer is to be read
but the next character is a letter, this flag is set.
e badbit is set if something is fundamentally wrong with the stream or characters have been
lost. This flag is set, for example, when positioning before the start of a file.
Note that eofbit is normally set together with failbit, because the condition EOF (end of
file) is only seen if the attempt to read further failed.
In addition, on some systems, the flag ios: :hardfail is included. However, this is not
standardized.
Because the flags are defined in the class ios_base, they have to be qualified accordingly:
std::ios_base::eofbit
This is possible because ios is an instantiation of the class basic_ios<>, which ts derived from
ios_base. Because, formerly, only the class ios was provided, this kind of qualification is
backwards compatible and is found quite often.
Various member functions (briefly introduced in Section 4.5.3 on page 203) belong to the flags
(see Table 8.3).
The first four member functions return a Boolean value that indicates whether particular flags
are set. Note that fail() yields whether failbit or badbit is set. This is due to historical
reasons and has the advantage that a call for the test is sufficient in the case of an error.
476 8: The Standard I/O Library in Detail
If clear() is called without an argument, all error flags (including ios::eofbit) are
cleared. If an argument is passed, the flags that are passed are set. On page 203, there is a
small working example that shows this.
Note that clear () has to be called for every failed stream operation. This is different to
the I/O interface of C. There, for example, after a failed attempt to read into an int due to a
format failure, one can immediately read the remainder of the line. In the stream classes of C++,
failbit is set, which ensures that all following attempts to read fail, until it is explicitly cleared
again.
Finally, in order to use streams as Boolean expressions in control constructs, the conversion
functions that were introduced in Section 4.5.2 on page 201 are defined (see Table 8.4).
If, at the moment of this call, one of these bits is already set, the corresponding exceptions are
triggered immediately.
Through the passing of 0 or goodbit, the automatic production of stream exceptions can be
switched off:
// do not automatically trigger exceptions
strm.exceptions(0) ;
The thrown exceptions have type std::ios_base::failure. For this class, it is only de-
fined that an implementation-specific string can be queried using what () (see Section 3.6.3 on
page 95).
Note that we are talking about ‘exceptions’ and not ‘errors’. For I/O in particular, there is
an important distinction between these terms. Faulty input is common and should be dealt with
locally, using the normal data flow. Usually, exceptions should only be triggered for the badbit
or if one reads data from a predefined configuration file in which there should not be any read
errors.
std::cout << "string: " << cstring << " is at the address:
<< static_cast<const void*>(cstring) << std::endl;
As well as the standard operators for streams (<< and >>), other member functions for reading or
writing can also be used. The semantics of these functions are discussed for instantiations of the
stream Classes for chars (i.e. using types istream and ostream). Special aspects, due to the
fact that they are functions for basic_istream<> and basic_ostream<>, are not taken into
account.
In the class istream, in addition to the >> operator, numerous other member functions are
defined that are used for reading characters:
e int get ()
returns the character or EOF that is next read. It corresponds to getchar() or getc() inC.
Note that the return value is assigned an int. Otherwise, the character with the value —1 or
with the value 255 cannot be distinguished from EOF, because EOF is typically defined as the
value -1.
e int gcount ()
returns how many characters were read during the preceding read command.
8.1 The Standard Stream Classes 479
e istream& unget ()
puts the character that was read last back into the input stream, so that it is available to be
read again next time.
e istream& putback (char c)
puts the character that was read last back into the input stream, so that is available to be read
again next time. c actually has to be the character that was read last; otherwise badbit is set.
With all these functions, whitespaces are not skipped.
Because an upper limit has to be entered, these functions are safer for reading strings than
the >> operator. However, it is also possible to define a maximum number of characters before
reading with >> (see Section 8.1.6 on page 487), although this can easily be forgotten.
The implementation of the functions for reading a fraction in Section 4.5.4 on page 208, and
of a string in Section 6.1.7 on page 366, contains working examples for different read functions.
In the ostream class, in addition to the << operator, some member functions are defined that can
be used for the output of characters:
e ostream& put (char c)
outputs the submitted parameter c as the next character and returns the stream. Its status then
gives information about whether the write was successful.
e ostream& flush ()
empties the output buffer by writing all characters that are contained in it.
Input and output streams can be connected together using a special member function:
e ostream* tie (ostream* os)
connects a stream with the submitted output stream os.
This has the consequence that if an input operation is called for the stream, the output buffer of
the output stream is flushed.
480 8: The Standard I/O Library in Detail
This ensures that a prompt for inputting is visible before an input value is read:
std::cout << "Please input x: ";
Sitdiic
ine > ax:
Examples
The classic filter framework, which simply outputs all the characters that have been read, looks
as follows in C++:
// t0/charcat1.cpp
#include <iostream>
int main()
{
chaigecr
Note that, in contrast to the C version, characters that have been read are
not tested for EOF and
therefore c does not have to be declared as an int. This is possible because we
read into the
passed argument c, so that the return value can be used to test the status of the stream.
As a small improvement, a test can be carried out to see whether the program was
actually
ended via EOF, and whether the output stream is in a fault-free status:
8.1 The Standard Stream Classes 481
// to/charcat2. cpp
#include <iostream>
#include <cstdlib>
int main()
{
char c;
Ita estdeescinneo
tC )))md
std::cerr << "read error" << std::endl;
return EXIT_FAILURE;
}
it (! fetd2coutecod Ona.
Sid 11Cenmn<qeu Wicd bemeriroOm uc sitbds sends:
return EXIT_FAILURE;
}
}
8.1.5 Manipulators
As was mentioned in Section 4.5.2 on page 199, different manipulators are defined for streams.
These are global objects, which, if they are called with the standard input or output operator,
manipulate the stream in some way. Table 8.6 lists all standard manipulators from <istream>
and <ostream>. Additional manipulators are provided for formatting. These are introduced in
the following sections.
There is actually a simple trick behind manipulators, which demonstrates how powerful the act
of overloading functions can be. Manipulators are nothing more than functions that are called
when they are passed to an input or output operator as a second argument.
For the class ostream, the output operator is, in principle, overloaded as follows (here, this is
pseudo-code for a definition of the class ostream; the actual code ofthe class basic_ostream<>
is parametrized according to the template parameter):
class ostream: ... {
}
Through the statement
std::cout << std::endl
the << operator is called with the parameter std: : endl:
std: :cout.operator<<(std:
:end1l)
Because std: : end] is a function, this call is converted so that the function passed as
an argument
is called with the stream as a parameter:
std::endl(std::cout);
This call finally outputs a line separator and empties the output buffer.
8.1 The Standard Stream Classes 483
User-Defined Manipulators
It is possible to define manipulators at any time. To do so, as with endl (), only one appropriate
function needs to be written.
With the following function, for example, a manipulator for skipping to the end of a line is
defined:
// to/ignore.hpp
#include <iostream>
#include <limits>
inline
std::istreamk ignoreLine(std::istream& strm)
ol
charac:
The function ignore() skips all characters, up to the line end (see Section 8.1.4 on page 479).
The use of the manipulator is fairly simple:
// ignore remainder of the line
std::cin >> ignoreLine;
By means of multiple calls, multiple lines can be ignored:
// ignore two lines
std::cin >> ignoreLine >> ignoreLine;
For the definition of I/O formats, different members are defined in the ios_base class that define,
for example, a minimum field width or the number of positions of floating-point values. In one
of these members, a number of format flags are managed as bit constants. Using these format
flags, for example, the output of a plus sign can be induced.
Some of these format flags belong together, such as, for example, the flags that activate the
octal, decimal or hexadecimal output. In order to simplify the management of these kinds of
flags, masks are defined, which manage certain groups of flags.
484 8: The Standard I/O Library in Detail
For general access to the format flags, the functions that are listed in Table 8.7 are defined.
The functions setf() and unsetf() set and delete flags, respectively. The flags must be
combined with the | operator. setf () can be passed a mask that results in all flags being deleted
in the appropriate group. For example:
// set flags showpos and uppercase
std: :cout.setf(std::ios::showpos | std::ios::uppercase);
// mark current
format flags
ios::fmtflags oldFlags = cout.flags();
One can also change the flags with the manipulators listed in Table 8.8.
Manipulator | Meaning
setiosflags (flags) sets flags as format flags
(calls setf (flags) for the stream)
resetiosflags (mask) | deletes all flags of the group identified by mask
(calls setf (0, mask) for the stream)
For these manipulators, as with all manipulators with parameters, the header file <iomanip>
has to be included. For example:
#include <iostream>
#include <iomanip>
For the definition of field width and filling symbol, the member functions width() and fi11()
are defined (see Table 8.9).
Table 8.9. Member functions for field width and filling symbol
Using the member function width(), a minimum field width can be defined for an output.
However, note that this setting only refers to the output that follows directly, as long as it is
not the output of a char. Without parameters, the current minimal field width is returned; with
parameters, a new minimal field width is set and the previous one returned. The default value
is 0. This is also the value to which the field width is set after a value has been written.
Note that the field width is never used to truncate output. Thus, you cannot specify a maxi-
mum field width. Instead, you have to program it. For example, you could write to a string and
output only a certain number of characters.
486 8: The Standard I/O Library in Detail
Using the member function £i11(), a filling symbol can be defined and queried. The default
filling symbol is a space.
For alignment within a field, there are three flags. These are defined in the class ios, with an
appropriate mask, as shown in Table 8.10.
In contrast to the field width, filling symbols and alignment remain valid until a new command
defines something else.
Table 8.11 shows the effects of the functions and flags during the output of different constants.
The underscore is used as a filling symbol.
Alignment
left
right
internal
For the width() and fi111() member functions, the manipulators shown in Table 8.12 are
defined. In order to make the manipulators available, the header file <iomanip> has to be in-
cluded.
Manipulator | Meaning
setw(val) sets the field width for I/O to val
(corresponds to width())
setfill(c) defines c as the fill character (corresponds to £i11() )
left forces left-aligned output
right forces right-aligned output
internal forces left-aligned sign and right-aligned value
You can also use the field width to define the maximum number of characters read when character
sequences of type char* are input. If the value of width() is not 0, then at most width() —1
characters are read.
To be on the safe side, width() should always be used if C-strings are read with the >>
operator, because otherwise, if there are too many characters, the memory behind the C-string is
simply overridden:
char buffer[81];
Two format flags influence the output of numbers: showpos and uppercase (see Table 8.13).
Use of the showpos flag forces the output of a positive sign. The flag uppercase defines
that, during the output of numbers, uppercase letters are output instead of lowercase ones. This
affects integral numbers that are output in hexadecimal form and floating-point values that are
output in scientific notation.
488 8: The Standard I/O Library in Detail
Meaning
showpos forces the writing of positive signs
uppercase forces the use of uppercase letters
Both flags can be set or deleted with the manipulators listed in Table 8.14.
ManipulatorMeaning
showpos forces the output of a positive sign
(sets the flag ios: : showpos)
noshowpos prevents the output of a positive sign
(deletes the flag ios: : showpos)
uppercase forces the use of uppercase letters
(sets the flag ios: : uppercase)
nouppercase | forces the use of lowercase letters
a es (deletes
ee the flag ios: :ee
uppercase)
2 2 ieee ee
Table 8.14, Manipulators for signs and letters of numeric values
Numeric Base
A group of three flags defines what base is used for I/O of integer values. The flags are
defined
in the class ios_base, with the corresponding mask (see Table 8.15).
A change in base applies to the processing of all integer numbers until the flags are reset. By
default, decimal format is used.
If no flag is set for the numeric base (this is not the default), the base of read values depends
on the leading characters of the value. If a value begins with Ox or OX, the number is read as a
hexadecimal; if it begins with 0, it is read as an octal; otherwise, it is read as a decimal.
There are basically two ways to switch between these flags:
1. Seta flag and reset the other one:
std::cout.unsetf(std::ios::dec);
std::cout .setf (std: 10s) nex) ;
2. Seta flag and in the process reset all other flags of the mask automatically:
std: :cout.setf(std::ios::hex, std::ios::basefield) ;
In addition, manipulators are defined that significantly simplify handling of these flags (see Ta-
ble 8.16).
Manipulator | Meaning
oct 1/O octal
dec I/O decimal
hex I/O hexadecimal
For example, the following statements output x and y hexadecimal and z decimal:
HME S25 Whe AE
An additional flag, showbase, lets you write numbers according to the usual C/C++ convention
for indicating numeric bases of literal values (see Table 8.17).
Flag Meaning
~
showbase | if set, indicates the numeric base
If the showbase flag is set, octal numbers are written with a leading 0 and hexadecimal
numbers are written with a leading Ox (or OX if ios: : uppercase is set).
490 8: The Standard I/O Library in Detail
std::cout << std::hex <<"127 <<) << 955" << istd: vend! -
std: :cout.setf(std::ios::showbase) ;
std;;¢out <<" 127° << 24 << 255) << std::endl
std: :cout.setf(std::ios::uppercase);
stds coute<cal2fe<<e ae <0 2558<<Gstdycendl >
Manipulator Meaning
showbase show numeric base (sets the flag ios: : showbase)
noshowbase do not show numeric base (deletes the flag ios: : showbase)
Floating-Point Notation
For the output of floating-point values, there are several format flags and members. The flags
listed in Table 8.19 basically define whether the output is given in decimal or exponential rep-
resentation. They are defined in the class ios, with the appropriate mask. If ios: : fixed is
set, floating-point values are output with the decimal notation. If ios: :scientific is set, the
scientific (exponential) notation is used.
For the definition of precision, there is the member function precision() (see Table 8.20).
Without parameters, the precision is returned. With parameters, the passed value is
set as the
new precision (and the old value is returned). The default value for the precision is 6.
8.1 The Standard Stream Classes 491
If the ios: : fixed flag is set, floating-point values are written in decimal representation; if
the ios: : scientific flag is set, exponential representation is used. The precision set using
precision() defines the number of places after the decimal point in both cases. Note that it is
not simply cut off, but rounded.
By default, neither ios: :fixed nor ios: :scientific is set. In this case, the notation
used depends on the value written. All meaningful but, at most, precision() decimal places
are written as follows: A leading zero before the decimal point and/or all trailing zeros, and
potentially even the decimal point, are removed. If precision() places are sufficient, decimal
notation is used; otherwise, scientific notation is used.
Using showpoint, we can explicitly request that the decimal point and trailing zeros are
written until there are sufficient precision() places (see Table 8.21).
Flag Meaning
showpoint | forces the output of a decimal point
For example of two concrete values, the somewhat more complicated dependencies of the
flags and the precision are shown in Table 8.22.
The output of a positive sign can also be forced for floating-point values, using the showpos
flag. Similarly, use the uppercase flag with the scientific notation, forces a large E.
492 8: The Standard I/O Library in Detail
The notation and the precision of floating-point values can also be changed using the manipu-
lators listed in Table 8.23. Because setprecision is a manipulator with parameters, the header
file <iomanip> has to be included.
Manipulator Meaning
fixed forces decimal notation
scientific forces scientific notation
setprecision(value) | sets value as the new value for the precision
showpoint forces the output of a decimal point
(sets the flag ios: : showpoint)
noshowpoint a decimal point is only output if needed
(deletes the flag ios: : showpoint)
The boolalpha flag defines the format that is used to read and write Boolean values. It deter-
mines whether a numeric or a textual representation is used (see Table 8.24).
Flag Meaning
boolalpha | if set, textual representation
if not set, numeric representation
If the boolalpha flag is not set (this is the default behavior), a numeric representation is used
for the input and output of Boolean values. In this case, 0 is used for false and 1 for
true. If
a Boolean value is read that is neither 0 nor 1, this is regarded as an error and failbit
is set in
the stream.
If the boolalpha flag is set, Boolean values are shown in a textual form. To do so,
the textual
representation of true and false that belong to the respective language environme
nt are used.
In the standard language environment of C++, these are the character strings ‘true’
and ‘false’;
in other language environments, there may be other character strings (for example,
‘wahr’ and
‘falsch’ within a German environment).
8.1 The Standard Stream Classes 493
Special manipulators are defined for the convenient manipulation ofthis flag (see Table 8.25).
Manipulator Meaning
boolalpha forces the textual representation
(sets the flag ios: :boolalpha)
noboolalpha | forces the numeric representation
(deletes the flag ios: : boolalpha)
For example, using the following statements, the value of the Boolean variable b is written
once numerically and once textually:
bool bs
Depending of the value of b and the current language environment, the output may read:
0 == false
Two flags are still missing in the list of format flags: skipws and unitbuf (see Table 8.26).
Meaning
skipws skip leading whitespaces with >> operator
unitbuf | empty output buffer after every output operation
The skipws flag is set by default. This causes the >> operator to skip leading whitespaces.
This is helpful in order to, for example, automatically skip the separating whitespaces when
reading in numeric values. However, this means that, using the >> operator, no whitespaces can
be read. In order to make this possible, the flag has to be set.
The unitbuf flag controls the buffering of outputs. If unitbuf is set, outputs are basically
written without being buffered. After every output operation, the output buffer is automatically
flushed. This flag is normally not set by default, which means that the outputs are buffered. With
the standard streams for error outputs, cerr and wcerr, this flag is set automatically in order to
force the unbuffered output.
Both flags can be set and unset using the manipulators listed in Table 8.27.
494 8: The Standard I/O Library in Detail
Manipulator | Meaning
skipws skips leading whitespaces with the >> operator
(sets the flag ios: : skipws)
noskipws does not skip leading whitespaces with the >> operator
(deletes the flag ios: : skipws)
unitbuf empties the output buffer after every output operation
(sets the flag ios: : unitbuf)
nounitbuf does not empty the output buffer after every output operation
(deletes the flag ios: :unitbuf)
8.1.7 Internationalization
One can format I/O according to national conventions. One such internationalization (or, in short,
i18n*) is supported via the class std: : locale.
Each stream belongs to a language environment via its association with a so-called ‘locale’
object. The initial locale is a copy of the global language environment attached at the time when
the stream was created. It can be queried and changed with the functions listed in Table 8.28.
The following example shows how these member functions can be used:
// to/loci1.cpp
#include <iostream>
#include <locale>
int main()
{
// use the classic language environment in order to
// read from the standard input
std::cin.imbue(std::locale::classic());
The statement
std::cin.imbue(std::locale::classic());
assigns the ‘classic’ locale to the standard input channel cin, which behaves like classic C. The
expression
stdemlocale-wichassic®
assigns the locale de_DE to the standard output channel. The string de_DE stands for ‘German in
Germany’. The general syntax for locale names is as follows:
Table 8.29 presents a selection of typical language strings. However, note that these strings are
not yet standardized. For example, sometimes the first character of language is capitalized. Some
implementations deviate from the format mentioned previously and, for example, use english to
select an English locale. All in all, the locales that are supported by a system are implementation
specific.
The expression
std: zlocale("de_DE"));
is only successful if this locale is supported by the system. If this is not the case, an exception of
the type std: :runtime_error (see Section 3.6.3 on page 95) is thrown.
If everything works, the input is read according to the classic (English) conventions and the
output is written according to German conventions. In the example, the loop therefore reads
floating-point values in English format and writes them in German format. So, for example, the
value read as
47.11
is written as
47,11
Normally, a program only defines locales if, for example, configuration data are read from a file
and one has to make sure that this data can be read independently from the current locale (with
a German locale, for example, there would be an attempt to read in 47.11 as a floating-point
value and to return the value 47, because the point terminats the floating-point value). Instead,
496 8: The Standard I/O Library in Detail
Locale Meaning
default: ANSI-C conventions (English, 7 bit)
German in Germany
German in Germany with ISO Latin-1 encoding
German in Austria
German in Switzerland
English in the USA
English in Great Britain
English in Australia
English in Canada
French in France
French in Switzerland
French in Canada
Japanese in Japan with Japanese Industrial Standard (JIS) encoding
Japanese in Japan with Shift JIS encoding
Japanese in Japan with UNIX JIS encoding
Japanese in Japan with Extended UNIX code encoding
Korean in Korea
Chinese in China
Chinese in Taiwan
ISO Latin, 7 bit
ISO Latin, 8 bit
POSIX POSIX conventions (English, 7 bit)
the locale normally determines the language environment by means of the environment variable
LANG. If one submits an empty string to the locale object for initialization, the correspond
ing
object is created:
std::locale langLocale(""); // locale by means of LANG
Using the member function name (), one can prompt the name of a locale as a string:
// output name of the locale
std::cout << langhLocale.name() << std::endl;
8.1.8 Summary
e By default, stream classes are available for I/O. Classes for input, for output and
for input and
output are differentiated.
e Numerous functions are predefined for stream classes. Besides the actual
I/O operations,
these include functions for format definition, accessing status bits, automati
c triggering of
exceptions and for internationalization.
8.1 The Standard Stream Classes 497
e Manipulators allow streams to be manipulated in input or output statements. They can also
be defined by the user.
Formatting is controlled not only by member functions, but also by numerous flags and ma-
nipulators.
498 8: The Standard I/O Library in Detail
The following special stream types are made available for file access:
ft The class template basic_ifstream<>, with the two specializations ifstream and
wifstream, are used for read access to files (‘input file stream’).
The class template basic_ofstream<>, with the two specializations ofstream and
wofstream, are used for write access to files (‘output file stream’).
The class template basic_fstream<>, with the two specializations fstreamand wfstrean,
are used for files that can have read and write access.
The class template basic_filebuf<>, with the specializations filebuf and wfilebuf, are
used by other file-stream classes for the actual reading and writing.
streambuf / wstreambuf
basic_ios<>
ios / wios basic _filebuf<>
filebuf / wfilebuf
basic_istream<> basic_ostream<>
istream / wistream ostream / wostream
basic _iostream<>
iostream / wiostream
basic _fstream<>
fstream / wfstream
These types are defined in the header file <fstream> and are associated with the fundamental
stream Classes, as is shown in Figure 8.2.
A considerable advantage of the file-stream classes, as compared to the file-access mecha-
nisms of C, is that the files are automatically opened during the declaration of the objects and are
automatically closed when the object is destroyed. This is guaranteed by the appropriate con-
structors and destructors. However, because opening a file can fail, one should check the stream
State after the construction of a file-stream object.
// forward declarations
void writeCharsetInFile(const std::stringk filename) ;
void printFile(const std::stringk filename);
int main()
sf
fryer
writeCharsetInFile("charset.out") ;
printFile ("charset.out") ;
}
catch (const std::stringk msg) {
std::cerr << "Exception: " << msg << std::endl;
return EXIT_FAILURE;
t
i
With the following query, it is determined whether the status of the stream is OK, and therefore
whether the opening was successful:
// was the file really opened?
Le UC iste)
/| NO, throw exception
throw "cannot open file \"" + filename
de WOU sore writing";
}
Finally, in a loop, all values from 32 to 126 are written, with the appropriate characters:
// write character set into file
for Cint 1=32; i<127; ++i) {
// output value as number and character:
frlew<ae value u<< sta a setw
(Ss lic ca "
<< "character: " << static_cast<char>(i) << std::endl;
tj
At the end of the function, the file is automatically closed, because the scope of the object is left.
The destructor of the class of stream is defined accordingly.
Similarly, the file submitted for reading is opened in printFile(). Again, if this fails, an
appropriate exception is thrown. Otherwise, all the characters are read from the file and are
output to the standard output channel cout. The opened file is automatically closed when the
function is exited.
Flag Meaning
in open for reading (default with ifstream)
out open for writing (default with of stream)
app always append when writing
ate position at the end of the file after opening
trunc | delete previous file content
binary | do not replace special characters
The binary flag should be set for access to binary files (such as executable programs). By
doing so, it is ensured that the characters are read and written as they are. If binary is not
set, it is automatically taken into account that, in the world of Windows, the line end is not
marked by a newline character, but by the CarriageReturn/LineFeed character sequence (CR/LF).
502 8: The Standard I/O Library in Detail
Without binary, therefore, on Windows systems, during the read, the CR/LF character sequence
is converted into the newline character, and, during the write, a newline is written as the CR/LF
character sequence.
Some implementations provide additional flags, such as nocreate (the file must exist when
it is opened) and noreplace (the file must not exist). However, these flags are not standard and
thus are not portable.
The flags can be combined with the | operator. They are then submitted to the constructor as
an optional second argument. For example, the following statement opens a file for writing and
makes sure that the written text is appended at the end of the file:
std::ofstream file("xyz.out", std::ios::out | std::ios::app);
Table 8.31 correlates the various combinations of flags with the strings used in the interface of C’s
function for opening files: fopen(). The combinations with binary and ate are not included.
A set binary flag corresponds to strings with b appended, and a set ate flag corresponds to a
seek to the end of the file immediately after opening. Other combinations not listed in the table,
such as trunc| app, are illegal.
Table 8.32. Member functions for explicitly opening and closing files
8.2 File Access 503
The following example shows one possible use. It opens all files, one after the other, submit-
ted as parameters in the command line and outputs their contents (this corresponds to the UNIX
program cat):
i tof cotinenp
// header files
#include <fstream>
#include <iostream>
using namespace std;
// close file
filerclose
Or:
Inside the program, the submitted filenames are sequentially opened as file, then output and
closed again. Note that, after the output of the contents, clear() is always called. This is
necessary because eofbit and failbit are set at the end of the file and these must be cleared
before every other operation. open() does not clear these bits. This is also the case if different
file is opened.
504 8: The Standard I/O Library in Detail
For positioning in files, C++ provides the member functions listed in Table 8.33.
For safety, the read and write positions are distinguished between g (stands for ‘get’) and p
(stands for ‘put’). The reading position functions are defined in istream, while the writing posi-
tion functions are defined in ostream. This does not mean, however, that they can be called for
all objects of the istream and ostream classes. For example, a call to cin, cout or cerr does
not make sense. Positioning in files is defined in the base classes because, usually, references to
objects of type istream and ostream are passed as parameters.
The seekg() and seekp() funstions are called with absolute or relative position statements:
e To handle absolute positions, you must use tellg() and tellp(). They return an absolute
position as a value of type pos_type. This value is not an integral value, or simply the
position of the character as an index. This is because the logical position and the real position
can differ. For example, in MS-DOS text files, newline characters are represented by two
characters in the file, even though it is logically only one character. Things are even worse
if
the file uses some form of multi-byte representation for the characters.
For example:
// mark current position
Std: :ios::pos_type pos = file.tellg();
> This was possible in some C++ versions before the standardization.
8.2 File Access 505
Constant | Meaning
beg relative to start of file (‘begin’)
cur relative to the current position (‘current’)
end relative to the end of the file (‘end’)
For example:
// position at the start
file.seekp(0, std::ios::beg);
Note that it is impossible to position before the start of the file or after the end of the file. Other-
wise the behavior is not defined.
The following example demonstrates the use of the above functions for positioning by means
of a function that outputs the same file twice:
// to/cat2.cpp
// header files
#include <iostream>
#include <fstream>
#include <string>
Note that, at the end of the file, eofbit and failbit are set. By doing so, a positioning of
the stream is no longer possible. For this reason, clear() has to be called before the call of
seekg().
reason, after a redirecting a stream, the status before the redirection should always be restored.
The following program shows a complete example:
// to/redirect.cpp
#include <iostream>
#include <fstream>
#include <string>
int main()
{
Std cout << first linew<“stdrendilh:
8.2.7 Summary
basic _streambuf<>
streambuf / wstreambuf
basic ios<>
ios / wios basic_stringbuf<>
stringbuf / wstringbuf
basic _iostream<>
iostream / wiostream
basic _stringstream<>
stringstream/wstringstream
The following program shows the use of string streams for formatted writing in streams:
// to/sstr1.cpp
#include <iostream>
#include <sstream>
int main()
if
// create string stream for formatted writing
std: :ostringstream os;
os << "dec: " << 15 << std: :hex << " hex: " << 15 << std::endl;
dec: 15 hex: f
float: 4.67
OGbo De xost
jedloenee Cle (67/
First, the value 15, in both decimal and hexadecimal formats, is written to the string stream and
the string is output. A floating-point value is then written to the string stream, appending it to
the previous content. Then the write position is set to the start with seekp(), and the previous
content of the string is overwritten by the octal value of 15. As the final output shows, the
remaining characters in the string are unchanged. Because the string itself contains a line break
after each line (due to the writing of std: : end1), and std: : endl is written again at the end of
every output, the output contains a trailing blank line.
If we want to empty the string of a string stream, an empty string has to be assigned to the
stream:
std::ostringstream os; // create string stream
int main()
{
// create string that will be read
std::string s = “Pi: 3.1415":
return out;
The first template parameter is the destination type and is explicitly specified (see Section 7.2.4
on page 431). The second template parameter is implicitly determined by means of the passed
argument. In the function, the passed argument is written to a string stream and is then read again
as a value of the destination type. The last query determines whether there are characters that
were not processed during the conversion. This ensures that the string “42.42” is not successfully
converted into an int.
The call of this operator could look, for example, as follows:
// to/lexzcast.cpp
#include <iostream>
#include <string>
#include <cstdlib>
#include "lexcast.hpp"
In the boost repository for supplementary C++ libraries, there is a ‘more condensed’ version of
this operator (see http://www. boost. org).
The stream classes that provide formatted access to C-strings are only available due to backwards
compatibility. They are, however, still used, and, because their usage creates some pitfalls, they
will be briefly discussed.
Similarly to the string-stream classes, there are stream classes for type char*:
e The class istrstream for formatted reading from C-strings (‘input string stream’).
e The class ostrstream for formatted writing to C-strings.
e The class strstream for C-strings that are used for reading and writing.
e The class strstreambuf as a buffer for char* streams.
They are defined in the header file <strstream>.
An istrstream can be initialized with a string (type char*) in two ways: the string can either
contain the end-of-string character ‘\0’, or the number of characters in the string can be passed
as an additional argument.
A typical example is the reading and processing of a name:
char buffer[1000] ; // buffer for a maximum of 999 characters
// read line
std::cin.get (buffer,sizeof(buffer));
input >> x;
8.3 Stream Classes for Strings au)
A char* stream for writing can either have a dynamic size or be initialized with a buffer of
a fixed size. With ios: :app or ios: : ate, one can append the character written to the string
stream to the characters that are already in the buffer.
With the member function str (), the content of the string is provided. However, the follow-
ing has to be taken into account:
e Provided that the char stream was not initialized with a buffer of a fixed size, the ownership
of the buffer is transferred to the caller of str (). To avoid memory leaks, you have to return
the buffer back to the char* stream again, using freeze().
e After the call of str(), the stream is frozen and can no longer be manipulated. By calling
freeze(), you can ensure that the char* stream can be manipulated once more.
e str() does not append and end-of-string character ‘\0’. If this is required, it must be explic-
itly appended using the manipulator std: : ends (see Section 8.1.5 on page 481).
The following program shows how char* streams can be used for formatted writing:
// to/charstr1.cpp
#include <iostream>
#include <strstream>
int main()
{
// create dynamic char* stream for writing
std::ostrstream buffer;
Note that the call of str() freezes the stream, which means that for further modifications and
correct memory management you have to ‘unfreeze’ the stream:
buffer.freeze(false) ;
Note also that std: :ends is appended, so that the string can be used as a C-string. Because
this end-of-string character would terminate the C-string in the middle of the value, is must be
overwritten for additional appended characters. By calling
buffer.seekp(-1,std::ios::end) ;
8.3.4 Summary
All language features that have not been explained previously, but play an important role in
practice, are introduced in this chapter. In addition to these, I will also provide some further
details about the standard library.
To be more precise, this chapter covers details of STL containers (in particular, vectors),
numeric limits, smart pointers, function objects, various other aspects of the use of new and
delete operators, function and member pointers, the combination of C++ code with C code,
and some additional keywords.
518 9: Other Language Features and Details
Table 9.1 lists the constructors and the destructor for vectors. Vectors can be created with or
without elements for initialization. In addition, an initial size can be passed without any accom-
panying elements, in which case the elements are created with their default constructor.
Expression |Meaning
vector<type> v creates an empty vector
vector<type> v1(v2) creates a vector as a copy of another vector (all elements
are copied)
vector<type> v(n) creates a vector with n elements, which are created with
the default constructor
vector<type> v(n,elem) creates a vector with n elements, which are copies of
elem
vector<type> v(beg,end) | creates a vector and initializes it with copies of the
elements in the range [beg,end)
v.~vector<type>() deletes all elements and frees the memory
' If the compiler does not Support member function templates, you can
only pass a container of the same
type for initialization.
9.1 Additional Details of the Standard Library 519
Comparisons
Expression | Meaning
vi == v2 returns whether v1 is equal to v2
Wi Ae 2 returns whether v1 is not equal to v2
(equivalent to ! (v1 == v2))
Vv ie<ev 2 returns whether v1 is less than v2
wae? returns whether v1 is greater than v2
(equivalent to v2 < v1)
vi <= v2 returns whether v1 is less than or equal to
v2
(equivalent to !(v2 < v1))
vi >= v2 returns whether v1 is greater than or equal
to v2
(equivalent to !(v1 < v2))
Comparisons with the normal comparison operators are only possible between vectors of
the same element type. For comparison of two containers of different classes or types, STL
algorithms must be used (see Section 9.1.3 on page 526).
To use vectors effectively (and correctly), you should understand how size and capacity work
hand in hand. Table 9.3 lists the appropriate operations.
Expression Meaning
v.size() returns the current number of elements
v.empty() returns whether the container is empty
(equivalent to size() == 0)
v.max_size() returns the maximum possible number of elements
v.capacity() returns the capacity (maximum number of elements without
reallocation)
. reserve (num) enlarges the capacity to at least num elements
Table 9.3. Member functions for the size and capacity of vectors
520 9: Other Language Features and Details
Y
There are three functions that can be used to query the size of vectors:
e size()
Returns the current number of elements in the vector.
The member function empty () checks whether the number of elements is 0 (i.e. the vector
is empty).
e max_size()
Returns the highest possible number of elements that a vector can hold. This value is imple-
mentation dependent. Because a vector typically manages all elements in a single memory
block, there may be limitations placed on some PCs or operating systems. Otherwise, it
usually returns the maximum value allowed for an index.
e capacity()
Returns the number of elements that a vector can take without allocating new memory. This
value is important for optimizing performance, because a request for new memory (a real-
location) invalidates all references to elements. This means that all iterators that refer to
elements of a vector become invalid.
In addition, a request for new memory can be time consuming as all elements from the old
memory are copied into the new memory. A reallocation means that a copy constructor and a
destructor are called for all elements. The copy constructor copies them into the new memory,
and the destructor destroys them in the old memory. This can be quite costly, especially if
these operations are not trivial.
It is, of course, useful to avoid requests for new memory. There are several ways of doing this:
¢ One option is to initialize a vector with a certain number of elements on its creation.
This
value can be submitted in various ways.
If no values are passed for initialization, the elements are created with the default con-
structor:
std: :vector<Elem> v(5); // creates a vector with five elements
// (five calls to Elem(), the default constructor)
This means that a default constructor has to be defined, and if its behavior is
non-trivial, this
can be quite costly.
e Another option is to reserve space for a certain number of elements, The reserve
© function
is provided for this:
std::vector<Elem> v; // creates an empty vector
v.reserve(5) ; // reserve space for five elements
The exact way the capacity is handled is implementation dependent. This
means that individual
implementations can always increase the Capacity in ever-increasing
steps. However, the lan-
guage standard states that the capacity of vectors can never be reduced
(passing a value less than
the current capacity to reserve () will have no effect). In this respect,
it is guaranteed that when
elements are deleted, references to the elements in front of the
deleted elements remain valid.
9.1 Additional Details of the Standard Library 521
Assignments
Table 9.4 lists the different ways of assigning new sets of elements to a vector.
Expression | Meaning
vi = v2 assigns all elements of v2 to v1
v.assign(n) assigns n elements, which are created with the default constructor
v.assign(n, elem) assigns n elements, which are copies of elem
v.assign(beg,end) | assigns copies of the elements in the range [beg,end)
vi.swap(v2) swaps the elements of v1 with those of v2
swap(vi1,v2) same as above (global function)
The assignment operator assigns all elements of a vector to another one. During this oper-
ation, depending on the situation and implementation, assignment operators, copy constructors
(if the vector increases in size) and destructors (if the vector becomes smaller) are called for the
elements.
The forms of the assign() function are equivalent to the abilities of the constructors. The
assignment of a range can be used to assign an element from another container to a vector *:
Stdecsist<eilem> a:
std: :vector<Elem> v;
v.assign(1.begin() ,l.end());
The swap() function offers much better efficiency than the traditional assignment operators. It
swaps only the internal data of the containers. In fact, it swaps only the internal pointers that
refer to the data. So, swap() is guaranteed to have a constant complexity, instead of the linear
complexity of an assignment. Therefore, if an assigned object is not needed after the assignment,
swap() should be used.
Element Access
To gain direct access to the elements of a vector, the operations shown in Table 9.5 can be used.
Note that, except for at (), no checks are made to discover whether an appropriate element
exists. With an empty vector, calls of [], front (), and back() therefore result in undefined
behavior:
std::vector<Elem> v; // empty!
? Unless a system supports member function templates, elements in the assigned range must be of the same
type.
p22 9: Other Language Features and Details
Expression | Meaning
v.at (idx) | returns the element at index idx (with range checking)
v Lidx] returns the element at index idx (without range checking)
v.front() | returns the first element (without checking whether it exists)
v.back() returns the last element (without checking whether it exists)
This means that the programmer must ensure that the index for an index operator is valid and
that the container is not empty when front () and back() are called:
std: :vector<Elem> v; //empty!
Lfe(visize()'> 5) r1
v[5] = elem; // OK
}
if (!v.empty()) {
Std=:cout.<<:v.tront(): 1) OK.
} ;
v.at(5) = elem; // ERROR: exception std: : out_of _range
The at () function, on the other hand, checks the range limits. If there is a violation, an exception
of the type std: : out_of_range (see Section 3.6.3 on page 95) is generated.
The standard guarantees that all elements of a vector can be accessed as an array. This has an
important consequence: you can use a vector in all cases in which you could use a dynamic
array.
For example, if a C function requires an array of windows, this can be managed using
a
vector:
void raise(Window* window, int number);
std::vector<Window> window;
Iterator Functions
For indirect access to the elements of the vector, the normal iterator
functions (see Table 9.6) are
provided.
9.1 Additional Details of the Standard Library 38)
Expression | Meaning
v.begin() returns an iterator for the first element
v.end() returns an iterator for the position behind the last element
v.rbegin() | returns a reverse iterator for the first element of a reversed iteration
(therefore the last element)
v.rend() returns a reverse iterator for the position after the last element of a
reversed iteration (therefore the position in front of the first element)
The iterators remain valid until an element with a smaller index is inserted or deleted, or the
Capacity is increased with the addition of new elements or by calling reserve().
Table 9.7 shows the member functions used to insert and delete elements in vectors.
Expression Meaning
v.insert (pos) inserts an element, at the iterator position pos, that is created
with the default constructor; returns the position of the new
element
v.insert (pos, elem) inserts a copy of elem at the iterator position pos and returns
the position of the new element
v.insert (pos,beg,end) | inserts copies of the elements in the range [beg,end) at the
iterator position
v. push_back (elem) appends a copy of elem
v.erase (pos) erases the element at the iterator position pos and returns the
position of the sequence element
v.erase (beg,end) erases all elements in the subrange [beg,end) and returns the
position of the sequence element
v.pop_back() erases the last element (without returning it)
v.resize(num) changes the number of the elements to num, whereby, with
an enlargement, new elements are created with the default
constructor
v.resize(num,elem) changes the number of elements to num, whereby, with an
enlargement, new elements are created as copies of elem
v.clear() erases all elements (empties the container)
With all these member functions, it should be remembered that no checks for invalid parame-
ters are carried out. The programmer must therefore ensure that iterators really refer to elements
of the vector, that the start of a range is before the end, and that no attempt is made to erase an
element from an empty set.
524 9: Other Language Features and Details
Y
Regarding performance, you should consider that inserting and removing happens faster
when
e the capacity was defined large enough on entry;
¢ multiple elements are inserted with a single operation rather than by multiple operatioons;
and
e elements are inserted and removed at the end of the vector.
Inserting or removing elements invalidates references, pointers, and iterators that refer to the
following elements. If an insertion causes reallocation, it invalidates all references, iterators, and
pointers.
Deleting specific elements is not directly supported. Instead, we can use the following algo-
rithm:
std: :vector<Elem> v;
Expression Meaning
ContType<type> c creates an empty container
ContType<type> c1(c2) creates a container as a copy of another
ContType<type> c(beg,end) | creates a container and initializes it with copies of
elements in the range [beg,end)
c. ~ ContType<type> () deletes all elements and frees the memory
c.size() returns the current number of elements
c.empty () returns whether the container is empty
(equivalent to size() == 0, but may be quicker)
c.max_size() returns the maximum possible number of elements
c1 == c2 returns whether c1 is equal to c2
ele atc? returns whether c1 is not equal to c2
(equivalent to !(c1 == c2))
cie<ic2 returns whether c1 is less than c2
el. xc? returns whether c1 is greater than c2
(equivalent to c2 < c1)
61 (<= 62 returns whether c1 is less than or equal to c2
(equivalent to !(c2 < c1))
c1 >= c2 returns whether ci is greater than or equal to c2
(equivalent to !(c1 < c2))
ci =5.¢2 assigns to c1 all elements of c2
c1.swap(c2) swaps the elements of c1 with those of c2
swap(cl,c2) same as above (global function)
c.begin() returns an iterator for the first element
c.end() returns an iterator for the position behind the last element
c.rbegin() returns a reverse iterator for the first element of a reversed
iteration (therefore the last element)
c.rend() returns a reverse iterator for the position after the last
element of a reversed iteration (therefore the position in
front of the first element)
c.insert (pos ,elem) inserts a copy of elem
(the return value and the meaning ofpos are different)
c.erase (pos) erases the element at the iterator position pos
c.erase (beg, end) erases all elements in the subrange [beg,end) (and returns
the position of the following element)
c.clear() erases all elements (empties the container)
Non-Modifying Algorithms
All non-modifying algorithms are listed in Table 9.9. These change neither the sequence nor the
value of the elements for which they are called. The algorithms can be used for all containers.
Most important is the for_each() algorithm. This allows very complicated actions to be
carried out with the elements. An example of its use can be found in Section 9.2.1 on page 538.
Modifying Algorithms
Modifying algorithms change the values of the elements. This may apply to the processed
range
or a target range of the algorithm. Provided the modification is performed for a target range,
and
this is not the same as the source range, the source range is not changed. Table 9.10 lists
the
modifying algorithms.
Note that elements of associative algorithms are constant. This ensures that an element
mod-
ification cannot compromise the sorted order of the elements. Therefore, you cannot
use asso-
Ciative containers as a destination for modifying algorithms.
9.1 Additional Details of the Standard Library 527
Algorithm Functionality
for_each() calls an operation for every element
count () counts the elements
count_if() counts the elements that fulfil a criterion
min_element () returns the position of the smallest element
max_element () returns the position of the largest element
find() searches for a particular element
tindeit () searches for a particular element that fulfils a criterion
search_n() searches n particular values, one after another
search() searches the first subsequence of particular values
find_end() searches the last subsequence of particular values
find_first_of() searches one of several possible elements
adjacent_find() searches two adjacent elements with particular
properties
equal () tests two ranges for equality
mismatch() returns the first two different elements of two ranges
lexicographical_compare() | compares two ranges for sorting
Algorithm Functionality
for_each() calls an operation for every element
copy () copies a range
copy_backward() | copies a range backwards
transform() modifies the elements of one or two ranges
merge () combines the elements of two ranges
swap_ranges() swaps the elements of two ranges
fa LEC) gives all elements a fixed value
£41 1en ©) gives n elements a fixed value
generate() gives all elements a generated value
generate_n() gives n elements a generated value
replace() replaces particular values with a new fixed value
replace_if() replaces values that fulfil a criterion with a fixed new
value
replace_copy() copies a range and replaces elements
replace_copy() copies a range and replaces elements that fulfil a
criterion
Removing Algorithms
Removing algorithms remove elements from a range. This affects either the processed range or
is a
a target range in which the elements that are not removed are copied. Provided that there
528 9: Other Language Features and Details
Y
target range, and that this is not equal to the source range, the source is not changed. Table 9.11
lists the removing algorithms.
Algorithm Functionality
remove () removes elements with a particular value
remove_if() removes elements that fulfil a criterion
remove_copy() copies a range and removes elements with a
particular value
remove_copy_if() | copies a range and removes elements that fulfil a
criterion
unique () removes duplicates, one after the other
unique_copy () copies and removes duplicates, one after the other
Again, as the elements of associative containers are considered as constants, these algorithms
cannot be used if the modified range or the target range is (part of) an associative container.
Note that removing algorithms remove elements logically only by overwriting them with the
following elements that were not removed. Thus, they do not change the number of elements
in the ranges on which they operate. Instead, they return the position of the new “end” of the
range. It’s up to the caller to use that new end, such as to remove the elements physically (see
Section 9.1.1 on page 524).
Mutating Algorithms
Mutating algorithms change the order of elements without altering their value. However,
this
does not mean that no assignments take place. With every mutation, elements are
assigned to
one another because the storage of the elements remains stable. Table 9.12 lists the
mutating
algorithms.
Algorithm Functionality
reverse() reverses the sequence of elements
reverse_copy() copies and reverses the sequence of the elements
rotate() rotates the elements
rotate_copy() copies and rotates the elements
next_permutation() | permutates the sequence in a given direction
prev_permutation() | permutates the sequence in the ‘other’ direction
random_shuffle() mixes up the sequence of the elements
partition() moves certain elements to the front
stable_partition() | moves certain elements to the front and keeps
relative
sequences
Again, as the elements of associative containers are considered as constants, these algorithms
cannot be used if the modified range or the target range is (part of) an associative container.
Sorting Algorithms
Sorting algorithms are a special kind of mutating algorithm because they also change the order
of the elements. As a result, the elements are at least partial sorted. The sorting criterion can be
submitted as a parameter. By default, elements are compared using the < operator. Table 9.13
lists the sorting algorithms.
Algorithm Functionality
sort () sorts elements
stable_sort () sorts elements but preserves the sequence of elements with
identical values
partial_sort() sorts the first n elements
partial_sort_copy() | copies the first n sorted elements
nth_element () sorts a certain element
make_heap() converts a range into a heap
push_heap() integrates an element in a heap
pop_heap() removes an element from a heap
sort_heap() sorts a heap (by doing so, it is no longer a heap)
Again, as the elements of associative containers are considered as constants, these algorithms
cannot be used if the modified range, or the target range, is (part of) an associative container.
In addition to traditional complete sorting, there are also different algorithms that only sort
partially. Because sorting is time consuming, it is often worthwhile not to sort all elements if
this is not necessary. If, for example, we require the middle element after sorting, or a grouping
between the first and the second half of a sorting, the nth_element() function can be used,
which processes the container without sorting all elements completely.
As long as one or several ranges are completely sorted, the algorithms that are introduced here
can be used. There are both non-modifying, as well as modifying, algorithms. Table 9.14 lists
the algorithms for sorted ranges.
The advantage of these algorithms is similar to that of associative containers over sequential
containers: they demonstrate a considerably improved performance.
The first five sorted-range algorithms in Table 9.14 are non-modifying, as they search only
according to their purpose. The other algorithms combine two sorted input ranges and write the
result to a destination range. In general, the result of these algorithms is also sorted.
530 9: Other Language Features and Details
Algorithm Functionality
binary_search() returns whether an element is contained
includes() returns whether all elements of a subset are contained
lower_bound() returns the first element that is greater than or equal to
a value
upper_bound() returns the last element that is greater than a value
equal_range() returns a range of elements with a certain value
merge() combines the elements of two ranges together
set_union() forms the union set of two ranges
set_intersection() forms the intersection of two ranges
set_difference() forms the difference set of two ranges
set_symmetric_difference() forms the complementary set of two ranges
inplace_merge() merges two subranges that are directly after one
another
Numeric Algorithms
A special kind of algorithm is the numeric algorithm. These are listed in Table 9.15.
Algorithm Functionality
accumulate() connects all elements
partial_sum() connects an element with all predecessors
adjacent_difference() connects an element with its predecessor
inner_product() connects all operations of two elements of two ranges
Numeric algorithms are used to numerically combine the elements of one range with
another.
If you understand the names, you get an idea of the purpose of the algorithms. However,
these
algorithms are more flexible and more powerful than they may seem at first. For example,
as a
default accumulate() computes the sum of all elements; but, by a simple redefinitio
n of the
combining operation, you could also compute the product of the elements. Similarly,
as a default
adjacent_difference() returns the difference between an element and its
predecessor. By
redefining the combining operation, the sum, product or any other conceivable
computation can
be calculated.
The adjacent_difference() and inner_product () operations can also be
used to con-
vert a sequence of absolute values into a sequence of relative values, and vice versa.
The accumulate() and inner_product () algorithms return a value as
a result and are, in
this respect, non-modifying. The two other algorithms process several values
that are written in
a target range and are, in this respect, modifying for the target range.
For more details on algorithms, refer to my book “The C++ Standard
Library—A Tutorial
and Reference’ (see [JosuttisStdLib]).
9.1 Additional Details of the Standard Library sje
The most important implementation details for the numeric fundamental types can be queried
using class templates numeric_limits<>. These numeric limits exist for the following types:
bool, char, signed char, unsigned char, wchar_t, short, unsigned short, int,
unsigned int, long, unsigned long, float, double and long double. They are defined
in the header file <1imits> and replace the preprocessor constants defined in the C header files
<climits>, <limits.h>,<cfloat> and <float.h>.
To define limits, we make use of the class template numeric_limits<>. This contains static
members that define different numeric properties of the types. Tables 9.16 and 9.17 list the
individual members (and the corresponding C constants if available).
The value range of types are, of course, particularly interesting. The following example
(which outputs the maxima for some types, with some additional information) shows a possible
evaluation of this information:
// etc/limits.cpp
#include <iostream>
#include <limits>
#include <string>
int main()
{
using namespace std;
is_signed(char): true
is_specialized(string): false
The last line of output tells us that there are no numeric limits defined for type std: : string.
This makes sense, because a string is not a numeric type. Despite this, as one can see, this
information can be queried for all types. This is also valid for user-defined types. This is made
possible by means of a programming technique used in connection with templates. A common
default implementation is defined for all types:
namespace std {
// general definition of the templates for numeric limits
template <typename T>
534 9: Other Language Features and Details
class numeric_limits {
public:
// generally, there are no numeric limits
static const bool is_specialized = false;
13
}
The following code shows a possible complete specialization of the numeric limits for
type
float. With this, the exact signatures of the members are shown:
namespace std {
template<> class numeric_limits<float> {
public:
Static const bool is_specialized = true;
9.1.5 Summary
In C++, it is possible to define the * and -> operators for objects. In this way, objects can be given
a pointer-like interface. As you implement these operators yourself, the result is the creation of
objects that behave like pointers, but that perform more complex, or safer, operations. For this
reason, the objects are also called smart pointers.
We have already come across one kind of smart pointer in connection with the STL. Itera-
tors are smart pointers, capable of iterating complex data structures using a simple pointer-like
interface (see Section 3.5.4 on page 74).
One kind of smart pointer is almost always helpful: pointers that manage objects created with
new, so that they are automatically deleted when the last pointer to the objects gets destroyed.
In this way, reference semantics can be implemented. This means that when copying objects,
only the references to them are copied, so that the data of the original and the copy is the
same.
In this way, you can manage objects that are in multiple STL containers at the same time.
The following class template defines a smart pointer using reference semantics:
// etc/countptr.hpp
#ifndef COUNTED_PTR_HPP
#define COUNTED_PTR_HPP
public:
// initialize with ordinary pointer
// - p has to be a value returned by new
explicit CountedPtr (T* p=0)
: ptr(p), count(new long(1)) {
// copy constructor
CountedPtr(const CountedPtr<T>& p) throw()
: ptr(p.ptr), count(p.count) { // copy object and counter
++x*count ; // increment number of references
// destructor
~“CountedPtr () throw() {
release() ; // release reference to the object
// assignment
CountedPtr<T>& operator= (const CountedPtr<T>& p) throw() {
if (this != &p) { // ifnot a reference to itself
release(); // release reference to old object
ptr = p-ptr; // copy new object
count = p.count; // copy counter
++*count ; // increment number of references
}
return *this;
private:
void release() {
538 9: Other Language Features and Details
.
ie
#endif /*COUNTED_PTR_HPPx/
You can now create any object using new and give control of it to this kind of pointer:
CountedPtr<CPPBook::Fraction> bp = new CPPBook: :Fraction(16,100) ;
bp can now be copied and assigned. By doing this, only other references to the object are created,
not new objects.
You will notice that the * operator, as well as the -> operator, is implemented. You can
therefore access the object using these two types of operator:
Std::cout << *bp << std::endl; .. double d = bp->toDouble();
If the -> operator is implemented for some classes, something has to be returned for which
the -> operator can be called again. If, as here, the -> operator is called for an object of type
CountedPtr<CPPBook: :Fraction>, this returns the type CPPBook: :Fraction*, for which
the -> operator is called again.
The following program shows how this class can be used to simultaneously manage objects
in multiple STL containers:
// etc/refsem1.cpp
#include <iostream>
#include <list>
#include <deque>
#include <algorithm>
#include "countptr.hpp"
using namespace std;
int main()
{
// type for smart pointer for this purpose
typedef CountedPtr<int> IntPtr;
9.2 Defining Special Operators Doo
// array of
initial ints
staticuwintevalues Ls=i{03535 05 ee 6, 4};
=8 6 Gi B ©
Os6#1 317 55-3
In the boost repository for supplementary C++ libraries, there are several smart-pointer classes
(see http://www. boost.org).
int main()
{
std: :set<int> @@slisle
std: :vector<int> coll2;
But what happens if you want to add different values? Provided that these values are established
at compile time, a template can be used (compare with Section 7.4 on page 448):
template <int W> int add (int elem)
{
return elem + W;
int main() {
}
However, it is not currently completely clear whether a template can simply be passed as an
operation in this way*. In this case, the exact type of the passed operation has to be specified:
transform(coll1.begin() ,colli.end(), // source
std: :back_inserter(coll2), // destination (inserting)
(int (*) (int) )add<10>); // operation
The expression
int (*) (int)
denotes a function that receives an int and returns an int. By means of
(int (*) (int) )add<10>
it is explicitly established that add<10> has this type.
But what happens if you find the value to be added at run time? Actually, this value is needed
as a second parameter, and transform() calls this operation with an argument, namely the
appropriate element. We therefore have to pass the value to add() in another way.
Of course, a global variable could be used, but this is fairly dangerous and pollutes the global
scope with a local value.
At this point, function objects (which are also called functors) are helpful. These are objects
that behave like functions, but, like objects, can manage additional data. The fact, that they
behave like functions is implemented via the call operator ‘()’ (see Section 4.2.6 on page 166):
// etc/add.hpp
// function object that adds the passed value
class Add {
private:
mn Wells // value to add
public:
// constructor (initializes the value to add)
Add(int w) : val(w) {
}
ig
At run time, we can create such an object, initialized with the value to add, and pass it as operation
to the transform() algorithm:
// etc/transform2.cpp
#include <set>
#include <vector>
#include <algorithm>
#include <iterator>
#include <iostream>
int main()
{
std: :set<int> COlmele
std::vector<int> coll2;
The expression
Add(*coll1.begin() )
creates a temporary object of the Add class and initializes it with the value of the first element
of
colli. With
transform(coll1.begin() ,colli.end(), //source
std: :back_inserter(coll2), // destination (inserting)
Add(*coll1.begin())); // operation
this temporary object is passed to transform() as an operation op. Within transform(),
the
passed operation is called:
template <typename InIter, typename OutIter, typename Operation>
transform (InIter begi, InIter endi,
OutIter beg2,
Operation op)
op (currentElement)
}
If op is a function, this function is called with the current element. If this
is a function object,
this call is evaluated as
op.operator() (currentElement)
Thus, for the object op you call operator ‘()’ with currentElement passed
as argument.
9.2 Defining Special Operators 545
9.2.3. Summary
e The operators * and -> can be defined for objects. By doing so, smart pointers can be imple-
mented.
e You can also define the () operator for objects. By doing so, function objects can be imple-
mented.
e Functions and function objects acn be used to define the specific behavior or criterion of an
algorithm.
546 9: Other Language Features and Details
The new, new[], delete and delete[] operators can be called so that they do not throw
exceptions, but instead return 0 or NULL if there is not enough memory.
To do so, new has to be passed the std: :nothrow value as a parameter:
CPPBook: :Fraction* bp = new(std: :nothrow) CPPBook: :Fraction(16,100) ;
if (bp == NULL) {
Std: cerme<<qa ail memory" << std::endl;
h;
This memory must be freed using delete and delete[]:
delete bp;
delete [] flist;
You can also separate the destruction and the deallocation of objects into two steps:
// clean-up object
bp->~CPPBook: :Fraction() ;
// free memory
::0perator delete((void*)
bp) ;
If a request for memory with new fails, under normal circumstances, an exception of type
std:bad_alloc is thrown.
You can intervene here. If new cannot reserve memory, a function that throws the appropriate
exceptions is called. This kind of function is called a new handler.
By using std: :set_new_handler(), a new handler can be installed. This function is de-
clared in the special C++ header file <new>.
The new handler is declared as a global function, is called without parameters and has no
return value. The following example shows this kind of new handler and its use:
// etc/newhdl1.cpp
/* myNewHandler ()
* - outputs error message and exits the program
af
void myNewHandler ()
4
// output error message on standard error channel
std::cerr << "out of meemmmoooorrrrrryyyyyyy..." << std::endl;
int main()
f
try ¢
548
Se
e 9: Other Language Features and Details
e BB ree CCE Crane
At the start of the program, the function myNewHandler() is installed as a new handler. by
passing it to std: :set_new_handler()?:
std: :set_new_handler (&myNewHandler) ;
As there is no difference in C++ (as in C) between a function and the address
of a function (the
name of a function is seen as its address), the & operator can be omitted:
std: :set_new_handler (myNewHandler) ;
Finally, an attempt is made to allocate memory in an endless loop:
fore my
new char[1000000];
}
Sooner or later, calling new therefore leads to the call of the installed
new handler. This is
reported with an appropriate error message, and throws an exception:
void myNewHandler() {
// output error message on standard error channel
std::cerr << "out of meemmmoooorrrrrryyyyyyy-.." << std::endl;
However, this first new handler needs to be rewritten. It may happen that an attempt is made, via
a new handler, to request new memory. It is possible that new may be called within the bounds of
the output operator <<, or by the exception object being created. This would result in a recursive
loop of new handler calls, and the program would eventually crash.
The std: :set_new_handler () function returns the previously installed new handler. This can
be used to install new handlers for a restricted control section. The returned new handler is then
reinstalled later.
// etc/newhdl2. cpp
#include <new>
‘onuel 26(OQ)
ae
std::new_handler oldNewHandler; // pointer to new handler
The std: :new_handler type is simply a function pointer (see Section 9.4.1 on page 557) for a
function without parameters and without a return value:
namespace std {
typedef void (*new_handler) ();
As a tule of thumb, with commercial programs, it is not enough to simply exit the program if
there are errors. Even if there is a lack of memory, clean-up work has to be carried out. However,
it is often the case that additional memory is needed for this work. Therefore, it may be useful
to allocated memory at the start of the program that is freed if the new handler is called. This
behavior could be implemented in a separate module:
550
serene Se eee eee srl 9:opted
Other Language
he aFeatures and Details
adhe aa,
// etc/newhdl3.cpp
// forward declaration
static void myNewHandler() ;
// reserved memory
static char* reserveMemory;
void initNewHandler()
{
// allocate memory as might be necessary
reserveMemory = new char[100000];
If there is a lack of memory, it is best if it is possible to let the program carry on running. To do
this, the new handler can be implemented so that the program is not exited, but tries to carry on
running after the call.
If a new handler is called and neither exits the program nor throws an exception, the programs
tries to allocate the memory again. The new operator is therefore predefined so that, if no memory
can be requested, either std: : bad_allocis thrown, or, if anew handler is installed, this is called
in a loop, again and again, until either the requested memory is available or the loop is exited via
an exception or a program abortion.
A program can therefore install a new handler that does not exit a program, but instead cleans
up and frees memory. In the same way, it is possible that the first call of the new handler frees re-
served memory and outputs a warning that the memory has been used up. The program, however,
runs further and is only then finally exited if the ‘reserve memory’ is consumed:
// etc/newhdl4. cpp
// forward declaration
static void myNewHandler() ;
// reserved memory
static char* reserveMemory1;
static char* reserveMemory2;
void initNewHandler()
{
// request reserved memory accoring to needs
reserveMemoryi = new char [1000000] ;
reserveMemory2 = new char[100000] ;
The predefined new and delete operators are declared in the language
as follows:
9.3. Additional Aspects of new and delete Spe)
new and delete can also be implemented individually within separate classes. In the following
example, new and new[ J are provided for a class MyClass, with an additional output statement:
// etc/mynew. cpp
#include <iostream>
#include <cstddef>
Again, note that the return type of new has to be void* (and not MyClass*), because no object
is created, but only memory is provided for it.
The scope operator : : inside the implementation operator new ensures that the global opera-
tor new is called. Provided that the implementation is not used to redefine the memory manage-
ment completely, it is always better to redirect it to the global operator. By doing so, it can be
ensured that if there is an error, the installed new handler is called, as is expected by the user.
To request memory, size always has to be used. As the new operator is inherited, it can also
be called for larger objects of derived classes. It is therefore an error to only provide memory for
an object of the appropriate class:
return ::new char[sizeof(MyClass)]; //ERROR
It is also incorrect to explicitly call the global new for an object of the class:
return ::new MyClass; // ERROR
Too little memory is created here, and, in addition, the constructor of the class is called
(which
should actually happen after the call of new).
Anyone who wants to implement the new operator just for objects of this class has
to call the
global new for derived classes. To do this, you can simply check as to whether the
size of the
requested memory corresponds to the size of the object of this class:
void* MyClass::operator new (size_t size) {
// call global new for derived classes
if (size != sizeof(MyClass)) {
return ::new char [size];
}
I
However, with derived classes without members, the test fails.
To implement the delete operator for any class so that it outputs additiona
l information, you
have to do the following:
9.3 Additional Aspects of new and delete 2b)
// etc/mydelete.cpp
#include <cstddef>
As the example shows, the delete operator contains the address of the memory to be freed as a
parameter. The return type always has to be void.
Note that, for both forms of new, the global new for arrays is called. For this reason, the
global delete for arrays also has to be called.
A second parameter of the type std: :size_t can optionally be declared. This contains the
size of the object to be freed:
void MyClass: :operator delete (void* p, std::size_t size) {
In C++, no operator is provided to explicitly reduce or enlarge memory. This functionality usu-
ally has to be realized with new and delete. To use the terminology of C, this means that a
realloc() has to be implemented using malloc ()and free().
If, however, it is guaranteed by a separate definition that new and delete are implemented
internally via malloc() and free(), realloc() can also be called for objects created with
new. However, in order to do this, a function such as renew() should be defined so that C
and C++ techniques are not confused. In addition, it should be remembered that this function
556
a ee re ere A ae ar 9:esOther
NeateLanguage
fhe aFeatures and inDetails
Apa Debt opine oe
AY
calls the new handler if an error occurs. (Recall that you can set the current new handler using
std: :set_new_handler() (see Section 9.3.3 on page 549).)
I particularly warn against calling realloc() directly for objects created with new. It nor-
mally works, as new is typically implemented using malloc (), but it is not portable and is very
dangerous, as new() can be defined differently, which means that the new handler cannot then
be used.
9.3.6 Summary
e By using std: :nothrow, it can be ensured that new does not throw an exception
if there is
not enough memory, but instead returns NULL.
e Using placement new, objects can be initialized in memory that has already been
reserved.
e Ifno memory can be requested with new, a new handler can be defined, which
is then called
automatically and which executes the appropriate treatment of errors.
e The new and delete operators can be overloaded.
e When overloading new, it has to be taken into account that, if there
is an error, an installed
new handler will be called.
e You can define separate versions of new with additional parameters.
9.4 Function Pointers and Member Pointers So7/
It is already possible in C to define pointers to functions. Error handlers, for example, are fre-
quently managed in this way.
Error handlers are functions that are installed anywhere, so that they can be called when errors
occur. To do so, an error handler is assigned to a function pointer. If an error then occurs, the
function to which the function pointer points is called.
An example of these kinds of error handlers are the new handlers introduced in Section 9.3.3
on page 547. A function pointer, as a global variable, points to the function that is called if there
is an error:
// define type for pointer to new handler
namespace std {
typedef void (*new_handler) ();
WW
This function pointer can be passed a new handler with the function set_new_handler() :
new_handler set_new_handler(new_handler newNewHandler)
“4
// store current new handler to restore later
new_handler oldNewHandler = myNewHandler;
According to the same pattern, functions for any situation can be installed. They are simply held
in a function pointer, and are called if and when the appropriate situation arises.
If member functions are to be used as parameters, pointers to member functions are required.
C++ introduced a pointer to members for this purpose. These always refer to a particular
class.
Access is possible via operators that were introduced solely for this purpose.
To declare a pointer to member, the scope operator has to be used in order to specify
the class for
which the pointer is valid:
// pointer to int members of the class MyClass
int MyClass::* intPtr;
Now the address of an integer member of the class can be assigned to the pointer:
intPtr = & MyClass: :xyz;
In order to do so, access must be granted to the member (e.g. with the public
keyword, or
placing the statement in the scope of a member or friend function).
Using the special C++ . * operator, the member can then be accessed:
MyClass obj;
9.4 Function Pointers and Member Pointers 559
If a pointer to an object of the class is used, the special C++ operator ->* can be used:
MyClass obj;
MyClass* op = &obj;
// for the object to which op points, access the member to which intPtr points
Cy erable = 1/5
Pointers to members are predominantly used for the parametrization of member functions. They
can be considered as function pointers for classes.
To declare a pointer to a member function, the scope operator has to be used in order to
specify the class for the function pointer:
void (MyClass: :*funcPtr) ();
In this case, funcPtr is declared as a pointer to a member function of class MyClass that is
called without parameters and has void as a return type.
It is recommended here that, as with function pointers, a separate type should be defined, so
that the different declarations are easier to maintain and more comprehensible:
typedef void (MyClass: :*MyClassFunction) () ;
MyClassFunction funcPtr;
After the assignment of a member, the C++ . * operator has to be used for access to the member:
MyClass obj;
// for object to which objPtr points, call member function to which funcPtr points
(objPtr->*funcPtr) () ;
A member function can be passed as a parameter in this way, and can be managed in a variable.
The following example demonstrates the use of pointers to member functions:
560
a 9: ee
Other Language Features and Details
ee
// etc/compptr1.cpp
#include <iostream>
class MyClass {
public:
void funci() {
std::cout << "call of funci()" << std::endl;
5
void func2() {
std::cout << "call of func2()" << std::endl;
ig
int main()
i
// pointer to member function of class MyClass
MyClassFunction funcPtr;
The funci() and func2() member functions are assigned to the funcPtr
member pointer one
after the other, and called via the same call statement. The output
of the program therefore reads
as follows:
9.4 Function Pointers and Member Pointers 561
Callimorerune i)
Call of func2()
A working example of the use of pointers to members are interfaces to other processes or lan-
guage environments in which member functions of objects are to be callable.
The following example shows how, in principle, this is possible:
// etc/compptr2.cpp
#include <iostream>
class MyClass {
public:
void funci() {
Std COU Gau call moter incl©) lE<qes tds end ils
by
void func2() {
std::cout << call of func2())' << std::endl:
};
int main()
In C++, the addresses of objects, as well as the addresses of the member pointers, can be exported
as void»:
void export0bjectAndFunction(void* op, void* fp);
MyClass x;
MyClassFunction funcPtr;
exportObjectAndFunction(kx, &funcPtr) ;
These addresses can also be used as pointers to members:
void callMyClassFunc(void* op, void* fp) {
// cast types back to original type
MyClass* myObjPtr = static_cast<MyClass*>(op) ;
MyClassFunction* myFuncPtr
= static_cast<MyClassFunction*>(fp) ;
Note that the address of a variable that refers to the member function gets exported. Thus you
have to make sure that this variable remains valid. It is not possible to export the address of a
member function.
9.4.4 Summary
i;
It does not matter whether this function is implemented in a C or a C++ module. Therefore, it
can be an imported C function or an exported C++ function.
By using extern "C", it is ensured that the function name foo remains unchanged as a
symbol. Normally, internal symbols for functions consist of information that contains the
types
of the parameters, as well as the function names. Because of this, it is ensured that a distinction
can be made between overloaded functions. This process is described as ‘name mangling’.
With the GCC compiler, for example, the declarations
void foo();
void foo(long, double);
void foo(const CPPBook: :Fraction&) ;
are managed internally as the following symbols:
foo__Fv
foo__Fld
foo__FRCQ27CPPBook8fraction
The generated symbols are not standardized. For this reason (and for other reasons), it is not
usually possible to combine the code of different C++ compilers.
As functions cannot be overloaded in C, name mangling cannot be used
there. By using
extern "C" it is switched offin C++ files.
By means of an appropriate external statement, linkage to other language
s is possible. How-
ever, this is not guaranteed by the language of C++, but instead depends
on the compilers and
linkers that are used. As it is usually possible to combine code of a
language with C code, this
code can always be combined with C++ by using extern "C",
9.5 Combining C++ Code with C Code 3168)
It is possible to provide header files for use in C, as well as for use in C++. This is supported by
the preprocessor constant __cplusp1us, which is only defined for C++>. For example, a header
file might look as follows:
#ifdef __cplusplus
extern: "C" e+
#endif
void foo(long, double);
#ifdef __cplusplus
}
#endif
}
is only taken into account during a compilation with the constant _.cplusp1lus defined (i.e. a
C++ compilation). Otherwise, the functions are declared, as in C or ANSI-C, with parameter
prototypes. The standard header files of C are usually implemented in this way for C++.
9.5.4 Summary
5 In order to avoid name conflicts, constants predefined by the system typically begin with two underscores.
566 9: Other Language Features and Details
9.6.1 Unions
A union is a data structure in which all members have the same address. Because of this, unions
only occupy as much memory as their largest element. Therefore, at any one time, unions can
only even contain one of the possible values.
The following unions can, for example, alternatively contain the address of a character or an
integral value:
union Value {
const char* addr;
int num;
};
The language does not manage what kind of value is in a union. If an address is written to a
union, it has to be ensured by the programmer that an address is also requested:
Value value;
struct Entry {
ValueKind kind;
Value value;
ip
9.6 Additional Keywords 567
Entry e;
Liao DRL
e.kind = Address; // assign address to union value
e.value.addr = "hello";
} else {
e.kind = Number; // assign number to union value
e.value.num = 42;
switch (e.kind) {
case Address:
t = e.value.addr; // query address from union value
break;
case Number:
i = e.value.num; // query number from union value
break;
}
Note that enumeration types and values are valid from the point of their definition to the end
of the scope in which they are defined. Thus, to avoid name clashes, you should ensure that
enumeration types are not defined in the global naming scope.
Integers can also be assigned to the values of enumeration types:
enum Season { Spring=1, Summer=2, Autumn=4, Winter=8 iP
Without this explicit assignment, the values start would start from zero.
Enumeration types can take on all values between zero and the greatest integer number
rounded up to the power of two. If an enumeration value has a negative value, the same ap-
plies (as many bits that are needed in the value range are always used).
Integers can be explicitly converted into enumeration values:
enum Range { min = 0, max = 100 } // range:0 to 127
Range r;
ie =! Senne //OK
r = max; // OK
r= Range(17); // between min and max
r = Range(120) ; // OK, same number ofbits as 100
r = Range(200) ; // ERROR (undefined behavior)
420.
Another example of the use of enumeration lists can be found in Section 6.5.2 on page
568
a ara t9: Other Language
hatictinidciersstes tie gh Features and metas
"enakes bienGelet Details
Y
Without the volatile keyword, the compiler would assume that the value of value could not
be changed after the first reading, and therefore could optimize further read attempts by just
removiong them. By using volatile, it is guaranteed that the value of value is always reread.
9.6.4 Summary
e Unions are objects that can manage data of different types with the same memory.
e Unions are defined using the union keyword.
e Enumeration types and values can be defined using enum.
e By using the volatile keyword, areas that are not under the control of the compiler
can be
protected from aggressive optimizations.
10
Summary
Because, for teaching purposes, the language features of C++ have been introduced in stages,
the most important features are summarized here. These include useful programming and design
guidelines, as well as a comprehensive list of the operators and their properties.
e During the design of class hierarchies, the following must be taken into account (see Section
6.3):
— Objects of derived classes should never be subject to any limitations as compared to ob-
jects of the base class (as far as either their attributes or their operations are concerned).
- Any newly added members must not change the meaning of the inherited data members.
e Avoid inheritance (see Section 5.5.5 on page 348).
e The ‘rule of three’: A class either needs an assignment operator, a copy constructor, and a
destructor, or none ofthese. If one of the three operations is implemented, it must be checked
whether the others need to be implemented as well (see Section 6.1).
e A user-defined assignment operator should
— begin with a query as to whether an object is being assigned to itself;
— return the object to which another object was assigned as a (constant) reference
(see Section 6.1.5 on page 362)
e With template code, declarations and implementations should be divided into separate header
files (see Section 7.6.1 on page 466).
e Instead of dynamic arrays, STL containers (such as vectors) should be used (see Section 9.1.1
on page 522).
e Names of types and classes should start with uppercase letters.
e Function names, variables, and objects should start with lowercase letters.
e All preprocessor constants should consist entirely of uppercase letters.
e Commercial classes, libraries, and members should always be defined in a separate names-
pace.
e Programmers should always know what they are doing.
aa ; a oe
= _ reen a
PrN ee 7 : ;
ee ee
aS Fett tee a is a ;
ay heel oat
wm, ee =e ap tings
Pe BS ts r;
é
7 : = ' ' 7 7 7 a ‘ ] a
This appendix gives the resources and literature that is referenced in this book and that, in my
opinion, is important for a more in-depth introduction to C++. This is not, however, a complete
bibliography for C++. An updated version of this bibliography can be found at:
http://www. josuttis.com/cppbook.
Newsgroups
The following newsgroups discuss C++, the standard, and the C++ standard library:
e Tutorial ievel C and C++ (unmoderated)
alt.comp.lang.learn.c-c++
e General aspects of C++ (unmoderated)
comp. lang.ct++
e General aspects of C++ (moderated)
comp. lang.c++.moderated
e Aspects of the C++ Standard (moderated)
comp.std.e++
The Standard
[Standard98 |
ISO
Information Technology—Programming Languages—C + +
Document Number ISO/IEC 14882-1998
Available at http: //www.ansi.org
ISO/IEC, 1998
578 Bibliography
[Standard02]|
ISO
Information Technology—Programming Languages—C ++
(as amended by the first technical corrigendum)
Document Number ISO/IEC 14882-2002
ISO/IEC, expected late 2002
[CargillExceptionSafety]|
Tom Cargill
Exception Handling: A False Sense of Security
C++ Report, November-December 1994
Available at:
http: //www.awprofessional.com/meyers cddemo/demo/magazine/ index.htm
[CzarneckiEiseneckerGenProg]
Krzysztof Czarnecki, Ulrich W. Eisenecker
Generative Programming
Methods, Tools, and Applications
Addison-Wesley, Reading, MA, 2000
[KoenigMooAcc}]
Andrew Koenig, Barbara E. Moo
Accelerated C++
Practical Programming by Example
Addison-Wesley, Reading, MA, 2000
[MeyersEffective]
Scott Meyers
Effective C++
50 Specific Ways to Improve Your Programs and Design (2nd Edition)
Addison-Wesley, Reading, MA, 1998
Bibliography Se
[MeyersMoreEffective]|
Scott Meyers
More Effective C++
35 New Ways to Improve Your Programs and Designs
Addison-Wesley, Reading, MA, 1996
[StroustrupC++PL}]
Bjarne Stroustrup
The C++ Programming Language, Special ed.
Addison-Wesley, Reading, MA, 2000
[SutterExceptional|
Herb Sutter
Exceptional C++
47 Engineering Puzzles, Programming Problems, and Solutions
Addison-Wesley, Reading, MA, 2000
[SutterMoreExceptional]
Herb Sutter
More Exceptional C++
40 New Engineering Puzzles, Programming Problems, and Solutions
Addison-Wesley, Reading, MA, 2001
[VandevoordeJosuttisTemplates|
David Vandevoorde, Nicolai M. Josuttis
C++ Templates - The Complete Guide
Addison-Wesley, Reading, MA, 2002
[JosuttisStdLib|
Nicolai M. Josuttis
The C++ Standard Library
A Tutorial and Reference
Addison-Wesley, Reading, MA, 1999
580
tale ree nee PO sees a Bibliography
ee es ee ee
Y
[LangerKreftIO]
Angelika Langer, Klaus Kreft
Standard C++ IOStreams and Locales
Advanced Programmer’s Guide and Reference
Addison-Wesley, Reading, MA, 1999
[StroustrupDnE]|
Bjarne Stroustrup
The Design and Evolution of C++
Addison-Wesley, Reading, MA, 1994
Glossary
This glossary is a compilation of the most important technical terms used in this book. See
http: //www.research.att.com/~bs/glossary.htm1 fora very complete, general glossary
of terms used by C++ programmers.
abstract class
Class for which the creation of concrete objects (instances) is impossible or not useful. Abstract
classes can be used to combine common properties of different classes in a single type or to
define a polymorphic interface. Because abstract classes are used as base classes, the acronym
ABC is sometimes used for abstract base class.
ANSI
American National Standard Institute, one national committee that works on the standardization
of C++.
attribute
Property or data member of a concrete object of a class. Every concrete object of a class uses
memory for its attributes. Their values reflect the status of the object.
base class
A class, from which another class inherits properties. Base classes are often abstract classes.
Base classes are often called superclasses in other programming communities.
cast
Name for a explicit type conversion that also exists in C.
582 Glossary
class
Description of a category of objects. The class defines a set of characteristics for any object.
These include its data (attributes, data members) as well as its operations (methods, member
functions). In C++, classes are structures with members that can also be functions and are subject
to access limitations. They are declared using the keywords class (or struct).
class template
A template of a class that is parametrized for different types or values. Actual classes can be
generated by substituting the template parameters by specific entities. Class templates are also
described conceptionally as ‘parametrizable classes.’
collection class
Class that is used to manage a group of objects. In C++, collection classes are also called con-
tainers.
concrete class
A class, from which concrete objects (instances) can be created (as opposed to an abstract class).
constructor
Member function of a class that is called during the creation of an object of the class and
is used
to bring the newly created object into a useful initial state.
container
See collection class.
conversion function
Special member function that defines how an object of a class can be converted to
an object of
another type. It is declared using the form operator type().
copy constructor
Function that is called if a new object is created as a copy of an existing one.
This happens in
particular with the passing of parameters and return values if no references are
used. In C++ a
default copy constructor is predefined for every Class that copies memberwise.
It can be replaced
by a user-defined implementation.
data member
Attribute of an object or a member that is not a member function. Every
concrete object of a
class uses memory for its data members with values that reflect the status
of the object.
declaration
A C++ construct that introduces or reintroduces a name into a C++ scope.
See also definition.
Glossary 583
default constructor
Constructor that is called when a new object is created without any arguments for initialization.
definition
A declaration that makes the details of the declared entity known or, in the case of variables, that
forces storage space to be reserved for the declared entity.
derived class
A class that inherits the properties of another class (called the base class). Semantically this is
often a ‘concretion’ of the base class. Base classes are often called subclasses in other program-
ming communities.
destructor
Function that is called for an object of a class if it is destroyed.
dot-C file
A file in which definitions of variables and noninline functions are located. Most of the exe-
cutable (as opposed to declarative) code of a program is normally placed in dot-C files. They are
the files that are used a translation units to get compiled (and linked) to object files, libraries, or
executable programs. Usually, they include header files, in which usable constants, functions,
classes, etc., are declared. They are named dot-C files because they are usually named with a
. cpp suffix (although, other suffixes such as .C, .c, .cc, or .cxx are also used).
encapsulation
Design concept which limits the access to things or objects. By means of this restriction, an
object can only be manipulated using a well defined interface. By doing so, interfaces can be
verified better and inconsistencies can be avoided. C++ supports the encapsulation by means of
special access keywords.
function template
A template of a function that is parametrized for different types or values. Actual functions can
be generated by substituting the template parameters by specific entities. Function templates are
also described conceptionally as ‘parametrizable functions.’
header file
A file that contains declarations of variables and functions that are referred to from more than one
translation unit, as well as definitions of types, inline functions, templates, constants, and macros.
Header files are meant to become part of a translation unit through a #include directive. They
are usually named with a . hpp suffix (although, other suffixes such as .h, .H, . hh, or .hxx are
also used). They are also called include files.
include file
See header file.
584 Glossary
inheritance
Reduction of a type or a class (derived class) to another type or class (base class) by defining only
the properties that are added (or modified). Inheritance makes it possible for common properties
of different kinds of objects to be combined and to be implemented only once. By doing so, the
base class is supporting abstraction. The base class is the generic class that defines the common
properties that can be used to derive many more concrete variants.
initializer list
A comma-separated list of expressions enclosed in braces used to initialize objects (or arrays). In
constructors, the values that are entered can be used for the initialization of members of a class
or for the call of the constructor of a base class.
instance
The term instance has two meanings in C++ programming: The meaning that is taken from the
object-oriented terminology is an instance of a class: An object that is the realization of a class.
For example, in C++, std: : cout is an instance of the class std: :ostream. The other meaning
is a template instance: A class, a function, or a member function obtained by substituting all the
template parameters by specific values. In this sense, an instance is also called a specialization.
ISO
International Organization for Standardization, the international committee that
works on the
standardization of C++.
iterator
An object that knows how to traverse a sequence of elements. Often, these elements
belong to a
collection (see collection class).
member
Element of a class. A member can be a data member (attribute) that has an object of
the class and
reflects the status of the object, or a member function (method) that describes which
operations
can be called for an object of the class. A special case is that of static members
that do not
describe properties of individual objects but properties of the whole class (data
that is shared by
all objects).
member function
Function that can be called for an object of a class. It is declared as
a member in the class
structure. It can also be called a method, which is the corresponding
general object-oriented
term.
message
Object-oriented name for the call of an operation for an object. The call
is seen as a message that
is sent to the object and leads to a reaction. In C++, a message is a call
of a member function.
method
Object-oriented name for the implementation of an operation that
can be called for an object of
a class. In C++, a method is called a member function.
Glossary 585
multiple inheritance
The capability that a class can be derived from more than one base class.
object
Central term of object-oriented programming. An object is an information carrier that represents
different data that has a certain status (value) and provides an interface to perform operations.
An object can therefore represent abstractions of any kind, such as a process or a football game,
for example. An object is simply ‘something’ that is of interest, is described, and plays a part in
a program. The properties of objects (the data that they represent and the operations with which
they can be executed) are described in classes.
pointer
Object or variable that contains a program address and therefore points to something else in the
program. A pointer is, for example, the address of a function or object to which indirect access
can then be made.
polymorphism
The ability for the same operation to be called for different kinds of objects that, according to the
object, leads to different reactions. In object-oriented terminology, this means that different kinds
of objects can receive the same message, but are interpreted differently because the classes have
different methods for the message. In C++, there is the ability for different objects to be managed
under a common class/type so that the same function call may result in different operations de-
pending on the actual class/type of the object/value. The common class/type can be implemented
as a base class (‘dynamic polymorphism’) or as a class template (‘static polymorphism’).
reference
A ‘second name,’ that can be given to an existing object. References are used in particular when
parameters and return values are passed in order not to produce copies (‘call-by-reference’).
In addition, ‘reference’ is also used if something ‘refers’ to something else (which might be a
reference in the sense of a ‘second name’ or a pointer).
source file
Header file or dot-C file.
specialization
The process or result of substituting template parameters by actual values so that actual classes
or functions are generated.
structure
Composition of different members into one common, complete object. Structures offer the option
of abstraction (for example, an engine, the body work, four wheels, and so on being combined
and managed as a Car).
subclass
See derived class.
586 Glossary
superclass
See base class.
template
A template of a function (function template) or a class (class template) that is parametrized for
different types or values.
translation unit
The code that is compiled as the result of preprocessing a dot-C file. That is, it is a dot-C file
with all the header files and standard library headers it #includes, without the program text that
is excluded by conditional compilation directives such as #if or #ifdef.
whitespace
The common name for the standard spaces under C, C++ and UNIX. These characters are new-
lines, blank spaces, and tabulator characters.
Index
Note: Bold page numbers indicate major topics; italic page numbers indicate examples.
half open 75 I
handle 369
has_denorm 532 i18n 494
has_denorm_loss 532 identity 339
has_infinity 532 testing 363
has_quiet_NaN 532 IDs 414
Index I 57
if 42 protected 295
#if 52 return type 285
sag single 256, 258
condition 202 terminology 255
#Hifdef 51 UML notation 256
#ifndef 47, 52, 130 versus composition 344
ifstream 498 with exception classes 246
ignore () inhomogeneous collection 314
for streams 479, 483 initialization
imbue() 495 and inheritance 266
for streams 494 implicit vs. explicit 220
implementation of constants 187, 412
of classes 14, 136 of references 412
of constructors 138 of static members 417
of manipulators 483 via constructor 266, 358, 408
of member functions 141 initializer list 139, 266, 312, 398, 408, 417, G584
ofnew 551 for base constructor 266
of operator functions 152 formembers 408
implementation inheritance 349 initializing
implicit type conversions of objects 133
see automatic type conversions inline
in 501 and templates 431
#include 27,51, 137 and virtual 283
include file G583 inline 173
see header file inline function 173, 268
includes() 530 virtual 283
increment operator 39, 164 inner_product() 530
for iterators 74 inplace_merge() 530
index of the book 587 input 195, 472
index operator 164, 373 field width 487
overloading 373 hexadecimal 489
infinity() 532 numeric base 489
inheritance 15,255, G584 octal 489
access to inherited members 264 of addresses 477
and destructors 267 of bool 492
as concretion 345 of strings 366, 487
avoiding 348 operator >> 200, 477
constructor 265 standard functions 478
correct 287 input/output
design pitfalls 344 see I/O
dynamic members 393 insert()
initialization 266 for containers 79, &2
levels 256 for vectors 523
Liskov substitution principle 349 inserter 88
multiple 256, 329 inserting elements 523
of friend functions 399 insert iterator 88
overriding 270 instance 10, 60, G584
private 295 instantiate 428
598 IndexI
O += 157
<V375571
object 60, G585 << 40, 198, 204, 477, 570
address 339, 363 <= 37,571
arrays 145 = 37, 38, 146, 163, 362, 383, 420, 571
as object of the base class 272, 339 == 37, 363, 571
delete 115 > 27h Sh
destruction 144, 241, 286 SL SYA
global 145 >> 40, 200, 204, 477, 570
IDs 414 | 40, 211, 484, 502,571
number of existing objects 414 I Si, SA
Static 145 55, 138, 334, 417, 554, 570
terminology 20 automatic type conversion 217
object as member 406 const_cast 68,570
object file 49 delete[] 116,570
objects 9 delete 115,570
octal input/output 488 dynamic_cast 322,570
oct 488, 489, 510 for classes 149
ofstream 498 for constants and variables 375
op= 38, 157, 571 newl[] 116,570
open() 502 new 115,570
for streams 502, 503 op= 38, 157,571
operation overloading 151
abstract 303, 308 overview 569
Operator 36, 569 reinterpret_cast 380,570
/ 37, 570 sizeof 40,570
-- 39, 164, 570 static_cast 380, 399,570
Index P 603
ABN EOOIVAESS
AOE
CPSIA information can be obtained at www.ICGtesting.com
Printed in the USA
BVOW061116291112
306823BV00003B/16/A
= au
C++/Object Technologies
eeeeeee eeeeeoseaoee
ee eee eeeeee6 eeeneesd Sse@eeseeoeeC?eee eee eeeoeeeeee eee s
Ih
ISBN 978-0-470-84399-4
@®WIL wiley.com