C Fundamentals Running Language Supports
C Fundamentals Running Language Supports
Preface i
Getting Started 1
Introduction ..................................................................................................... 2
The C++ Compilation Model .......................................................................... 2
Difference Between Header and Source Files ................................................... 4
Compiling a File into an Object File .................................................................... 7
Linking Object Files ............................................................................................... 8
Working with the main Function ........................................................................ 9
Exercise 1: Compiling and Executing the main Function ............................... 10
Built-in Data Types ........................................................................................ 11
Primitive Data Types .......................................................................................... 11
Datatype Modifiers ............................................................................................. 12
Variable Definition .............................................................................................. 13
Demystifying Variable Initialization ................................................................. 14
Pointers and References .............................................................................. 15
Pointers ................................................................................................................ 15
References ........................................................................................................... 17
The const Qualifier ............................................................................................. 18
The Scope of Variables ....................................................................................... 22
Control Flow Statements ............................................................................. 24
Selection Statement – if-else ............................................................................. 25
Selection Statement – switch ............................................................................ 28
Iteration Statement – for loop ........................................................................... 29
Iteration Statement – while loop ...................................................................... 32
Iteration Statement – do-while loop ................................................................. 32
Jump Statements – break and continue .......................................................... 32
The try-catch block ....................................................................................... 33
Exercise 2: Counting the Number of Times a Specific Number
Appears in a Given List ....................................................................................... 35
Activity 1: Finding the Factors of 7 between 1 and 100 Using a
while Loop ............................................................................................................ 36
Arrays ............................................................................................................. 36
Array Declaration ................................................................................................ 36
Array Initialization .............................................................................................. 37
Accessing the Values of an Array ...................................................................... 37
Multidimensional Arrays .................................................................................... 38
Activity 2: Defining a Bi-Dimensional Array and Initializing
Its Elements ......................................................................................................... 40
Summary ........................................................................................................ 40
Functions 43
Introduction ................................................................................................... 44
Function Declaration and Definition .......................................................... 45
Defining a Function ............................................................................................. 47
Exercise 3: Calling a Function from main() ...................................................... 49
Local and Global Variables .......................................................................... 50
Working with Variable Objects .......................................................................... 52
Exercise 4: Using Local and Global Variables in a Fibonacci Sequence ........ 55
Passing Arguments and Returning Values ................................................. 57
Pass by Value ....................................................................................................... 59
Exercise 5: Calculating Age using Pass by Value Arguments ......................... 59
Pass by Reference ............................................................................................... 61
Exercise 6: Calculating Incrementation of Age using Pass
by Reference ........................................................................................................ 62
Activity 3: Checking Voting Eligibility ................................................................ 63
Working with const References or r-value References ............................ 63
Returning Values from Functions ..................................................................... 65
Returning by Value ............................................................................................. 67
Returning by Reference ..................................................................................... 68
Activity 4: Using Pass by Reference and Pass by Value .................................. 72
Const Parameters and Default Arguments ............................................... 72
Passing by const Value ....................................................................................... 72
Passing by const Reference ............................................................................... 73
Returning by const Value ................................................................................... 74
Returning by const Reference ........................................................................... 74
Default Arguments ....................................................................................... 75
Namespaces .................................................................................................. 76
Activity 5: Organizing Functions in Namespaces ............................................ 78
Function Overloading ................................................................................... 79
Activity 6: Writing a Math Library for a 3D Game ........................................... 80
Summary ........................................................................................................ 81
Classes 83
Introduction ................................................................................................... 84
Declaring and Defining a Class .................................................................... 84
The Advantages of Using Classes ...................................................................... 84
C++ Data Members and Access Specifiers ....................................................... 86
Static Members ................................................................................................... 90
Exercise 7: Working with Static Variables ........................................................ 92
Member Functions ........................................................................................ 93
Declaring a Member Function ........................................................................... 94
Using const Member Functions ........................................................................ 94
The this Keyword ................................................................................................ 96
Exercise 8: Creating a Program Using the this Keyword to
Greet New Users ................................................................................................. 97
Non-Member Class-Related Functions ............................................................. 98
Activity 7: Information Hiding Through Getters and Setters ......................... 98
Constructors and Destructors ..................................................................... 99
Constructors ..................................................................................................... 100
Overloading Constructor ................................................................................ 102
Constructor Member Initialization ................................................................ 104
Aggregate Classes Initialization ..................................................................... 105
Destructors ....................................................................................................... 106
Exercise 9: Creating a Simple Coordinate Program to
Demonstrate the Use of Constructors and Destructors ............................. 107
Default Constructor and Destructor ............................................................. 108
Activity 8: Representing Positions in a 2D Map ............................................ 108
Resource Acquisition Is Initialization ....................................................... 109
Activity 9: Storing Multiple Coordinates of Different
Positions on a Map .......................................................................................... 111
Nested Class Declarations ......................................................................... 111
Friend Specifier ........................................................................................... 113
Friend Functions .............................................................................................. 113
Friend Classes ................................................................................................... 113
Exercise 10: Creating a Program to Print the User's Height ....................... 115
Activity 10: The AppleTree Class, which Creates an
Apple Instance .................................................................................................. 116
Copy Constructors and Assignment Operators ...................................... 117
The copy Assignment Operator ..................................................................... 119
The move-constructor and move-assignment Operator ............................ 121
Preventing Implicit Constructors and Assignment Operators ................... 123
Operator Overloading ................................................................................ 124
Activity 11: Ordering Point Objects ................................................................ 127
Introducing Functors .................................................................................. 128
Activity 12: Implementing Functors ............................................................... 129
Summary ...................................................................................................... 130
Introduction ................................................................................................. 134
Templates .................................................................................................... 135
Compiling the Template Code ........................................................................ 136
Exercise 11: Finding the Bank Account of the User with the
Highest Balance ............................................................................................... 137
Using Template Type Parameters .................................................................. 139
Requirements of Template Parameter Types .............................................. 140
Defining Function and Class Templates ................................................... 144
Function Template ........................................................................................... 144
Class Templates ............................................................................................... 146
Dependent Types ............................................................................................. 150
Activity 13: Reading Objects from a Connection .......................................... 151
Activity 14: Creating a User Account to Support
Multiple Currencies ......................................................................................... 153
Non-Type Template Parameters ............................................................... 154
Activity 15: Writing a Matrix Class for Mathematical
Operations in a Game ..................................................................................... 156
Making Templates Easier to Use ............................................................... 158
Default Template Arguments ......................................................................... 158
Template Argument Deduction ..................................................................... 160
Parameter and Argument Types .................................................................... 160
Activity 16: Making the Matrix Class Easier to Use ...................................... 162
Being Generic in Templates ....................................................................... 163
Activity 17: Ensuring Users are Logged in When Performing
Actions on the Account ................................................................................... 168
Variadic Templates ..................................................................................... 170
Activity 18: Safely Performing Operations on the User Cart
with an Arbitrary Number of Parameters .................................................... 174
Writing Easy-to-Read Templates ............................................................... 175
Type Alias .......................................................................................................... 175
Template Type Alias ........................................................................................ 179
Summary ...................................................................................................... 179
Introduction ................................................................................................. 182
Sequence Containers .................................................................................. 182
Array .................................................................................................................. 183
Vector ................................................................................................................ 184
Deque ................................................................................................................ 188
List ..................................................................................................................... 188
Forward-List ...................................................................................................... 190
Providing Initial Values to Sequence Containers ......................................... 190
Activity 19: Storing User Accounts ................................................................. 191
Associative Containers ............................................................................... 192
Set and Multiset ............................................................................................... 192
Map and Multimap .......................................................................................... 194
Activity 20: Retrieving a User's Balance from their Given Username ....... 196
Unordered Containers ............................................................................... 197
Container Adaptors .................................................................................... 199
Stack .................................................................................................................. 200
Queue ................................................................................................................ 200
Priority Queue .................................................................................................. 201
Activity 21: Processing User Registration in Order ...................................... 201
Unconventional Containers ....................................................................... 202
Strings ............................................................................................................... 202
Exercise 12: Demonstrating Working Mechanism of the
c_str() Function ............................................................................................... 203
Pairs and Tuples ............................................................................................... 206
std::optional ................................................................................................ 207
std::variant ................................................................................................... 210
Exercise 13: Using Variant in the Program ................................................... 211
Exercise 14: Visitor Variant ............................................................................. 212
Activity 22: Airport System Management ..................................................... 214
Iterators ....................................................................................................... 215
Exercise 15: Exploring Iterator ....................................................................... 218
Reverse Iterators ............................................................................................. 219
Exercise 16: Exploring Functions of Reverse Iterator .................................. 220
Insert Iterators ................................................................................................. 220
Stream Iterators ............................................................................................... 221
Exercise 17: Stream Iterator ........................................................................... 221
Iterator Invalidation ........................................................................................ 222
Exercise 18: Printing All of the Customers' Balances .................................. 223
Algorithms Provided by the C++ Standard Template Library ................ 224
Lambda ............................................................................................................. 225
Read-Only Algorithms ..................................................................................... 227
Modifying Algorithms ...................................................................................... 229
Mutating Algorithms ....................................................................................... 231
Sorting Algorithms ........................................................................................... 232
Binary Search Algorithms ............................................................................... 233
Numeric Algorithms ........................................................................................ 233
Exercise 19: Customer Analytics .................................................................... 234
Summary ...................................................................................................... 236
Introduction ................................................................................................. 240
Inheritance .................................................................................................. 240
Exercise 20: Creating a Program to Illustrate Inheritance in C++ .............. 242
Exercise 21: Using Multiple Inheritance to Create a "Welcome to
the Community" Message Application .......................................................... 248
Activity 23: Creating Game Characters ......................................................... 249
Polymorphism ............................................................................................. 250
Dynamic Binding .............................................................................................. 251
Virtual Methods .......................................................................................... 254
Exercise 22: Exploring the Virtual Method .................................................... 254
Activity 24: Calculating Employee Salaries ................................................... 258
Interfaces in C++ ......................................................................................... 260
Activity 25: Retrieving User Information ....................................................... 264
Dynamic Memory ........................................................................................ 266
Safe and Easy Dynamic Memory ............................................................... 270
A Single Owner Using std::unique_ptr ........................................................... 272
Shared Ownership Using std::shared_ptr ..................................................... 275
Activity 26: Creating a Factory for UserProfileStorage ................................ 276
Activity 27: Using a Database Connection for Multiple Operations .......... 277
Summary ...................................................................................................... 278
Appendix 281
Index 317
Preface
>
About
This section briefly introduces the authors, the coverage of this book, the technical skills you'll
need to get started, and the hardware and software requirements required to complete all of
the included activities and exercises.
ii | Preface
Objectives
• C++ compilation model
• Apply best practices for writing functions and classes
• Write safe, generic, and efficient code with templates
• Explore the containers that the C++ standard offers
• Discover the new features introduced with C++11, C++14, and C++17
• Get to grips with the core language features of C++
• Solve complex problems using object-oriented programming in C++
Audience
If you're a developer looking to learn a new powerful language, or are familiar with C++
but want to update your knowledge with the modern paradigms of C++11, C++14, and
C++17, this book is for you. To easily understand the concepts in the book, you must be
familiar with the basics of programming.
Approach
C++ Fundamentals perfectly balances theory and exercises. Each module is designed to
build on the previous module. The book contains multiple activities that use real-life
business scenarios for you to practice and apply your new skills in a highly relevant
context.
Software Requirements
You'll also need the following software installed in advance:
• OS: Any desktop Linux version or macOS, or Windows 7, 8.1, or 10
• For Windows 10 systems: Windows subsystem for Linux (this is only available in
the latest versions)
• Browser: Use one of the latest browsers, such as Firefox, Chrome, Safari, Edge, or
IE11
• Modern C++ compiler
Additional Resources
The code bundle for this book is also hosted on GitHub at https://github.com/
TrainingByPackt/Cpp-Fundamentals.
We also have other code bundles from our rich catalog of books and videos available at
https://github.com/PacktPublishing/. Check them out!
Conventions
Code words in text, database table names, folder names, filenames, file extensions,
pathnames, dummy URLs, user input, and Twitter handles are shown as follows: "Create
a file named HelloUniverse.cpp and save it."
A block of code is set as follows:
#include <iostream>
int main() {
std::cout << "Hello Universe" << std::endl;
return 0;
}
New terms and important words are shown in bold. Words that you see on the screen,
for example, in menus or dialog boxes, appear in the text like this: "Select System info
from the Administration panel."
Getting Started
1
Lesson Objectives
In this chapter, you will learn about the usage of variables and control flow statements to create
more robust programs.
2 | Getting Started
Introduction
C++ has been a major player in the software development industry for more than 30
years, supporting some of the most successful companies in the world.
In recent years, interest in the language has been growing more than ever, and it is an
extremely popular choice for large-scale systems, with many big companies sponsoring
its advancement.
C++ remains a complex language, which puts a lot of power in the hands of the
developer. However, this also comes with a lot of opportunities to make mistakes. It
is a unique language as it has the ability to enable programmers to write high-level
abstractions while retaining full control of hardware, performance, and maintainability.
Let's start with a simple C++ program to understand how compilation happens.
Create a file named HelloUniverse.cpp and save it on the Desktop after copy-pasting
the following code:
#include <iostream>
int main(){
// This is a single line comment
/* This is a multi-line
comment */
std::cout << "Hello Universe" << std::endl;
return 0;
}
The C++ Compilation Model | 3
Now, using the cd command on the Terminal, navigate to the location where our file is
saved and execute the following command if you are on UNIX:
> g++ -o HelloUniverse HelloUniverse.cpp
> ./HelloUniverse
If you are on a Windows system, a different compiler must be used. The command to
compile the code with the Visual Studio compiler is as follows:
> cl /EHsc HelloUniverse.cpp
> HelloUniverse.exe
This program, once executed, will print Hello Universe on the Terminal.
Let's demystify the C++ compilation process using the following diagram:
1. When the C++ preprocessor encounters the #include <file> directive, it replaces
it with the content of the file creating an expanded source code file.
2. Then, this expanded source code file is compiled into an assembly language for
the platform.
3. The assembler converts the file that's generated by the compiler into the object
code file.
4. This object code file is linked together with the object code files for any library
functions to produce an executable file.
Let's also assume that the content of the calculator.hpp header is as follows:
#include <logger.hpp>
// implementation of calculator
The C++ Compilation Model | 5
In the main.cpp file, we include both directives, as shown in the following code snippet:
#include <logger.hpp>
#include <calculator.hpp>
int main() {
// use both the logger and the calculator
}
As we can see, the logger has been added in the resulting file twice:
• It was added the first time because we included logger.hpp in the main.cpp file
• It was added the second time because we included calculator.hpp, which then
includes logger.hpp
Included files that are not directly specified in a #include directive in the file we are
compiling, but are instead included by some other included file, are called transitive
included files.
Often, including the same header file multiple times creates a problem with multiple
definitions, as we will see in Lesson 2, Functions, and the Lesson 03, Classes.
Including the same file multiple times is very likely because of the transitive included
files we explained before, and will often result in a compilation error. In C++, there is
a convention to prevent problems that originate from including a header file multiple
times: include guards.
An include guard is a specific pattern of instructing the preprocessor to ignore the
content of the header if it has been included before.
6 | Getting Started
It consists of writing all the header code inside the following structure:
#ifndef <unique_name>
#define <unique_name>
// all the header code should go here
#endif /* <unique_name> */
Here, <unique_name> is a name unique throughout the C++ project; it typically consists of
the header file name, such as LOGGER_HPP for the logger.hpp header.
The preceding code checks whether a special preprocessor variable, <unique_name>,
exists. If it does not exist, it defines it and it proceeds to read the content of the header.
If it exists, it will skip all the code until the #endif part.
Since initially the special variable does not exist, the first time the preprocessor
includes a header, it creates the variable and proceeds to read the file. The subsequent
times, the variable is already defined, so the preprocessor jumps to the #endif directive,
skipping all the content of the header file.
Compilation is a process that ensures that a program is syntactically correct, but it does
not perform any checks regarding its logical correctness. This means that a program
that compiles correctly might still produce undesired results:
Every C++ program needs to define a starting point, that is, the part of the code the
execution should start from. The convention is to have a uniquely named main function
in the source code, which will be the first thing to be executed. This function is called
by the operating system, so it needs to return a value that indicates the status of the
program; for this reason, it is also referred to as
the exit status code.
Let's see how we can compile a program.
Together with C, C++ is the language with the most supported hardware and platforms.
This means that there are many C++ compilers, produced by many different vendors.
Each compiler can accept parameters in a different way, and it's important to consult
the manual of the compiler you are using when developing in C++ to understand the
available options and their meaning.
We'll now see how to compile a program with two of the most common compilers: the
Microsoft Visual Studio compiler and GCC.
If myfile.cpp is including a header in the include directory, we would compile the file
with the following commands:
Figure 1.4: Compiling the CPP file with the include directory
We can compile several files in their respective object files, and then link them all
together to create the final application.
With MSVC, we will create an executable named main.exe, while with g++, the
executable will be named main.
For convenience, MSVC and GCC offer a way to compile several files into an executable,
without the need to create an object file for each file, and then link the files together.
Even in this case, if the files are including any user-defined header, you need to specify
the header location with the /I or -I flags.
The C++ Compilation Model | 9
To compile the main.cpp and mylib.cpp files together, which uses some headers from
the include folder, you can use the following commands:
The first line contains the definition of the function, constituted by the return type int,
the name of the main function, and the list of arguments, which in this case is an empty
list. Then, we have the body of the function, delimited by curly braces. Finally, the body
is composed of a single instruction that will return a successful status code.
Note
As opposed to C, in a C++ program, the return statement is optional. The compiler
automatically adds return 0 if you don't explicitly return a value.
10 | Getting Started
We will discuss these topics in more detail later; what is important to know is that this
is a valid C++ program that can be compiled and executed.
Note
Most C compilers can compile C or C++ by determining the language based on the
file extension.
//On Windows:
> cl /EHsc main.cpp
4. The compilation process will produce an executable file, which will be named
main.exe on a Windows system and main.out on a UNIX one.
Built-in Data Types | 11
The character types char and wchar_t hold numeric values corresponding to the
characters in the machine's character set.
12 | Getting Started
Datatype Modifiers
The numeric types offered by the C++ programming language fall into three categories:
• Signed
• Unsigned
• Floating point
The signed and unsigned types come with different sizes, which means each of them
can represent a smaller or larger range of values.
Integer types can be signed or unsigned, where signed types can be used to distinguish
between negative or positive numbers, while unsigned can only represent numbers
greater than or equal to zero.
The signed keyword is optional; the programmer only needs to specify it if the type
is unsigned. Thus, signed int and int are the same types, but they are different from
unsigned int, or just unsigned for brevity. Indeed, if it is not specified, an unsigned type
always defaults to int.
Integers, as previously mentioned, can come in different sizes:
• int
• short int
• long int
• long long int
The short int type, or just short, is guaranteed to be at least 16 bits according to the
standard. This means it can hold values in the range of -32768 to 32767. If it was also
unsigned, so unsigned short int or just unsigned int, this range would be 0 to 65535.
Note
The effective size in memory of types can change based on the platform for which
the code is compiled. C++ is present in many platforms, from supercomputers
in data centers to small embedded chips in industrial settings. To be able to
support all these different types of machines, the standard only sets the minimum
requirements on built-in types.
Built-in Data Types | 13
Variable Definition
A variable is named storage that refers to a location in memory that can be used to hold
a value. C++ is a strongly-typed language and it requires every variable to be declared
with its type before its first use.
The type of the variable is used by the compiler to determine the memory that needs to
be reserved and the way to interpret its value.
The following syntax is used to declare a new variable:
type variable_name;
Variable names in C++ can contain letters from the alphabet, both upper and lower case,
digits and underscores (_). While digits are allowed, they cannot be the first character
of a variable name. Multiple variables of the same type can all be declared in the same
statement by listing their variable names, separated by commas:
type variable_name1, variable_name2, …;
Another way to avoid directly providing a type is to use the decltype specifier. It is used
to deduce a type of a given entity and is written with the following syntax:
type variable_name1;
decltype(variable_name1) variable_name2;
Here, variable_name2 is declared according to the type deducted from
variable_name1.
Note
Type deduction using the auto and decltype keywords has been introduced by the
C++11 standard to simplify and facilitate variable declaration when the type cannot
be obtained. But at the same time, their extended use when not really needed can
reduce code readability and robustness. We will see this in more detail in Lesson 4,
Generic Programming and Templates.
In the following code, we will check a valid statement for variables by creating a new
source file named main.cpp and analyzing the code one line at a time.
Which one of the following is a valid statement?
int foo;
auto foo2;
int bar = 10;
sum = 0;
float price = 5.3 , cost = 10.1;
auto val = 5.6;
auto val = 5.6f;
auto var = val;
int a = 0, b = {1} , c(0);
Pointers and References | 15
Using this in code will return the physical memory address of the variable.
Pointers
A data structure that's capable of storing a memory address in C++ is known as a
pointer. A pointer always points to an object of a specific type, and because of that we
need to specify the type of the object that's pointed to when declaring the pointer.
The syntax to declare a pointer is as follows:
type * pointer_name;
Multiple declarations in the same statement are also possible when it comes to a
pointer, but it is important to remember that an asterisk (*) is needed for each pointer
declaration. An example of multiple pointer declaration is as follows:
type * pointer_name1, * pointer_name2, *...;
When the asterisk is specified only for the first declaration, the two variables will have
different types. For example, in the following declaration, only the former is a pointer:
type * pointer_name, pointer_name;
Note
Independently of the pointed variable type, a pointer will always occupy the same
size in memory. This derives from the fact that the memory space needed by the
pointer is not related to a value stored by the variable, but to a memory address
that is platform-dependent.
16 | Getting Started
Intuitively, a pointer assignment has the same syntax as any other variable:
pointer_name = &variable_name;
The previous syntax will copy the memory address of the variable_name variable into
the pointer named pointer_name.
The following code snippet will first initialize pointer_name1 with the memory address
of variable_name, and then it initializes pointer_name2 with the value stored in pointer_
name1, which is the memory address of variable_name. As a result, pointer_name2 will end
up pointing to the variable_name variable:
type * pointer_name1 = &variable_name;
type * pointer_name2 = pointer_name1;
The following implementation is invalid:
type * pointer_name1 = &variable_name;
type * pointer_name2 = &pointer_name1;
This time, pointer_name2 would be initialized with the memory address of
pointer_name1, resulting in a pointer that points to another pointer. The way
to point a pointer to another pointer is to use the following code:
type ** pointer_name;
Two asterisks (*) indicate the type that's pointed is now a pointer. In general, the syntax
simply requires an asterisk (*) for each level of indirection in the declaration of the
pointer.
To access the actual content at a given memory address, it is possible to use the
dereference operator (*), followed by the memory address or a pointer:
type variable_name1 = value;
type * pointer_name = &variable_name1;
type variable_name2 = *pointer_name;
The value contained by variable_name2 is the same as the one contained by variable_
name1. The same applies when it comes to assignment:
type variable_name1 = value1;
type * pointer_name = &variable_name1;
*pointer_name = value2;
Pointers and References | 17
References
Unlike a pointer, a reference is just an alias for an object, which is essentially a way to
give another name to an existing variable. The way to define a reference is as follows:
type variable_name = value;
type &reference_name = variable_name;
Let's examine the following example:
#include <iostream>
int main()
{
int first_variable = 10;
int &ref_name = first_variable;
std::cout << "Value of first_variable: " << first_variable << std::endl;
std::cout << "Value of ref_name: " << ref_name << std::endl;
}
//Output
Value of first_variable: 10
Value of ref_name: 10
We can identify three main differences with pointers:
• Once initialized, a reference remains bound to its initial object. So, it is not
possible to reassign a reference to another object. Any operations performed on a
reference are actually operations on the object that has been referred.
• Since there is not the possibility to rebind a reference, it is necessary to initialize
it.
• References are always associated with a variable that's stored in memory, but the
variable might not be valid, in which case the reference should not be used. We
will see more on this in the Lesson 6, Object-Oriented Programming.
It is possible to define multiple references to the same object. Since the reference is not
an object, it is not possible to have a reference to another reference.
18 | Getting Started
There are several reasons to enforce immutability in a C++ program, the most important
ones being correctness and performance. Ensuring that a variable is constant will
prevent the compilation of code that accidentally tries to change that variable,
preventing possible bugs.
The other reason is that informing the compiler about the immutability of the variable
allows for optimizing the code and logic behind the implementation of the code.
Note
After creating an object, if its state remains unchanged, then this characteristic is
known as immutability.
Pointers and References | 19
References to const cannot be used to change the object they refer to. Note that it
is possible to bind a const reference to a non-const type, which is typically used to
express that the object that's been referenced will be used as an immutable one:
type variable_name;
const type &reference_name = variable_name;
However, the opposite is not allowed. If an object is const, then it can only be
referenced by a const reference:
const type variable_name = value;
type &reference_name = variable_name;
// Wrong
An example of this is as follows:
#include <iostream>
int main()
{
const int const_v = 10;
int &const_ref = const_v;
//Error
std::cout << const_v << std::endl;
//Output: 10
}
Just like for references, pointers can point to the const object, and the syntax is also
similar and intuitive:
const type *pointer_name = &variable_name;
const object addresses can only be stored in a pointer to const, but the opposite is not
true. We could have a pointer to const point to a non-const object and, in this case, like
for a reference to const, we are not guaranteed that the object itself will not change, but
only that the pointer cannot be used to modify it.
With pointers, since they are also objects, we have an additional case, which is the const
pointer. While for references saying const reference is just a short version of reference
to const, this is not the case for the pointer and has a totally different meaning.
Indeed, a const pointer is a pointer that is itself constant. Here, the pointer does not
indicate anything about the pointed object; it might be either const or non-const, but
what we cannot change instead is the address pointed to by the pointer once it has
been initialized. The syntax is as follows:
type *const pointer_name = &variable_name;
As you can see, the const keyword is placed after the * symbol. The easiest way to keep
this rule in mind is to read from right to left, so pointer-name > const > * > type can
be read as follows: pointer-name is a const pointer to an object of type type. An example
of this is as follows:
#include <iostream>
int main()
{
int v = 10;
int *const v_const_pointer = &v;
std::cout << v << std::endl;
22 | Getting Started
//Output: 10
std::cout << v_const_pointer << std::endl;
//Output: Memory location of v
}
Note
Pointer to const and const to pointer are independent and can be expressed in the
same statement:
The preceding line indicates that both the pointed object and the pointer
are const.
The same name can be declared in two scopes and refers to different entities. Also, a
name is visible once it is declared until the end of the block in which it is declared.
Pointers and References | 23
Let's understand the scope of global and local variables with the following example:
#include <iostream>
int global_var = 100;
//Global variable initialized
int print(){
std::cout << global_var << std::endl;
//Output: 100
std::cout << local_var << std::endl;
//Output: Error: Out of scope
}
int main()
{
int local_var = 10;
std::cout << local_var << std::endl;
//Output: 10
std::cout << global_var << std::endl;
//Output: 100
print();
//Output:100
//Output: Error: Out of scope
}
Scopes can be nested, and we call the containing and contained scope the outer and
inner scope, respectively. Names declared in the outer scope can be used in the inner
one. Re-declaration of a name that was initially declared in the outer scope is possible.
The result will be that the new variable will hide the one that was declared in the outer
scope.
Let's examine the following code:
#include <iostream>
int global_var = 1000;
int main()
24 | Getting Started
{
int global_var = 100;
std::cout << "Global: "<< ::global_var << std::endl;
std::cout << "Local: " << global_var << std::endl;
}
Output:
Global: 1000
Local: 100
In the next chapter, we will explore how to use local and global variables with functions.
In the following code, we will find the values of all the variables without executing the
program.
The following program shows how variable initialization works:
#include <iostream>
int main()
{
int a = 10;
{
int b = a;
}
const int c = 11;
int d = c;
c = a;
}
Note
It is common to forget the brace brackets and write the control statement in the
following manner:
if (condition)
statement1
statement2
In such a case, the compiler will not warn you, and it will execute statement1
depending on the condition, but always execute statement2. To avoid such a
situation, it can be a good practice to always add the braces.
26 | Getting Started
It is possible to specify what to execute instead when the condition evaluates to false.
This is done through the else keyword, which is followed by a statement or a block.
The following syntax is used to indicate that statement1 should be executed if the case
condition evaluates to true, and otherwise statement2 is executed:
if (condition) statement1 else statement2
With this generic structure, it is possible to check the unlimited number of conditions
and execute only the corresponding statement or the final one contained in the else
branch.
It is important to be aware that once one of the conditions is met, all of the ones that
follow are discarded, for example:
if (x > 0) {
// When x is greater than 0, statement1 is executed.
// If that is not the case, the control jumps to the else block.
statement1
} else if (x > 100) {
statement2
}
The previous code will always execute statement1 for any positive x, regardless of
whether it is greater than 100 or not.
Control Flow Statements | 27
Output:
10
9
28 | Getting Started
In this way, all the conditions are evaluated independently and more than one
statement can potentially be executed.
Note
As the else statement has no condition defined, after evaluating the if statement,
the control comes to the else block to execute the statement.
case constant2:
group-of-statements-2;
break;
...
default:
default-group-of-statements;
break;
}
Control Flow Statements | 29
The expression present in the parentheses following the switch keyword is evaluated
against multiple cases, searching for the first equality between the expression and the
constants. If none of the cases match, the default one (if it exists, since it is optional) is
executed.
It is important to keep in mind that the order of evaluation is sequential, and as soon
as one of the constants matches, the corresponding group of statements are executed.
The break keyword prevents them from further execution. If the break keyword is not
included, all statements following the case, including the ones under different labels,
are also executed.
We will explore the break keyword more in the Jump statements – break and continue
section.
The for loop consists of two parts: the header and the body. The former controls how
many times the latter is repeated. The header is the part enclosed by parentheses and
it is formed by initialization, condition, and increase statements. The body can be a
single statement or a block of multiple ones.
The initialization statement is typically (but not necessarily) used to declare a new
variable, usually a counter, and to initialize it to a certain value. The initialization
statement is executed only once, at the beginning of the loop.
Secondly, the condition statement is checked. This is similar to the condition that's
checked for an if statement. If the condition is true, the body of the loop is executed,
otherwise the program continues its execution with the instruction after the body of
the for loop.
30 | Getting Started
After the body executes, the increase statement is executed. This usually changes the
counter of the initialization statement. The condition is then checked again and, if true,
the steps are repeated. The loop ends when the condition evaluates to false.
The fields in the header of a for loop are optional and can be left blank, but the
semicolons cannot be omitted. When the condition is omitted, it always evaluates to
true. For example, the following corresponds to an infinite loop where the statement is
executed unconditionally:
for ( ; ; ) statement;
Another variant of the for loop is called a range-based for loop, the syntax for which is
as follows:
for ( declaration : range ) statement;
A range is a sequence of elements, like arrays, which are explained in the next section.
This range-based for loop is used to iterate over all elements of these sequences. The
range is the name of the sequence, and in the for declaration, the name is a temporary
variable that's declared for every iteration of the loop. This is used to store the current
element. The declaration needs to be the same type as the elements contained in the
range.
Note
A range-based for loop is a good example where type deduction and the use of
the auto keyword for the declaration makes the code more readable and helps the
programmer find the right type to use.
Control Flow Statements | 31
A loop placed inside a loop is known as a nested loop. Let's look at the following
diagram to understand what a nested for loop is:
Using the following example, let's explore how a nested for loop works and print a
reverse half-triangle on the console:
#include <iostream>
int main()
{
for (int x = 0; x < 5; x++){
for (int y = 5; y > x; y--){
std::cout << "*";
}
std::cout <<"\n" ;
}
}
Output:
*****
****
32 | Getting Started
***
**
*
It repeats the statement until the condition is met. When the condition is not true
anymore, the loop ends and the program continues its execution right after the loop:
Note
A while loop can always be expressed using a for loop.
It guarantees at least one execution of the statement, even though the condition never
evaluates to true.
Alternatively, the continue statement is used to skip the rest of the body's loop in the
current iteration. In the following example, when condition2 evaluates to true, continue
is called, causing the program to reach the end of the loop, skipping statement2 and
continuing with the next iteration:
while (condition1){
statement1;
if (condition2)
continue;
statement2;
}
Note
The break and continue statements can be used in both for and while loops.
A catch block consists of the catch keyword, the exception declaration, and a block.
Based on the exception thrown inside the try block, one catch clause is selected and
the corresponding block is executed. Once the catch block has terminated, the program
continues its execution with the statement following the last catch clause.
Let's examine the following example to understand how try-catch conditional
statements handle exceptions:
#include <iostream>
int main()
{
int x = 10;
try {
std::cout << "Inside try block" << std::endl;
if (x > 0) // True
{
throw x;// Following statement will be skipped
std::cout << "After throw keyword" << std::endl;
}
}
catch (int x ) {
std::cout << "Inside catch block: Exception found" << std::endl;
}
std::cout << "Outside try-catch block" << std::endl;
}
Output:
Inside try block
Inside catch block: Exception found
Outside try-catch block
The try-catch block | 35
Hint
To find out if a number is divisible by another, use the modulo (%) operator.
Bonus exercises:
• Find how many numbers are divisible by 11 within the range of 1 to 100
• Print all the numbers that are not divisible by 3 within the range of 1 to 30
Activity 1: Finding the Factors of 7 between 1 and 100 Using a while Loop
In the following activity, we will use a while loop and implement the previous concept
from the earlier exercise to print the numbers between 1 and 100 that are divisible by 7.
Now, let's rewrite the previous code using a while loop in the following way:
1. Create a variable of the unsigned type.
2. Now, write the logic to print the numbers that are divisible by 7 using
the while loop.
3. Then, we have to increase the value of i after each iteration.
Use the following code:
i++;
The solution for this activity can be found on page 282.
Arrays
An array is a data structure containing a series of elements of the same type that have
been placed in contiguous memory locations that can be individually accessed by their
position.
Arrays have fixed sizes and cannot be extended; this contributes to their runtime
performance, with a cost in terms of limited flexibility.
Array Declaration
Like any other variable, arrays need to be declared before they can be used. An array
declaration has the following form:
type name [elements];
Here, type is the type of the contained elements, name is the identifier of the array
variable, and elements is the length of the array, so it signifies the number of elements
contained within.
Arrays | 37
The term elements needs to be a constant expressions that is known at compile time,
since that is the time when the array size is evaluated to determine the dimension of
the block of static memory to allocate.
When an array is declared, its content is left undetermined, which means that the
elements are not set to any specific value. This is often confusing for programmers as
you might expect that the elements are initialized to the default value for the array type.
Array Initialization
Array elements can be specifically initialized at declaration time by enclosing these
initial values in braces:
int foo [5] = { 1, 2, 11, 15, 1989 };
When we initialize a list array, we can also omit its length as it will be determined by the
number of values provided. The following declaration is equivalent to the previous one:
int foo [] = { 1, 2, 11, 15, 1989 };
If the number of elements is provided, but the array is initialized with fewer elements,
then the remaining value will be zero-initialized, for example:
int foo [5] = { 1, 2, 11 };
An element of an array can be accessed to store a new element or to read its value.
For example, the following statement updates the value at position 4 of the previously
declared array named foo:
foo [4] = 15
The following is used to copy the content of the element at position 2 into a new
variable:
int x = foo [2]
38 | Getting Started
It is important to notice that the elements at positions 4 and 2 refer to the fifth and
third elements, respectively. This is due to the fact that indexing starts from 0. The
following diagram illustrates how index entries work in arrays:
Exceeding the valid range of indices for an array is syntactically correct, so the compiler
will not produce any errors. Accessing an array out of bounds in C++ is considered an
undefined behavior, which means that the code's behavior is not prescribed by the
language specification. This can result in runtime errors such as bugs caused by access
to an unallocated location in memory or program termination (segmentation fault) due
to an attempt to access memory not owned by the program.
Multidimensional Arrays
Multidimensional arrays are commonly described as arrays of arrays, where an array's
elements are other arrays.
The following syntax illustrates a bi-dimensional array:
type name [n][m];
int bi_array [3][4]
Here, n is the dimension of the array and m is the dimension of its elements.
Typically, in a bi-dimensional array like the previous one, the first dimension is referred
to as the row and the second one is referred to as the column.
Multidimensional arrays are not limited to two dimensions; they can have as
many dimensions as needed, but keep in mind that the memory that's used
increases exponentially with each dimension. Similar to one-dimensional arrays,
multidimensional arrays can be initialized by specifying a list of initializers, one for each
row. Let's examine the following code:
#include <iostream>
int main()
{
Arrays | 39
Note:
The solution for this activity can be found on page 282.
Summary
In this chapter, we saw the basic structure and syntax of the language. We started with
an overview of the compilation model, the process of transforming C++ source code
into an executable program. We wrote, compiled, and ran our first program, a simple
main function that successfully returns an exit/return code.
We described the built-in arithmetic types that the language offers.
We learned how to declare and define variable names, and what the difference is
between references and pointers. We also saw the use of the const qualifier and
its advantages.
Furthermore, we talked about control flow statements and how to exploit them to
perform more complex actions.
Finally, we presented arrays and multidimensional arrays, and the operation to perform
to initialize them and access their values. In the next chapter, we will learn what
functions in C++ are, and how and why we should use them in our code.
Functions
2
Lesson Objectives
In this chapter, we are going to look at functions in C++, how to use them, and why we would
want to use them.
44 | Functions
Introduction
Functions are a core tool in a programmer's toolkit for writing maintainable code. The
concept of a function is common in almost every programming language. Functions
have different names in various languages: procedures, routines, and many more, but
they all have two main characteristics in common:
• They represent a sequence of instructions grouped together.
• The sequence of instructions is identified by a name, which can be used to refer to
the function.
The programmer can call, or invoke a function when the functionalities provided by the
function are needed.
When the function is called, the sequence of instructions is executed. The caller can
also provide some data to the function to be used in operations within the program. The
following are the main advantages of using functions:
• Reduces repetition: It often occurs that a program needs to repeat the same
operations in different parts of the codebase. Functions allow us to write a single
implementation that is carefully tested, documented, and of high quality. This
code can be called from different places in the codebase, which enables code
reusability. This, in turn, increases the productivity of the programmer and the
quality of the software.
• Boosts code readability and modification: Often, we need several operations to
implement a functionality in our program. In these cases, grouping the operations
together in a function and giving a descriptive name to the function helps to
express what we want to do instead of how we do it.
Using functions greatly increases the readability of our code because it's now
composed of descriptive names of what we are trying to achieve, without the
noise of how the result is achieved.
In fact, it is easier to test and debug as you may only need to modify a function
without having to revisit the structure of the program.
Function Declaration and Definition | 45
Note
Abstraction is the process of extracting all relevant properties from a class and
exposing them, while hiding details that are not important for a specific usage.
In computer science, we want to apply the same concept: capture the key
fundamental properties of a class without showing the algorithm that implements
it.
A prime example of this is the sort function, which is present in many languages. We
know what the function expects and what it is going to do, but rarely are we aware
of the algorithm that is used to do it, and it might also change between different
implementations of the language.
In the following sections, we will demystify how function declaration and definition
works.
A declaration is composed of the type of the returned value, followed by the name of
the function and by a list of parameters inside a pair of parentheses. These last two
components form the signature of the function. The syntax of a function declaration is
as follows:
// Declaration: function without body
return_type function_name( parameter list );
If a function returns nothing, then the type void can be used, and if a function is not
expecting any parameters the list can be empty.
Let's look at an example of a function declaration:
void doNothingForNow();
void doNothingForNow();
int main() {
doNothingForNow ();
std::cout << "Done";
}
Function Declaration and Definition | 47
If we were to compile this program, the compilation would succeed, but linking would
fail.
In this program, we instructed the compiler that a function called doNothingForNow()
exists and then we invoked it. The compiler generates an output that calls
doNothingForNow().
The linker then tries to create an executable from the compiler output, but since we did
not define doNothingForNow(), it cannot find the function's definition, so it fails.
To successfully compile the program, we need to define doNothingForNow(). In the next
section, we will explore how to define a function using the same example.
Defining a Function
To define a function, we need to write the same information that we used for the
declaration: the return type, the name of the function, and the parameter list, followed
by the function body. The function body delimits a new scope and is composed of a
sequence of statements delimited by curly braces.
When the function is executed, the statements are executed in order:
// Definition: function with body
return_type function_name( parameter_list ) {
statement1;
statement2;
...
last statement;
}
Let's fix the program by adding the body for doNothingForNow():
void doNothingForNow() {
// Do nothing
}
48 | Functions
Here, we defined doNothingForNow() with an empty body. This means that as soon as the
function execution starts, the control flow returns to the function that called it.
Note
When we define a function, we need to make sure that the signature (the return
value, the name, and the parameters) are the same as the declaration.
Let's revisit our program now since we have added the definition for our function:
#include <iostream>
void doNothingForNow() {
// do nothing
}
int main() {
doNothingForNow();
std::cout << "Done";
}
If we compile and run the program, it will succeed and show Done on the output
console.
Function Declaration and Definition | 49
In a program, there can be multiple declarations of the same function, as long as the
declarations are the same. On the other hand, only a single definition of the function
can exist, as mandated by the One Definition Rule (ODR).
Note
Several definitions of the same function may exist if compiled in different files, but
they need to be identical. If they are not, then the program might do unpredictable
things.
The solution is to have the declaration in a header file, and the definition in an
implementation file.
A header file is included in many different implementation files, and the code in these
files can call the function.
An implementation file is compiled only once, so we can guarantee that the definition is
seen only once by the compiler.
Then, the linker puts all of the outputs of the compiler together, finds a definition of the
function, and produces a valid executable.
2. Now, let's create a new file, log.cpp, where we define the log() function to print to
the standard output:
#include <iostream>
// This is where std::cout and std::endl are defined
void log() {
std::cout << "Error!" << std::endl;
}
3. Change the main.cpp file to include log.h and call log() in the main() function:
#include <log.h>
int main() {
log();
}
4. Compile the two files and run the executable. You will see that the message Error!
is printed when we execute it.
Note
Local variables are in the function scope and can only be accessed by the function.
On the contrary, global variables can be accessed by any function that can see
them.
Local and Global Variables | 51
It is desirable to use local variables over global variables because they enable
encapsulation: only the code inside the function body can access and modify the
variable, making the variable invisible to the rest of the program. This makes it easy
to understand how a variable is used by a function since its usage is restricted to the
function body and we are guaranteed that no other code is accessing it.
Encapsulation is usually used for three separate reasons, which we will explore in more
detail in Lesson 3, Classes:
• To restrict the access to data used by a functionality
• To bundle together the data and the functionality that operates on it
• Encapsulation is a key concept that allows you to create abstractions
Note
Always use the const qualifier with global variables whenever possible.
It is a good practice to use global const variables instead of using values directly in
the code. They allow you to give a name and a meaning to the value, without any
of the risks that come with mutable global variables.
52 | Functions
Note
An object is a piece of data in the program's memory.
There is a distinction in C++ between the scope of a variable and the lifetime of the
object it refers to. The scope of a variable is the part of the program where the variable
can be used.
The lifetime of an object, on the contrary, is the time during execution wherein the
object is valid to access.
Let's examine the following program to understand the lifetime of an object:
#include <iostream>
/* 1 */ const int globalVar = 10;
int* foo(const int* other) {
/* 5 */ int fooLocal = 0;
std::cout << "foo's local: " << fooLocal << std::endl;
std::cout << "main's local: " << *other << std::endl;
/* 6 */ return &fooLocal;
}
int main()
{
/* 2 */ int mainLocal = 15;
/* 3 */ int* fooPointer = foo(&mainLocal);
std::cout << "main's local: " << mainLocal << std::endl;
Local and Global Variables | 53
std::cout << "We should not access the content of fooPointer! It's not
valid." << std::endl;
/* 4 */ return 0;
}
The lifetime of a variable starts when it is initialized and ends when the containing
block ends. Even if we have a pointer or reference to a variable, we should access it only
if it's still valid. fooPointer is pointing to a variable which is no longer valid, so it should
not be used!
When we declare a local variable in the scope of a function, the compiler automatically
creates an object when the function execution reaches the variable declaration; the
variable refers to that object.
When we declare a global variable instead, we are declaring it in a scope that does not
have a clear duration – it is valid for the complete duration of the program. Because of
this, the compiler creates the object when the program starts before any function is
executed – even the main() function.
The compiler also takes care of terminating the object's lifetime when the execution
exits from the scope in which the variable has been declared, or when the program
terminates in the case of a global variable. The termination of the lifetime of an object is
usually called destruction.
Variables declared in a scope block, either local or global, are called automatic
variables, because the compiler takes care of initializing and terminating the lifetime of
the object associated with the variables.
54 | Functions
Note
The default initialization of basic types, such as integers, is doing nothing for us.
This means that the variable a will have an unspecified value.
If multiple local variables are defined, the initialization of the objects happens in the
order of declaration:
void foo() {
int a;
int b;
}
Variable a is initialized before b. Since variable b was initialized after a, its object is
destroyed before the one a refers to.
If the execution never reaches the declaration, the variable is not initialized. If the
variable is not initialized, it is also not destroyed:
void foo() {
if (false) {
int a;
}
int b;
}
Local and Global Variables | 55
Here, the variable a is never default initialized, and thus never destroyed. This is similar
for global variables:
const int a = 1;
void main() {
std::cout << "a=" << a << std::endl;
}
Variable a is initialized before the main() function is called and is destroyed after we
return the value from the main() function.
Note
The nth Fibonacci number is defined as the sum of the n-1th and the n-2th, with the
first number in the sequence being 0 and the second being 1.
Example:
We want to use the best practice of giving a name and a meaning to values, so instead of
using 10 in the code, we are going to define a const global variable, named POSITION.
We will also use two local variables in the function to remember the n-1th and the n-2th
number:
1. Write the program and include the following constant global variable after the
header file:
#include <iostream>
For each combination of cond1 and cond2, identify when initialization and destruction
occurs in the following program:
void foo()
if(cond1) {
int a;
}
if (cond2) {
int b;
}
}
Inside its body, the function can access the identifiers defined in the function signature
as if they were declared variables. The values of the function parameters are decided
when the function is called.
To call a function that takes a parameter, you need to write the name of the function,
followed by a list of expressions inside a pair of parentheses:
two_ints(1,2);
58 | Functions
Note
Parameter: This is a variable that was defined by a function, and can be used to
provide data as per the code.
Argument: The value the caller wants to bind to the parameters of the function.
In the following example, we used two values, but we can also use arbitrary expressions
as arguments:
two_ints(1+2, 2+3);
Note
The order in which the expression is evaluated is not specified!
This means that when calling two_ints(1+2, 2+3);, the compiler might first execute
1+2 and then 2+3, or 2+3 and then 1+2. This is usually not a problem if the expression
does not change any state in the program, but it can create bugs that are hard to detect
when it does. For example, given int i = 0;, if we call two_ints(i++, i++), we don't
know whether the function is going to be called with two_ints(0, 1) or two_ints(1, 0).
In general, it's better to declare expressions that change the state of the program
in their own statements, and call functions with expressions that do not modify the
program's state.
The function parameters can be of any type. As we already saw, a type in C++ could be
a value, a reference, or a pointer. This gives the programmer a few options on how to
accept parameters from the callers, based on the behavior it wants.
In the following subsections, we will explore the working mechanism of Pass by value
and Pass by reference in more detail.
Passing Arguments and Returning Values | 59
Pass by Value
When the parameter type of a function is a value type, we say that the function is taking
an argument by value or the argument is passed by value.
When a parameter is a value type, a new local object is created each time the function is
called.
As we saw with automatic variables, the lifetime of the object lasts until the execution
does not reach the end of the function's scope.
When the parameter is initialized, a new copy is made from the argument provided
when invoking the function.
Note
If you want to modify a parameter but do not want or do not care about the calling
code seeing the modification, use pass by value.
2. Now, in main(), call the function we created in the previous step by passing the
variable age as a value:
int main() {
int age = 95;
byvalue_age_in_5_years(age);
std::cout << "Current age: " << age;
// Prints 95
}
Note
Pass by value should be the default way of accepting arguments: always use it
unless you have a specific reason not to.
The reason for this is that it makes the separation between the calling code and
the called function stricter: the calling code cannot see the changes that the called
function makes on the parameters.
Passing parameters by value creates a clear boundary between the calling function and
the called function, because the parameters are copied:
1. As the calling function, we know that the variables we passed to the functions will
not be modified by it.
2. As the called function, we know that even if we modify the provided parameters,
there will be no impact on the called function.
This makes it easy to understand the code, because the changes we make to the
parameters have no impact outside of the function.
Pass by value can be the faster option when taking an argument, especially if the
memory size of the argument is small (for example, integers, characters, float, or small
structures).
Passing Arguments and Returning Values | 61
We need to remember though that passing by value performs a copy of the argument.
Sometimes, this can be an expensive operation both in terms of memory and processing
time, like when copying a container with many elements.
There are some cases where this limitation can be overcome with the move semantic
that was added in C++11. We will see more of it in Lesson 3, Classes.
Let's look at an alternative to pass by value that has a different set of properties.
Pass by Reference
When the parameter type of the function is a reference type, we say that the function is
taking an argument by reference or the argument is passed by reference.
We saw earlier that a reference type does not create a new object – it is simply a new
variable, or name that refers to an object that already exists.
When the function that accepts the argument by reference is called, the reference is
bound to the object used in the argument: the parameter will refer to the given object.
This means that the function has access to the object the calling code provided and can
modify it.
This is convenient if the goal of the function is to modify an object, but it can be more
difficult to understand the interaction between the caller and the called function in
such situations.
Note
Unless the function must modify the variable, always use const references, as we
will see later.
62 | Functions
Note
If you want to use pass by reference, but you are not modifying the provided
object, make sure to use const.
With C++, we can use std::cin to read input from the console executing the program.
When writing std::cin >> variable;, the program will block waiting for some user
input, and then it will populate variable with the value read from the input as long as it
is a valid value and the program knows how to read it. By default, we can assign all the
built-in data types and some types defined in the standard library, such as string.
Working with const References or r-value References | 63
We need to remember that a pointer is a value that represents the location of an object.
Being a value, it means that when we are accepting a parameter as a pointer, the pointer
itself is passed as a value.
This means that the modification of the pointer inside the function is not going to be
visible to the caller.
But if we are modifying the object the pointer points to, then the original object is going
to be modified:
void modify_pointer(int* pointer) {
*pointer = 1;
pointer = 0;
}
int main() {
int a = 0;
int* ptr = &a;
modify_pointer(ptr);
std::cout << "Value: " << *ptr << std::endl;
std::cout << "Did the pointer change? " << std::boolalpha << (ptr == &a);
}
Most of the time, we can think of passing a pointer as passing a reference, with the
caveat that you need to be aware that the pointer might be null.
Accepting a parameter as a pointer is mainly used for three reasons:
• Traversing the elements of an array, by providing the start pointer and either the
end pointer or the size of the array.
• Optionally modifying a value. This means that the function modifies a value if it is
provided.
• Returning more than a single value. This is often done to set the value of a pointer
passed as an argument and then return an error code to signal whether the
operation was performed.
We will see in Lesson 4, Generic Programming and Templates, how features introduced
in C++11 and C++17 allow us to avoid using pointers for some of these use cases,
eliminating the possibility of some common classes of errors, such as dereferencing
invalid pointers or accessing unallocated memory.
Working with const References or r-value References | 65
The options of passing by value or passing by reference are applicable to every single
parameter the function expects, independently.
This means that a function can take some arguments by value and some by reference.
The previous function accepts two integers by value as parameters and returns an
integer.
The invocation of the function in the caller code is an expression evaluating to an
integer. This means that we can use it anywhere that an expression is allowed:
int a = sum(1, 2);
A function can return a value by using the return keyword, followed by the value it
wants to return.
The function can use the return keyword several times inside its body, and each time
the execution reaches the return keyword, the program will stop executing the function
and go back to the caller, with the value returned by the function, if any. Let's look at
the following code:
void rideRollercoasterWithChecks(int heightInCm) {
if (heightInCm < 100) {
std::cout << "Too short";
return;
}
if (heightInCm > 210) {
std::cout << "Too tall";
return;
}
rideRollercoaster();
66 | Functions
Note
It is surprising, but every major compiler allows the compiling of functions, which
declare a return type other than void, but don't return a value.
This is easy to spot in simple functions, but it is much harder in complex ones with
lots of branches.
Note
It is a good practice to return as early as possible in an algorithm.
The reason for this is that as you follow the logic of the code, especially when
there are many conditionals, a return statement tells you when that execution
path is finished, allowing you to ignore what happens in the remaining part of the
function.
If you only return at the end of the function, you always have to look at the full
code of the function.
Since a function can be declared to return any type, we have to decide whether to
return a value or a reference.
Returning by Value
A function whose return type is a value type is said to return by value.
When a function that returns by value reaches a return statement, the program
creates a new object, which is initialized from the value of the expression in the return
statement.
In the previous function, sum, when the code reaches the stage of returning a + b, a new
integer is created, with the value equal to the sum of a and b, and is returned.
On the side of the caller, int a = sum(1,2);, a new temporary automatic object is
created and is initialized from the value returned by the function (the integer that was
created from the sum of a and b).
This object is called temporary because its lifetime is valid only while the full-
expression in which it is created is executed. We will see in the Returning by Reference
section, what this means and why this is important.
The calling code can then use the returned temporary value in another expression or
assign it to a value.
Add the end of the full expression, since the lifetime of the temporary object is over, it
is destroyed.
68 | Functions
In this explanation, we mentioned that objects are initialized several times while
returning a value. This is not a performance concern as C++ allows compilers to
optimize all these initializations, and often initialization happens only once.
Note
It is preferable to return by value as it's often easier to understand, easier to use,
and as fast as returning by reference.
How can returning by value be so fast? C++11 introduced the move semantic, which
allows moving instead of copying the return types when they support the move
operation. We'll see how in Lesson 3, Classes. Even before C++11, all mainstream
compilers implemented return value optimization (RVO) and named return
value optimization (NRVO), where the return value of a function is constructed
directly in the variable into which they would have been copied to when returned.
In C++17, this optimization, also called copy elision, became mandatory.
Returning by Reference
A function whose return type is a reference is said to return by reference.
When a function returning a reference reaches a return statement, a new reference is
initialized from the expression that's used in the return statement.
In the caller, the function call expression is substituted by the returned reference.
However, in this situation, we need to also be aware of the lifetime of the object the
reference is referring to. Let's look at an example:
const int& max(const int& a, const int& b) {
if (a > b) {
return a;
} else {
return b;
}
}
Working with const References or r-value References | 69
First, we need to note that this function already has a caveat. The max function is
returning by value, and it did not make a difference if we returned a or b when they
were equal.
In this function, instead, when a == b we are returning b, this means that the code
calling this function needs to be aware of this distinction. In the case where a function
returns a non-const reference it might modify the object referred to by the returned
reference, and whether a or b is returned might make a difference.
We are already seeing how references can make our code harder to understand.
Let's look at the function we used:
int main() {
const int& a = max(1,2);
std::cout << a;
}
This program has an error! The reason is that 1 and 2 are temporary values, and as
we explained before, a temporary value is alive until the end of the full expression
containing it.
To better understand what is meant by "the end of the full expression containing it", let's
look at the code we have in the preceding code block: int& a = max(1,2);. There are
four expressions in this piece of code:
• 1 is an integer literal, which still counts as an expression
• 2 is an integer literal, similar to 1
• max(expression1, expression2) is a function call expression
• a = expression3 is an assignment expression
We created a local, automatic object in the function body and then we returned a
reference to it.
In the previous section, we saw that local objects' lifetimes end at the end of the
function. This means that we are returning a reference to an object whose lifetime will
always be terminated.
Earlier, we mentioned the similarities between passing arguments by reference and
passing arguments by pointers.
This similarity persists when returning pointers: the object pointed to by a pointer
needs to be alive when the pointer is later dereferenced.
So far, we have covered examples of mistakes while returning by reference. How can
references be used correctly as return types to functions?
The important part of using references correctly as return values is to make sure that
the object outlives the reference: the object must always be alive – at least until there is
a reference to it.
Working with const References or r-value References | 71
In this example, we are returning a reference to an element inside an array, and the
array remains alive longer than the reference.
The following are guidelines for using return by reference correctly:
• Never return a reference to a local variable (or a part of it)
• Never return a reference to a parameter accepted by value (or a part of it)
When returning a reference that was received as a parameter, the argument passed to
the function must live longer than the returned reference.
Apply the previous rule, even when you are returning a reference to a part of the object
(for example, an element of an array).
72 | Functions
Take the arrays by reference and return by reference because we are saying that the
calling function is supposed to modify the element. Take the index by value since there
is no reason to use references.
If the values are the same, the element from the first array is returned.
Note
The solution to this activity can be found at page 285.
The only reason to use const in the function signature is to document to the
implementation that it cannot modify such a value.
This is not commonly done, as the biggest value of a function signature is for the caller
to understand the contract of calling the function. Because of this, it is rare to see int
max(const int, const int), even if the function does not modify the parameters.
There is an exception, though: when the function accepts a pointer.
In such cases, the function wants to make sure that it is not assigning a new value to the
pointer. The pointer acts similar to a reference here, since it cannot be bound to a new
object, but provides nullability.
An example could be setValue(int * const), a function that takes a const pointer to an
int.
The integer is not const, so it can be changed, but the pointer is const and the
implementation cannot change it during implementation.
This should clearly take a reference to the array since its purpose is to modify the array.
On the other hand, we can use the following:
int findFirstGreaterThan(const std::array<int, 10>& array, int threshold)
Default Arguments | 75
This tells us that we are only looking into the array – we are not changing it, so we
should use const.
Note
It is a best practice to use const as much as possible, as it allows the compiler to
make sure that we are not modifying objects that we do not want to modify.
It also helps to keep another best practice in mind: never use the same variable
to represent different concepts. Since the variable cannot be changed, it is less
natural to reuse it instead of creating a new one.
Default Arguments
Another feature C++ provides to make life easier for the caller when it comes to calling
functions are default arguments.
Default arguments are added to a function declaration. The syntax is to add an = sign
and supply the value of the default argument after the identifier of the parameter of the
function. An example of this would be:
int multiply(int multiplied, int multiplier = 1);
The caller of the function can call multiply either with 1 or 2 arguments:
multiply(10); // Returns 10
multiply(10, 2); // Returns 20
When an argument with a default value is omitted, the function uses the default value
instead. This is extremely convenient if there are functions with sensible defaults that
callers mostly do not want to modify, except in specific cases.
Imagine a function that returns the first word of a string:
char const * firstWord(char const * string, char separator = ' ').
Most of the time, a word is separated by a whitespace character, but a function can
decide whether or not it should use a different separator. The fact that a function offers
the possibility to provide a separator is not forcing most callers, which simply want to
use the space, to specify it.
76 | Functions
It is a best practice to set the default arguments in the function signature declaration,
and not declare them in the definition.
Namespaces
One of the goals of functions is to better organize our code. To do so, it is important to
give meaningful names to them.
For example, in package management software, there might be a function called sort
for sorting packages. As you can see, the name is the same as the function that would
sort a list of numbers.
C++ has a feature that allows you to avoid these kinds of problems and groups names
together: namespaces.
A namespace starts a scope in which all the names declared inside are part of the
namespace.
To create a namespace, we use the namespace keyword, followed by the identifier and
then the code block:
namespace example_namespace {
// code goes here
}
To access an identifier inside a namespace, we prepend the name of the namespace to
the name of the function.
Namespaces can be nested as well. Simply use the same declaration as before inside the
namespace:
namespace parent {
namespace child {
// code goes here
}
}
To access an identifier inside a namespace, you prepend the name of the identifier with
the name of the namespace in which it is declared, followed by ::.
You might have noticed that, before we were using std::cout. This is because the C++
standard library defines the std namespace and we were accessing the variable named
cout.
Namespaces | 77
To access an identifier inside multiple namespaces, you can prepend the list of all the
namespaces separated by :: – parent::child::some_identifier. We can access names in
the global scope by prepending :: to the name—::name_in_global_scope.
If we were to only use cout, the compiler would have told us that the name does not
exist in the current scope.
This is because the compiler searches only in the current namespace and the parent
namespaces to find an identifier by default, so unless we specify the std namespace, the
compiler will not search in it.
C++ helps make this more ergonomic with the help of the using declaration.
The using declaration is defined by the using keyword, followed by an identifier
specified with its namespaces.
For example, using std::cout; is a using declaration that declares that we want to use
cout. When we want to use all the declarations from a namespace, we can write using
namespace namespace_name;. For example, if we want to use every name defined in the
std namespace, we would write: using namespace std;.
When a name is declared inside the using declaration, the compiler also looks for that
name when looking for an identifier.
This means that, in our code, we can use cout and the compiler will find std::cout.
A using declaration is valid as long as we are in the scope in which it is declared.
Note
To better organize your code and avoid naming conflicts, you should always put
your code inside a namespace that's specific to either your application or library.
Namespaces can also be used to specify that some code is used only by the
current code.
Let's imagine you have a file called a.cpp that contains int default_name = 0; and
another file called b.cpp with int default_name = 1;. When you compile the two files
and link them together, we get an invalid program: the same variable has been declared
with two different values, and this violates the One Definition Rule (ODR).
78 | Functions
But you never meant for those to be the same variable. To you, they were some
variables that you just wanted to use inside your .cpp file.
To tell that to the compiler, you can use anonymous namespaces: a namespace with no
identifier.
All the identifiers created inside it will be private to the current translation unit
(normally the .cpp file).
How can you access an identifier inside an anonymous namespace? You can access the
identifier directly, without the need to use the namespace name, which does not exist,
or the using declaration.
Note
You should only use anonymous namespaces in .cpp files.
Note
The solution for this activity can be found on page 285.
Function Overloading | 79
Function Overloading
We saw how C++ allows us to write a function that takes parameters either by value or
by reference, using const, and organizes them in namespaces.
There is an additional powerful feature of C++ that allows us to give the same name
to functions that perform the same conceptual operation on different types: function
overloading.
Function overloading is the ability to declare several functions with the same name –
that is, if the set of parameters they accept is different.
An example of this is the multiply function. We can imagine this function being defined
for integers and floats, or even for vectors and matrices.
If the concept represented by the function is the same, we can provide several
functions that accept different kinds of parameters.
When a function is invoked, the compiler looks at all the functions with that name,
called the overload set, and picks the function that is the best match for the arguments
provided.
The precise rule on how the function is selected is complex, but the behavior is often
intuitive: the compiler looks for the better match between the arguments and the
expected parameters of the function. If we have two functions, int increment(int)
and float increment(float), and we call them with increment(1), the integer overload
is selected because an integer is a better match to an integer than a float, even if an
integer can be converted into a float. An example of this would be:
bool isSafeHeightForRollercoaster(int heightInCm) {
return heightInCm > 100 && heightInCm < 210;
}
Thanks to this feature, the calling code does not need to worry about which overload
of the function the compiler is going to select, and the code can be more expressive
thanks to using the same function to express the same meaning.
Note
The solution for this activity can be found on page 286.
Summary | 81
Make sure that the functions are well-organized by grouping them together.
Remember that the distance between two points is computed as the square root of
(x2-x1)^2 + (y2-y1)^2 + (z2-z1)^2.
C++ offers the std::pow function for the power function, which takes the base and the
exponent, and the std::sqrt function, which takes the number to square. Both are in
the cmath header.
Summary
In this chapter, we saw the powerful features C++ offers to implement functions.
We started by discussing why functions are useful and what they can be used for, and
then we dove into how to declare and define them.
We analyzed different ways of accepting parameters and returning values, how to make
use of local variables, and then explored how to improve the safety and convenience of
calling them with const and default arguments.
Finally, we saw how functions can be organized in namespaces and the ability to give
the same name to different functions that implement the same concept, making the
calling code not have to think about which version to call.
In the next chapter, we will look at how to create classes and how they are used in C++
to make building complex programs easy and safe.
3
Classes
Lesson Objectives
• Overload operators
• Implement functors
Introduction
In the previous chapter, we saw how we can use functions to combine basic operations
into units with a clear meaning. Additionally, in the first chapter, we saw how, in C++, we
can store data in basic types, such as integers, chars, and floats.
In this chapter, we will be covering how to define and declare classes and how to access
member functions of a class. We will explore what member and friend functions are and
how to use each in a program. Later in the chapter, we will look at how constructors
and destructors work. At the end of the chapter, we will explore functors and how you
can use them in your programs.
In our example, we can create a class to represent GPS coordinates. The data will
be the two float variables to describe latitude and longitude.
Examples of operations could be ways to compute distances between coordinates,
or to check whether a coordinate is inside a specific state. The programmer will
directly operate on the class and will not have to interact with the two float
variables that are used to represent it.
• Information hiding: The process of exposing a set of functionalities to the user of
the class while hiding the details of how they are implemented in the class.
This approach reduces the complexity of interacting with the class and makes it
easier to update the class implementation in the future:
Figure 2.1: The class exposes functionality that the user code uses directly, hiding the fact that it is
implemented with two floats
We discussed the fact that we can represent GPS coordinates as latitude and
longitude. Later, we might decide to represent a coordinate as the distance
from the North Pole. Thanks to information hiding, we can change how a class
is implemented and the users of the class will not be impacted, since we do not
change the functionality offered by the class:
Figure 2.2: The implementation of the class changes, but since it is hidden from the user and the
functionality was not changed, the user does not have to change how their code interacts with it
86 | Classes
The set of functionalities the class exposes to the users is normally referred to as
the public interface.
Note
Changing the implementation of a class is generally more convenient than to
changing the interface of a class, which requires you to change all the users of the
class to adapt to the new interface. Getting the design of the public interface of a
class right is the first step to creating a class that is easy to use and requires low
maintenance.
• Encapsulation: This is the principle of grouping the data and the operations we
can perform on it together. Since the data is hidden in the class, the user cannot
access or operate on it. The class must provide functionality to interact with it.
C++ enables encapsulation by letting the user put the operations to interact with
a class and the data that is used to implement such operations in the same unit:
class.
Let's explore the structure of a class in C++ and the information associated with it. The
following is the basic structure of a class:
class ClassName {
// class body
};
Note
It is common to forget the last semicolon after closing curly brackets. Always make
sure that you add it.
Access specifiers followed by a colon delimit an area in the class, and any member
defined in that area has the access specifier that precedes it. Here's the syntax:
class ClassName {
private:
int privateDataMember;
int privateMemberFunction();
protected:
float protectedDataMember;
float protectedMemberFunction();
public:
double publicDataMember;
double publicMemberFunction();
};
Note
By default, class members have the private access modifier.
88 | Classes
In C++, we can also use the struct keyword to define a class. A struct is identical to a
class, with the only exception that, by default, the access modifier is public, while for
the class it is private.
The following side-by-side code snippets are equivalent:
Figure 3.2: The difference between the code snippets of class and struct
Whether to use struct or class depends on convention used: usually, we use structs
when we want a collection of data members that should be accessible from anywhere
in the code; on the other hand, we use classes when we are modelling a more complex
concept.
We have learned how to define a class. Now, let's understand how to use one in a
program.
A class defines a blueprint or the design of an object. Like a blueprint, we can create
multiple objects from the same class. These objects are called instances.
We can create an instance in the same way that we create any basic type: define the
type of the variable followed by the name of the variable. Let's explore the following
example.
class Coordinates {
public:
float latitude;
float longitude;
On the other hand, when we are writing the body of a class method, we are inside
the class's scope. This means that we can access the other members of the class by
using their names directly, without having to use the dot operator. The members of the
current instance are going to be used.
Let's assume that the distance method is implemented as follows:
float Coordinates::distance(const Coordinates& other_coordinate) {
return pythagorean_distance(latitude, longitude, other_coodinate.latitude,
other_coordinate.longitude);
}
When we call newYorkPosition.distance(tokyoPosition);, the distance method is
called on the newYorkPosition instance. This means that latitude and longitude in the
distance method refer to newYorkPosition.latitude and newYorkPosition.longitude,
while other_coordinate.latitude refers to tokyoPosition.latitude.
If we had called tokyoPosition.distance(newYorkPosition); instead, the current
instance would have been tokyoPosition, and latitude and longitude would have
referred to the tokyoPosition, and other_coordinate to newYorkPosition.
90 | Classes
Static Members
In the previous section, we learned that a class defines the fields and methods that
compose an object. It is like a blueprint, specifying what the object looks like, but it
does not actually build it. An instance is the object that's built from the blueprint that's
defined by the class. Instances contain data and we can operate on instances.
Imagine the blueprint of a car. It specifies the engine of the car and that the car will
have four wheels. The blueprint is the class of the car, but we cannot turn on and drive
a blueprint. A car that's built by following the blueprint is an instance of the class.
The built car has four wheels and an engine, and we can drive it. In the same way, an
instance of a class contains the fields that are specified by the class.
This means that the value of each field is connected to a specific instance of a class
and evolves independently from the fields of all the other instances. At the same time,
it also means that a field cannot exist without the associated instance: there would be
no object that would be able to provide the storage (the space in memory) to store the
value of the field!
However, sometimes, we want to share the same value across all instances. In those
cases, we can associate the field with the class instead of the instance by creating a
static field. Let's examine the following syntax:
class ClassName {
static Type memberName;
};
There will be only one memberName field, which is shared across all instances. Like any
variable in C++, memberName needs to be stored in memory. We cannot use the storage
of the instance object, since memberName is not associated with any specific instance.
memberName is stored in a similar way to a global variable.
Outside of the class in which the static variable is declared, in a .cpp file, we can define
the value of the static variable. The syntax to initialize the value is as follows:
Type ClassName::memberName = value;
Note
Note that we do not repeat the static keyword.
It is important to define the values of the static variables in the .cpp file. If we
define them inside the header file, the definition will be included anywhere inside
the header, which will create multiple definitions, and the linker will complain.
Declaring and Defining a Class | 91
A class static variable's lifetime lasts for the complete duration of the program, like
global variables.
Let's see an example of how a static field in a class can be defined in the header and
how to assign a value to it in the .cpp file:
// In the .h file
class Coordinates {
// Data member
float latitude_ = 0;
// Data member
float longitude_ = 0;
public:
// Static data member declaration
static const Coordinates hearthCenter;
When accessing the members of an instance, we learned to use the dot operator.
92 | Classes
When accessing a static member, we might not have an instance to use the dot operator
on. C++ gives us the ability to access the static members of a class by using the scope
resolution operator, which is, a double colon (::), after the class name.
Note
Always use const when declaring a static field. Any instance can access the static
fields of its class; if they are mutable, it becomes extremely hard to track down
which instances is modifying the value. In programs that use multiple threads, it is
common to create bugs by modifying the static fields from different threads at the
same time.
Let's examine the following exercise to understand how static variables work.
Note
Static methods can only call other static methods and static fields inside a class.
Member Functions
Member functions are functions that are used to manipulate the data members of a
class, and they define the properties and behavior of the objects of the class.
Declaring a member function is just a matter of declaring a function inside the body of a
class. Let's examine the following syntax:
class Car
{
public:
void turnOn() {}
};
94 | Classes
Member functions, like the data members of a class, can be accessed using the dot (.)
operator that's applied on the object:
Car car;
car.turnOn();
Let's understand how to declare a member function outside the class scope.
void Car::turnOn() {}
In addition to the overloading rules that we learned in the previous chapter that
member functions can be overloaded in their const-ness, which means that two
functions can have identical signatures except for one being const and the other not.
The const member function will be called when an object is declared const; otherwise,
the non-const function is called. Let's examine the following code:
class Car
{
std::string& getColor() {}
const std::string& getColor() const {}
};
Car car;
// Call std::string& getColor()
car.getColor();
const Car constCar;
// Call const Color& getColor() const
constCar.getColor();
Note
It is important to distinguish between a const function and a function returning a
const type. Both make use of the same const keyword, but in different places in
the function prototype. They express a different concept and are independent.
void setColorToRed()
{
this->color = "Red";
// explicit use of this
}
void setColorToBlue()
{
color = "Blue";
// same as this->color = "Blue";
}
};
Note
pointer->member is a convenient way to access the member of the struct pointed
by pointer. It is equivalent to (*pointer).member.
Member Functions | 97
Exercise 8: Creating a Program Using the this Keyword to Greet New Users
Let's write a program that asks users for their names and greets them with a welcoming
message:
1. First, include the required header files.
2. Then, add the following functions to print the required output:
class PrintName {
std::string name;
};
3. Now, let's complete the program with a closing message using the this keyword.
Define the following methods inside the previous class:
public:
void set_name(const std::string &name){
this->name = name;
}
void print_name() {
std::cout << this->name << "! Welcome to the C++ community :)" <<
std::endl;
}
4. Write the main function, as follows:
int main()
{
PrintName object;
object.set_name("Marco");
object.print_name();
}
The output is as follows:
Marco! Welcome to the C++ community :)
Note
A function argument that has the same name as a data member of a class can
shadow its visibility. In this case, the this keyword is required for disambiguation.
98 | Classes
The print function writes the radius of the circle on the given stream, which is most
commonly the standard output.
Note
The set_latitude and set_longitude operations are used to set the x and y
coordinates (also referred to as setters), while get_latitude and get_longitude
are used to retrieve them (sometimes called getters).
Constructors and Destructors | 99
Performing encapsulation using the member functions through getter and setters.
To perform this activity, follow these steps:
1. Define a class with the name Coordinates, with its members under a private
access specifier.
2. Add the four operations previously specified and make them publicly accessible by
preceding their declaration by the public access specifier.
3. The setters (set_latitude and set_longitude) should take a float as a parameter
and return void, while the getters do not take any parameters and return a float.
4. The four methods should now be implemented. The setters assign the given value
to the corresponding member they are supposed to set; the getters return the
values that are stored.
Note
The solution for this activity can be found on page 288.
Rectangle rectangle;
This line will print a random value because we never set the value of int. The C++ rule
for the initialization of basic types is that they get non-specified values.
Note
In some situations, the values of variables are set to 0 when they are not initialized.
This might happen because of some details in the implementation of the operating
system, the standard library, or the compiler, and the C++ standard does not
guarantee it. A program will have strange bugs when it relies on this behavior,
since it is unpredictable when variables are initialized to 0. Always explicitly
initialize variables with basic types.
Constructors
The way to initialize data members is by using a constructor. A constructor is a special
member function that has the same name as the class and no return type, and it is called
automatically by the compiler when a new object of the class is created.
Like any other function, a constructor can accept parameters and has a function body.
We can invoke a constructor by adding a parameter list after the name of the variable:
Rectangle rectangle(parameter1, paramter2, ..., parameterN);
When there are no parameters, we can avoid using parentheses, which is what we did in
the previous example.
An example of a constructor with no parameters for the Rectangle struct would look as
follows:
struct Rectangle {
int height, width;
Rectangle() {
height = 0;
width = 0;
}
};
Note
When the only operation the constructor does is initialize the data members, opt
for using the initialization list, which we will show you later in this chapter.
Constructors and Destructors | 101
The constructor does not only initialize the data members but also ensure that the class
respects the invariant. After the constructor is executed, the invariant must be true.
Note
The concept of an invariant is not specific to the C++ language, and there is no
dedicated facility to specify the invariant of a class. A best practice is to document
the expected invariant of the class together with the class code so that the
developers working with the class can easily check what the expected invariant is
and make sure they respect it.
Using assertions in code also helps in identifying when the invariant is not respected.
This probably means there is a bug in the code.
Overloading Constructor
Similar to other functions, we can overload the constructor by accepting different
parameters. This is useful when an object can be created in several ways, since the user
can create the object by providing the expected parameter, and the correct constructor
is going to be called.
We showed an example of a default constructor for the Rectangle class earlier in this
chapter. If we want to add a constructor that creates a rectangle from a square, we
could add the following constructor to the Rectangle class:
class Rectangle {
public:
Rectangle(); // as before
Rectangle (Square square);
};
The second constructor is an overloaded constructor and will be invoked according to
the way the class object is initialized.
Constructors and Destructors | 103
In the following example, the first line will call the constructor with empty parameters,
while the second line will call the overloaded constructor:
Rectangle obj; // Calls the first constructor
Rectangle obj(square); // Calls the second overloaded constructor
Note
A constructor with a single non-default parameter is also called a converting
constructor. This kind of constructor specifies an implicit conversion, from the
type of the argument to the class type.
int main() {
Square square;
use_rectangle(square);
}
When calling use_rectangle, the compiler creates a new object of type Rectangle by
calling the conversion constructor, which accepts a Square.
One way to avoid this is to use the explicit specifier before the constructor definition:
explicit class_name(type arg) {}
When we use try to use Square to call a function that takes ExplicitRectangle, we get an
error:
void use_explicit_rectangle(ExplicitRectangle rectangle);
int main() {
Square square;
use_explicit_rectangle(square); // Error!
}
Note how, in this last case, the constructor does nothing other than initialize its
members. Hence, it has an empty function body.
Now, if we try to print the width and the height of the Rectangle object, we will notice
that they are correctly initialized to 0:
Rectangle rectangle;
std::cout << "Width: " << rectangle.width << std::endl; // 0
std::cout << "Height: " << rectangle.height << std::endl; // 0
Constructors and Destructors | 105
Initializer lists are the recommended way to initialize member variables in C++, and they
are necessary when a data member is const.
When using an initializer list, the order in which the members are constructed is the
one in which they are declared inside the class; not the one in which they appear in the
initializer list. Let's look at the following example:
class Example {
Example() : second(0), first(0) {}
int first;
int second;
};
When calling the default constructor of the Example class, the first method will be
initialized first, and the second method after it, even if they appear in a different order in
the initializer list.
Note
You should always write the members in the initializer list in the same order as
they are declared; compilers will help you by warning you when the order differs
from the expected one.
Note
We will talk about base classes and virtual functions in chapter 6.
106 | Classes
These types of classes can be initialized, even though they do not have a constructor, by
using a brace-enclosed comma-separated list of initializer-clauses, as shown here:
struct Rectangle {
int length;
int width;
};
Destructors
A destructor function is called automatically when the object goes out of scope and is
used to destroy objects of its class type.
Destructors have the same name as the class preceded by a tilde (~) and do not take any
argument nor return any value (not even void). Let's examine the following example:
class class_name {
public:
class_name() {} // constructor
~class_name() {} // destructor
};
After executing the body of the destructor and destroying any automatic objects
allocated within the body, a destructor for a class calls the destructors for all the
direct members of the class. Data members are destroyed in reverse order of their
construction.
Constructors and Destructors | 107
~Coordinates(){
std::cout << "Destructor called!" << std::endl;
}
};
3. In the main function, add the following code:
int main()
{
Coordinates c;
// Constructor called!
// Destructor called!
}
The output is as follows:
Constructor called!
Destructor called!
108 | Classes
Note
The default constructor might not initialize data members. Classes that have
members of a built-in or compound type should ordinarily either initialize those
members inside the class or define their version of the default constructor.
4. Alice can now use this Coordinates class to represent 2D positions on the map.
Note
The solution for this activity can be found on page 289.
Note
C++, like many languages, represents input/output operations as streams, where
data can be written to or read from.
The constructor of the class opens the file into a provided stream, while the destructor
closes it:
class file_handle {
public:
file_handle(ofstream& stream, const char* filepath) : _stream(stream) {
_stream.open(filepath);
}
~file_handle {
_stream.close();
110 | Classes
}
private:
ofstream& _stream;
};
To open the file, it is sufficient to provide the file path to the file_handle class. Then,
for the entire lifetime of the file_handle object, the file will not be closed. Once the
object reaches the end of the scope, the file is closed:
ofstream stream;
{
file_handle myfile(stream, "Some path"); // file is opened
do_something_with_file(stream);
} // file is closed here
Even though the benefit provided by applying the RAII idiom seems to be just to reduce
code, the real improvement is having safer code. It is common for a programmer to
write a function that correctly opens a file but never closes it or allocates memory that
never gets destroyed.
RAII makes sure that these operations cannot be forgotten, as it automatically handles
them.
Nested Class Declarations | 111
Note
The solution for this activity can be found on page 290.
To access a nested class, we can use the double colon (::), similar to accessing static
members of the outer class. Let's examine the following example:
// Declaration
class Coordinate {
...
struct CoordinateDistance {
float x = 0;
float y = 0;
static float walkingDistance(CoordinateDistance distance);
}
};
// Create an instance of the nested class CoordinateDistance
Coordinate::CoordinateDistance distance;
/* Invoke the static method walkingDistance declared inside the nested class
CoordinateDistance */
Coordinate::CoordinateDistance::walkingDistance(distance);
Nested classes are useful for two main reasons:
• When implementing a class, we need an object that manages some of the
logic of the class. In such cases, the nested class is usually private, and is not
exposed through the public interface of the class. It is mostly used to ease the
implementation of the class.
• When designing the functionality of a class, we want to provide a different class,
closely related to the original one, which provides part of that functionality. In that
case, the class is accessible by the users of the class and is usually an important
part of the interaction with the class.
Imagine a list – a sequence of objects. We would like the user to be able to iterate over
the items contained in the list. To do so, we need to keep track of which items the
user has already iterated over and which are remaining. This is typically done with an
iterator, which is a nested class. The iterator is an integral part of interacting with the
List class.
We will look at iterators more in detail in Lesson 5, Standard Library Containers and
Algorithms.
Friend Specifier | 113
Friend Specifier
As we have already seen, private and protected members of a class are not accessible
from within other functions and classes. A class can declare another function or class as
a friend: this function or class will have access to the private and protected members of
the class which declares the friend relationship.
The user has to specify the friend declaration within the body of the class.
Friend Functions
Friend functions are non-member functions that are entitled to access the private and
protected members of a class. The way to declare a function as a friend function is by
adding its declaration within the class and preceding it by the friend keyword. Let's
examine the following code:
class class_name {
type_1 member_1;
type_2 member_2;
public:
friend void print(const class_name &obj);
};
Friend Classes
Similarly, like a friend function, a class can also be made a friend of another class by
using the friend keyword.
114 | Classes
Declaring a class as a friend is like declaring all of its methods as friend functions.
Note
Friendship is not mutual. If a class is a friend of another, then the opposite is not
automatically true.
The following code demonstrates the concept of how friendship is not mutual:
class A {
friend class B;
int a = 0;
};
class B {
friend class C;
int b = 0;
};
class C {
int c = 0;
Friendship is not transitive; so, in the previous example, class C is not a friend of class A,
and the methods of class C cannot access the protected or private members of class A.
Additionally, A cannot access B's private members, since B is a friend of A, but friendship
is not mutual.
Friend Specifier | 115
4. The Apple object can now be constructed using the following code:
AppleTree tree;
Apple apple = tree.createFruit();
Note
The solution for this activity can be found on page 291.
private:
type member;
};
A copy constructor is declared implicitly by the compiler when the class definition
does not explicitly declare a copy constructor and all the data members have a copy
constructor. This implicit copy constructor performs a copy of the class members in the
same order of initialization.
118 | Classes
struct B {
B() {}
B(const B& a) {
std::cout << "Copy construct B" << std::endl;
}
};
class C {
A a;
B b;
// The copy constructor is implicitly generated
};
int main() {
C first;
C second(first);
// Prints: "Copy construct A", "Copy construct B"
}
Copy Constructors and Assignment Operators | 119
When C is copy constructed, the members are copied in order: first, a is copied and
then b is copied. To copy A and B, the compiler calls the copy constructor defined in
those classes.
Note
When a pointer is copied, we are not copying the object pointed to, but simply the
address at which the object is located.
This means that when a class contains a pointer as a data member, the implicit
copy constructor only copies the pointer and not the pointed object, so the copied
object and the original one will share the object that's pointed to by the pointer.
This is sometimes called a shallow copy.
private:
type member;
};
120 | Classes
Also, for the copy assignment operator, the compiler generates an implicit one when it
is not explicitly declared. As for the copy constructor, the members are copied in the
same order of initialization.
In the following example, the copy constructor and the copy assignment operator will
output a sentence when they are called:
class class_name {
public:
class_name(const class_name& other) : member(other.member){
std::cout << "Copy constructor called!" << std::endl;
}
private:
type member;
};
The following code shows two ways of copying an object. The former uses the
copy constructor, while the latter uses the copy assignment operator. The two
implementations will print a sentence when they are called:
class_name obj;
class_name other_obj1(obj);
\\ prints "Copy constructor called!"
Note
For clarity, we can briefly describe an rvalue reference (formed by placing an &&
operator after the type of the function argument) as a value that does not have a
memory address and does not persist beyond a single expression, for example, a
temporary object.
A move constructor and a move assignment operator enable the resources owned by an
rvalue object to be moved into an lvalue without copying.
When we move a construct or assign a source object to a destination object, we transfer
the content of the source object into the destination object, but the source object needs
to remain valid. To do so, when implementing such methods, it is fundamental to reset
the data members of the source object to a valid value. This is necessary to prevent the
destructor from freeing the resources (such as memory) of the class multiple times.
Let's assume that there is a Resource that can be acquired, released, reset, and checked
if it's reset.
Here is an example of a WrongMove constructor:
class WrongMove {
public:
WrongMove() : _resource(acquire_resource()) {}
WrongMove(WrongMove&& other) {
_resource = other._resource;
122 | Classes
~WrongMove() {
if (not is_reset_resource(_resource)) {
release_resource(_resource);
}
}
private:
Resource _resource;
}
The move-constructor of the WrongMove class will release the resource twice:
{
WrongMove first;
// Acquires the resource
{
/* Call the move constructor: we copy the resource to second, but we are
not resetting it in first */
WrongMove second(std::move(first));
}
/* Second is destroyed: second._resource is released here. Since we copied
the resource, now first._resource has been released as well. */
}
// First is destroyed: the same resource is released again! Error!
Instead, the move constructor should have reset the _resource member of other, so that
the destructor would not call release_resource again:
WrongMove(WrongMove&& other) {
_resource = other._resource;
other._resource = resetted_resource();
}
Copy Constructors and Assignment Operators | 123
The move constructor and move assignment operator can be implicitly generated by
the compiler if no user-defined ones are provided and there are no user-declared
destructors, copy constructors, or copy or move assignment operators:
struct MovableClass {
MovableClass(MovableClass&& other) {
std::cout << "Move construct" << std::endl;
}
MovableClass first;
// Move construct
MovableClass second = std::move(first);
// Or: MovableClass second(std::move(first));
MovableClass third;
// Move assignment
second = std::move(third);
Operator Overloading
C++ classes represent user-defined types. So, the need arises to be able to operate with
these types in a different way. Some operator functions may have a different meaning
when operating on different types. Operator overloading lets you define the meaning of
an operator when applied to a class type object.
For example, the + operator applied to numerical types is different than when it is
applied to the following Point class, which is constituted of coordinates. The language
cannot specify what the + operator should do for user-defined types such as Point,
as it is not in control of such types and does not know what the expected behavior is.
Because of that, the language does not define the operators for user-defined types.
However, C++ allows the user to specify the behavior of most operators for user-defined
types, including classes.
Here is an example of the + operator, defined for the Point class:
class Point
{
Point operator+(const Point &other)
{
Point new_point;
new_point.x = x + other.x;
new_point.y = y + other.y;
return new_point;
Operator Overloading | 125
}
private:
int x;
int y;
}
Here is a list of all the operators that can and cannot be overloaded:
• The following are the operators that can be overloaded:
Operators that expect two operands are called binary operators. Examples are +, -, *,
and /.
A method overloading a binary operator needs to accept a single parameter. When the
compiler encounters the use of the operator, it will call the method on the variable
on the left-hand side of the operator, while the variable on the right-hand side will be
passed as parameter to the method.
126 | Classes
We saw in the previous example that Point defines the + operator, which takes a
parameter. When using the addition operation on a Point, the code would look like this:
Point first;
Point second;
Point sum = first + second;
The last line from the code example is equivalent to writing the following:
Point sum = first.operator+(second);
The compiler automatically rewrites the first expression to the second one.
Operators that expect only one operand are called unary operators. Examples are --,
++, and !.
A method overloading a unary operator must not accept any parameters. When the
compiler encounters the use of the operator, it will call the method on the variable to
which the operator is assigned.
As an example, let's say we are given an object that's defined as follows:
class ClassOverloadingNotOperator {
public:
bool condition = false;
ClassOverloadingNotOperator& operator!() {
condition = !condition;
}
};
We would write the following:
ClassOverloadingNotOperator object;
!object;
Operator Overloading | 127
Note
Operator overloading is possible in two ways: either as a member function or as a
non-member function. The two end up producing the same effect.
3. Since, in our example, p_1.x is initialized to 1 and p_2.x to 2, the result of the
comparison will be true, which indicates that p_1 comes earlier than p_2 in the
order.
Note
The solution for this activity can be found on page 293.
Introducing Functors
A Functor (function object) is similar to a class. The class that overloads the operator()
function is also known as the function call operator.
The syntax that's used to define a functor is as follows:
class class_name {
public:
type operator()(type arg) {}
};
The function call operator has a return type and takes any number of arguments of
any type. To invoke the call operator of an object, we can write the name of the object,
followed by parentheses containing the arguments to pass to the operator. You can
imagine that an object that provides a call operator can be used in the same way as you
would use a function. Here's an example of a functor:
class_name obj;
type t;
/* obj is an instance of a class with the call operator: it can be used as
if it was a function */
obj(t);
They are particularly useful in places where you can pass a function object to an
algorithmic template that accepts an object with operator() defined. This exploits code
reusability and testability. We will see more on this in chapter 5 when we talk about
lambda.
Introducing Functors | 129
The following is a simple example of a functor that prints a string before appending a
new line at the end of it:
class logger{
public:
void operator()(const std::string &s) {
std::cout << s << std::endl;
}
};
logger log;
log ("Hello world!");
log("Keep learning C++");
private:
int value;
};
AddX add_five(5);
std::cout << add_five(4) << std::endl; // prints 9
Note
The solution for this activity can be found on page 294.
Summary
In this chapter, we saw how the concept of classes can be used in C++. We started by
delineating the advantages of using classes, describing how they can help us to create
powerful abstractions.
We outlined the access modifiers a class can use to control who has access to class
fields and methods.
We continued by exploring the conceptual differences between a class and its
instances, along with the implications this has when implementing static fields and
static methods.
We saw how constructors are used to initialize classes and their members, while
destructors are used to clean up the resources that are managed by a class.
We then explored how constructors and destructors can be combined to implement the
fundamental paradigm C++ is famous for: RAII. We showed how RAII makes it easy to
create classes that handle resources and make programs safer and easier to work with.
Finally, we introduced the concept of operator overloading and how it can be used to
create classes that are as easy to use as built-in types.
In the next chapter, we'll focus on templates. We'll primarily look at how to implement
template functions and classes, and write code that works for multiple types.
Generic Programming
4
and Templates
Lesson Objectives
In this chapter, you will learn how to use templates effectively in your program.
134 | Generic Programming and Templates
Introduction
When programming, it is common to face problems that are recurring for different
types of objects, such as storing a list of objects, or searching elements in a list, or
finding the maximum between two elements.
Let's say that in our program we want to be able to find the maximum between two
elements, either integers or doubles. With the features we have learned so far, we could
write the following code:
int max(int a, int b) {
if ( a > b) return a;
else return b;
}
In the previous code, the two functions are identical except for the types of the
parameters and the return type. Ideally, we would like to write these kind of operations
only once and reuse them in the entire program.
Moreover, our max() function can only be called with types for which an overload exists:
int and double in this case. If we wanted it to work with any numerical type, we would
need to write an overload for each of the numerical types: we would need to know in
advance about all the types that will be used to call it, especially when the function
is part of a library that is intended to be used by other developers, as it becomes
impossible for us to know the types that will be used when calling the function.
We can see that there is nothing specific to integers being required to find the
maximum elements; if the elements implement operator<, then it is possible to find the
greater of the two numbers, and the algorithm does not change. In these situations, C++
offers an effective tool—templates.
Templates | 135
Templates
Templates are a way to define functions or classes that can work for many different
types, while still writing them only once.
They do so by having special kinds of parameters—type parameters.
When writing the template code, we can use this type parameter as if it were a real
type, such as int or string.
When the templated function is called or the template class is instantiated, the type
parameter is substituted with the real type that's used by the calling code.
Now let's look at an example of a template in C++ code:
template<typename T>
T max(T a, T b) {
if(a>b) {
return a;
} else {
return b;
}
}
A template always starts with the template keyword, followed by the list of template
parameters enclosed in angle brackets.
A template parameter list is a list of comma-separated parameters. In this case, we only
have one—typename T.
The typename keyword tells the template that we are writing a templated function that
uses a generic type, which we are going to name T.
Note
You can also use the class keyword in place of typename, since there is no
difference between them.
136 | Generic Programming and Templates
Then, the definition of the function follows. In the function definition, we can use the
name T when we want to refer to the generic type.
To call the template, we specify the name of the template, followed by the list of types
we want to use as type arguments, enclosed in angle brackets:
max<int>(10, 15);
This calls the templated function max, specifying int as the type parameter. We say that
we instantiated the templated function max with type int, and then called that instance.
We do not always need to specify the type parameters of a template; the compiler can
deduce them from the calling code. A later section will describe this feature.
Because of how powerful templates are, the big part of the C++ standard library is based
on templates, as we will see in Chapter 5, Standard Library Containers and Algorithms.
Now we'll explore in depth what happens when we compile the code that contains
templates.
For example: when we call max<int>(1,2), the compiler looks at the template definition
we specified earlier and generates code as if we wrote the following:
int max(int a, int b) {
if(a>b) {
return a;
} else {
return b;
}
}
Note
Since the compiler generates the code from the template definition, it means that
the full definitions need to be visible to the calling code, not only the declaration,
as was the case for functions and classes.
The template can still be forward declared, but the compiler must also see the
definition. Because of this, when writing templates that should be accessed by several
files, both the definition and the declaration of the templates must be in the header file.
This restriction does not apply if the template is used only in one file.
Exercise 11: Finding the Bank Account of the User with the Highest Balance
Write a template function that accepts details of two bank accounts (of the same type)
and returns the balance of the bank account with the highest balance.
For this exercise, perform the following steps:
1. Let's create two structs named EUBankAccount and UKBankAccount to represent the
European Union bank account and the United Kingdom bank account with the
required basic information, as shown in the following code:
#include <string>
struct EUBankAccount {
std::string IBAN;
int amount;
};
138 | Generic Programming and Templates
struct UKBankAccount {
std::string sortNumber;
std::string accountNumber;
int amount;
};
2. The template function will have to compare the amount of the bank accounts. We
want to work with different bank account types, so we need to use a template:
template<typename BankAccount>
int getMaxAmount(const BankAccount& acc1, const BankAccount& acc2) {
// All bank accounts have an 'amount' field, so we can access it safely
if (acc1.amount > acc2.amount) {
return acc1.amount;
} else {
return acc2.amount;
}
}
3. Now, in the main function, call both the structs and the template function, as
shown here:
int main() {
EUBankAccount euAccount1{"IBAN1", 1000};
EUBankAccount euAccount2{"IBAN2", 2000};
std::cout << "The greater amount between EU accounts is " <<
getMaxAmount(euAccount1, euAccount2) << std::endl;
Here, we can see a template function that creates a new object from a different instance
of an object.
Since the function does not modify the original type, the function would like to accept
it as a const reference.
Since we are declaring the type T in the template, in the function definition we can
use the modifiers on the type to accept the parameter in the way we deem more
appropriate.
Notice that we used the type two times: once with some modifiers and once with no
modifiers.
This gives a lot of flexibility when using templates and writing functions, as we can
liberally modify the type to suit our needs.
Similarly, we have a lot of freedom in where we can use the template arguments.
Let's see two templates with a multiple template type argument:
template<typename A, typename B>
A transform(const B& b) {
return A(b);
}
140 | Generic Programming and Templates
We can then write the following function as main and compile the program:
int main() {
// do nothing
}
When we compile this program, the compilation ends successfully without any error.
Templates | 141
Note
We did not specify the type to the template. We will see later in this chapter when
the compiler can automatically deduce the types from the call.
Note how the error appeared when we used the template function, and that it was not
detected before.
The error is telling us that we tried to call the getAccount method on an integer, which
does not have such a method.
Why didn't the compiler tell us this when we were writing the template?
The reason for this is that the compiler does not know what type User will be; therefore,
it cannot tell whether the getAccount method will exist or not.
When we tried to use the template, we tried to generate the code with two specific
types, and the compiler checked that these two types were suitable for the template;
they were not, and the compiler gave us an error.
The types we used were not satisfying the requirements of the template types.
Unfortunately, there is no easy way in the current C++ standard, even the most recent
C++17, to specify the requirements of templates in the code—for that, we need good
documentation.
142 | Generic Programming and Templates
The template has two type arguments, so we can look at the requirements for each
type:
• User requirements: The User object must have a getAccount method
• Container requirements: The Container object must have a push_back method
The compiler finds the first problem when we call the getAccount() function and it
notifies us.
To solve this issue, let's declare a suitable class, as shown here:
struct Account {
// Some fields
};
class User {
public:
Account getAccount() const{
return Account();
}
};
Now, let's call the template with the help of the following code:
int main() {
std::string accounts;
User user;
populateAccountCollection(accounts, user);
}
We still get an error:
error: no matching function for call to 'std::__cxx11::basic_
string<char>::push_back(Account)'
This time, the error message is less clear, but the compiler is telling us that there is no
method called push_back that accepts an account in basic_string<char> (std::string is
an alias for it). The reason for this is that std::string has a method called push_back, but
it only accepts characters. Since we are calling it with an Account, it fails.
Templates | 143
Note
The std::vector type in the C++ standard library allows to store sequences of
elements of an arbitrary type. push_back is a method that's used for adding a
new element at the end of the vector. We will see more about vectors in Chapter 5,
Standard Library Containers and Algorithms.
Note
To make it easy to use our templates with many types, we should try to set the
least requirements we can on the types.
144 | Generic Programming and Templates
Function Template
In the previous section, we learned how function templates are written.
In this section, we will learn about the two features that were introduced by C++11 that
make it easier to write template functions. These two functions are trailing return types
and decltype.
Let's start with the decltype. The decltype is a keyword that accepts an expression and
returns the type of that expression. Let's examine the following code:
int x;
decltype(x) y;
In the previous code, y is declared as an integer, because we are using the type of the
expression x, which is int.
Any expression can be used inside decltype, even complex ones, for example:
User user;
decltype(user.getAccount()) account;
Let's look at the second feature—trailing return types.
We saw that a function definition starts with the return type, followed by the name of
the function and then the parameters. For example:
int max(int a, int b);
Starting from C++11, it is possible to use a trailing return type: specifying the return type
at the end of the function signature. The syntax to declare a function with a trailing
return type is to use the keyword auto, followed by the name of the function and the
parameters, and then by an arrow and the return type.
The following is an example of a trailing return type:
auto max(int a, int b) -> int;
This is not beneficial when writing regular functions, but it becomes useful when
writing templates and when combined with decltype.
Defining Function and Class Templates | 145
The reason for this is that decltype has access to the variables defined in the
parameters of the function, and the return type can be computed from them:
template<typename User>
auto getAccount(User user) -> decltype(user.getAccount());
This is an example of a forward declaration of a function template.
Note
When the user wants to provide a definition, it needs to provide the same template
declaration, followed by the body of the function.
Without the trailing return type, we would have to know what the type returned
by user.getAccount() is to use it as the return type of the getAccount() function.
The return type of user.getAccount() can be different depending on the type of the
template parameter User, which in turn means that the return type of the getAccount
function could change depending on the User type. With the trailing return type, we
don't need to know what type is returned by user.getAccount(), as it is determined
automatically. Even better, when different types are used in our function or a user
changes the return type of the getAccount method in one of the types that's used to
instantiate the template, our code will handle it automatically.
More recently, C++14 introduced the ability to simply specify auto in the function
declaration, without the need for the trailing return type:
auto max(int a, int b)
The return type is automatically deduced by the compiler, and to do so, the compiler
needs to see the definition of the function—we cannot forward declare functions that
return auto.
Additionally, auto always returns a value—it never returns a reference: this is something
to be aware of when using it, as we could unintentionally create copies of the returned
value.
One last useful feature of function templates is how to reference them without calling
them.
Up until now, we have only seen how to call the function templates, but C++ allows us to
pass functions as parameters as well. For example: when sorting a container, a custom
comparison function can be provided.
146 | Generic Programming and Templates
We know that a template is just a blueprint for a function, and the real function is going
to be created only when the template is instantiated. C++ allows us to instantiate the
template function even without calling it. We can do this by specifying the name of the
template function, followed by the template parameters, without adding the parameters
for the call.
Let's understand the following example:
template<typename T>
void sort(std::array<T, 5> array, bool (*function)(const T&, const T&));
The sort is a function that takes an array of five elements and a pointer to the function
to compare two elements:
template<typename T>
bool less(const T& a, const T& b) {
return a < b;
}
To call sort with an instance of the less template for integers, we would write the
following code:
int main() {
std::array<int, 5> array = {4,3,5,1,2};
sort(array, &less<int>);
}
Here, we take a pointer to the instance of less for integers. This is particularly useful
when using the Standard Template Library, which we will see in Chapter 5, Standard
Library Containers and Algorithms.
Class Templates
In the previous section, we learned how to write template functions. The syntax for
class templates is equivalent to the one for functions: first, there is the template
declaration, followed by the declaration of the class:
template<typename T>
class MyArray {
// As usual
};
Defining Function and Class Templates | 147
And equivalently to functions, to instantiate a class template, we use the angle brackets
containing a list of types:
MyArray<int> array;
Like functions, class template code gets generated when the template is instantiated,
and the same restrictions apply: the definition needs to be available to the compiler and
some of the error-checking is executed when the template is instantiated.
As we saw in Lesson 3, Classes, while writing the body of a class, the name of the class
is sometimes used with a special meaning. For example, the name of the constructor
functions must match the name of the class.
In the same way, when writing a class template, the name of the class can be used
directly, and it will refer to the specific template instance being created:
template<typename T>
class MyArray {
// There is no need to use MyArray<T> to refer to the class, MyArray
automatically refers to the current template instantiation
MyArray();
// Define the constructor for the current template T
MyArray<T>();
// This is not a valid constructor.
};
This makes writing template classes a similar experience to writing regular classes, with
the added benefit of being able to use the template parameters to make the class work
with generic types.
Like regular classes, template classes can have fields and methods. The field can depend
on the type declared by the template. Let's review the following code example:
template<typename T>
class MyArray {
T[] internal_array;
};
148 | Generic Programming and Templates
Also when writing methods, the class can use the type parameter of the class:
template<typename T>
class MyArray {
void push_back(const T& element);
};
Classes can also have templated methods. Templated methods are similar to template
functions, but they can access the class instance data.
Let's review the following example:
template<typename T>
class MyArray {
template<typename Comparator>
void sort (const Comparator & element);
};
The sort method will accept any type and will compile if the type satisfies all the
requirements that the method imposes on the type.
To call the method, the syntax follows the one for calling functions:
MyArray<int> array;
MyComparator comparator;
array.sort<MyComparator>(comparator);
Note
The method template can be part of a non-template class.
In these situations, the compiler can sometimes deduce the type of the parameter,
where the user does not have to specify it.
Defining Function and Class Templates | 149
If a method is only declared in the class, as we did in the example with sort, the user
can later implement it by specifying the template types of both the class and the
method:
template<typename T> // template of the class
template<typename Comparator> // template of the method
MyArray<T>::sort(const Comparator& element) {
// implementation
}
The name of the types does not have to match, but it is a good practice to be consistent
with the names.
Similar to methods, the class can also have templated overloaded operators. The
approach is identical to writing the operator overloads for regular classes, with the
difference that the declaration of a template must precede the overload declaration like
we saw for method templates.
Finally, something to be aware of is how static methods and static fields interact with
the class template.
We need to remember that the template is a guide on the code that will be generated
for the specific types. This means that when a template class declares a static member,
the member is shared only between the instantiations of the template with the same
template parameters:
template<typename T>
class MyArray {
const Static int element_size = sizeof(T);
};
MyArray<int> int_array1;
MyArray<int> int_array2;
MyArray<std::string> string_array;
150 | Generic Programming and Templates
int_array1 and int_array2 will share the same static variable, element_size, since
they are both of the same type: MyArray<int>. On the other hand, string_array has
a different one, because its class type is MyArray<std::string>. MyArray<int> and
MyArray<std::string>, even if generated from the same class template, are two different
classes, and thus do not share static fields.
Dependent Types
It's fairly common, especially for code that interacts with templates, to define some
public aliases to types.
A typical example would be the value_type type alias for containers, which specifies
the type contained:
template<typename T>
class MyArray {
public:
using value_type = T;
};
Why is this being done?
The reason for this is that if we are accepting a generic array as a template parameter,
we might want to find out the contained type.
If we were accepting a specific type, this problem would not arise. Since we know the
type of vector, we could write the following code:
void createOneAndAppend(std::vector<int>& container) {
int new_element{}; // We know the vector contains int
container.push_back(new_element);
}
But how can we do this when we accept any container that provides the push_back
method?
template<typename Container>
void createOneAndAppend(Container& container) {
// what type should new_element be?
container.push_back(new_element);
}
Defining Function and Class Templates | 151
We can access the type alias declared inside the container, which specifies which kind
of values it contains, and we use it to instantiate a new value:
template<typename Container>
void createOneAndAppend(Container& container) {
Container::value_type new_element;
container.push_back(new_element);
}
This code, unfortunately, does not compile.
The reason for this is that value_type is a dependent type. A dependent type is a type
that is derived from one of the template parameters.
When the compiler compiles this code, it notices that we are accessing the value_type
identifier in the Container class.
That could either be a static field or a type alias. The compiler cannot know when
it parses the template, since it does not know what the Container type will be and
whether it has a type alias or a static variable. Therefore, it assumes we are accessing
a static value. If this is the case, the syntax we are using is not valid, since we still have
new_element{} after access to the field.
To solve this issue, we can tell the compiler that we are accessing a type in the class,
and we do so by prepending the typename keyword to the type we are accessing:
template<typename Container>
void createOneAndAppend(Container& container) {
typename Container::value_type new_element{};
container.push_back(new_element);
}
class TcpConnection {
public:
std::array<char, 100> readNext() {
std::cout << "the data has been read" << std::endl;
return std::array<char, 100>{};
}
void writeNext(const std::array<char, 100>& blob) {
std::cout << "the data has been written" << std::endl;
}
};
2. The deserialize() static method takes an std::array of 100 characters
representing the object, and creates an object from it.
3. The connection objects are already provided. Create the header connection.h with
the following declarations:
template<typename Object, typename Connection>
Object readObjectFromConnection(Connection& con) {
std::array<char, 100> data = con.readNext();
return Object::deserialize(data);
}
Defining Function and Class Templates | 153
Note
The solution for this activity can be found on page 295.
template<typename Currency>
class UserAccount {
public:
Currency balance;
};
3. Our aim is to write an Account class that stores the current balance in any
currency provided by the template parameter.
4. The user account must provide a method called addToBalance that accepts any
kind of currency, and after converting it to the correct currency that's used for the
account, it should sum the value to the balance:
template<typename OtherCurrency>
void addToBalance(OtherCurrency& other) {
balance.value += to<Currency>(other).value;
}
5. The user now understands how to write class templates, how to instantiate them,
and how to call their templates.
Note
The solution for this activity can be found on page 296.
Here, the second parameter is a non-type template parameter, which represents the
size of the array.
The declaration of a non-type template parameter is in the parameter list of the
template, but instead of starting with a typename keyword such as the type parameters,
it starts with the type of the value, followed by the identifier.
There are strict restrictions on the types that are supported as non-type template
parameters: they must be of integral type.
Let's examine the following example of the declaration of a non-type template
parameter:
template<typename T, unsigned int size>
Array {
// Implementation
};
For example: here, we declared a class template that takes a type parameter and a
non-type parameter.
We already saw that functions can take parameters directly and classes can accept
parameters in the constructor. Additionally, the type of regular parameters is not
restricted to be an integral type.
What is the difference between template and non-template parameters? Why would we
use a non-type template parameter instead of a regular parameter?
The main difference is when the parameter is known to the program. Like all the
template parameters and unlike the non-template parameters, the value must be known
at compile time.
This is useful when we want to use the parameters in expressions that need to be
evaluated at compile time, as we do when declaring the size of an array.
The other advantage is that the compiler has access to the value when compiling the
code, so it can perform some computations during compilation, reducing the amount of
instruction to execute at runtime, thus making the program faster.
Additionally, knowing some values at compile time allows our program to perform
additional checks so that we can identify problems when we compile the program
instead of when the program is executed.
156 | Generic Programming and Templates
Note
The solution for this activity can be found on page 298.
Bonus step:
In games, multiplying a matrix by a vector is a common operation.
Add a method to the class that takes a std::array containing elements of the same type
of the matrix, and returns a std::array containing the result of the multiplication. See
the definition of a matrix-vector product at https://mathinsight.org/matrix_vector_
multiplication.
Bonus step:
We add a new method, multiply, which takes a std::array of type T with the length of C
by const reference, since we are not modifying it.
The function returns an array of the same type, but a length of R?
We follow the definition of the matrix-vector multiplication to compute the result:
std::array<T, R> multiply(const std::array<T, C>& vector){
std::array<T, R> result = {};
for(int r = 0; r < R; r++) {
for(int c = 0; c < C; c++) {
result[r] += get(r, c) * vector[c];
}
}
return result;
}
158 | Generic Programming and Templates
The reason to use the default arguments is to provide a sensible option for the
template, but still allowing the user to provide their own type or value when needed.
Let's see an example of type arguments:
template<typename T>
struct Less {
bool operator()(const T& a, const T& b) {
return a < b;
}
};
template<typename T, typename Comparator= Less<T>>
class SortedArray;
The hypothetical type SortedArray is an array that keeps its elements always sorted. It
accepts the type of the elements it should hold and a comparator. To make it easy to
use for the user, it sets the comparator to use the less operator by default.
The following code shows how a user can implement it:
SortedArray<int> sortedArray1;
SortedArrat<int, Greater<int>> sortedArray2;
We can also see an example of a default non-type template parameter:
template<size_t Size = 512>
struct MemoryBuffer;
The hypothetical type MemoryBuffer is an object that reserves an amount of memory on
the stack; the program will then allocate objects into that memory. By default, it uses
512 bytes of memory, but the user can specify a different size:
MemoryBuffer<> buffer1;
MemoryBuffer<1024> buffer2;
Note the empty angle brackets in the buffer1 declaration. They are needed to signal to
the compiler that we are making use of a template. This requirement has been removed
in C++17, and we can write MemoryBuffer buffer1;.
160 | Generic Programming and Templates
The error happens because we cannot bind a temporary value, like 1, to a non-const
reference.
As we can see, the compiler tries to deduce a type so that when it is substituted in the
parameter, it matches the argument as best as possible.
The compiler cannot always find such a type; in those situations, it gives an error and
it's up to the user to provide the type.
The compiler cannot deduce a type for any of the following reasons:
The type is not used in the parameters. For example: the compiler cannot deduce a type
if it is only used in the return type, or only used inside the body of the function.
The type in the parameter is a derived type. For example: template<typename T> void
foo(T::value_type a). The compiler cannot find the type T given the parameter that's
used to call the function.
Knowing these rules, we can derive a best practice for the order of the template
parameters when writing templates: the types that we expect the user to provide need
to come before the types that are deduced.
The reason for this is that a user can only provide the template arguments in the same
order they have been declared.
Let's consider the following template:
template<typename A, typename B, typename C>
C foo(A, B);
When calling foo(1, 2.23), the compiler can deduce A and B, but cannot deduce C. Since
we need all the types, and the user has to provide them in order, the user has to provide
all of the types: foo<int, double, and float>(1, 2.23);.
Let's say we put the types that cannot be deduced before the types that can be
deduced, as in the following example:
template< typename C, typename A, typename B>
C foo(A, B);
We could call the function with foo<float>(1, 2.23). We would then provide the type to
use for C and the compiler would automatically deduce A and B.
In a similar way, we need to reason about default template arguments.
Since they need to come last, we need to make sure to put the types that the user is
more likely to want to modify first, since that will force them to provide all the template
arguments up to that parameter.
162 | Generic Programming and Templates
Note
The solution for this activity can be found on page 300.
Similar examples would apply if you wanted to log before calling a function, or you
wanted to execute the function in a different thread, such as std::async does.
Let's demystify the difference by using the following code:
struct PrintOnCopyOrMove {
PrintOnCopyOrMove(std::string name) : _name(name) {}
PrintOnCopyOrMove(const PrintOnCopyOrMove& other) : _name(other._name) {
std::cout << "Copy: " << _name << std::endl; }
PrintOnCopyOrMove(PrintOnCopyOrMove&& other) : _name(other._name) {
std::cout << "Move: " << _name << std::endl; }
std::string _name;
};
void use_printoncopyormove_obj(PrintOnCopyOrMove obj) {}
Note
use_printoncopyormove_obj always accepts the parameter by value.
Forwarding references look like r-value references, but they only apply where the type
is deduced by the compiler:
void do_action(PrintOnCopyOrMove&&)
// not deduced: r-value reference
template<typename T>
void do_action(T&&) // deduced by the compiler: forwarding reference
Note
If you see a type identifier declared in the template, the type is deduced, and the
type has &&, then it is a forwarding reference.
Note
Let's say the type is not deduced, but, it is provided explicitly, for example:
int x = 0;
do_action<int>(x);
The advantage, as we saw before, is that we work with any kind of reference, and when
the calling code knows it can move the object, then we can make use of the additional
performance provided by the move constructor, but when a reference is preferred, then
the code can use it as well.
Additionally, some types do not support copying, and we can make our template work
with those types as well.
When we write the body of the template function, the parameter is used as an l-value
reference, and we can write code ignoring whether T is an l-value reference or an
r-value one:
template<typename T>
void do_action(T&& obj) { /* forwarding reference, but we can access obj as
if it was a normal l-value reference */
obj.some_method();
some_function(obj);
}
In Chapter 3, Classes, we learned that std::move can make our code more efficient when
we need to use an object that we are not going to access after the call happens.
But we saw that we should never move objects we receive as an l-value reference
parameter, since the code that called us might still use the object after we return.
When we are writing templates using a forwarding reference, we are in front of a
dilemma: our type might be a value or a reference, so how do we decide whether we
can use std::move?
Does it mean we cannot make use of the benefit that std::move brings us?
The answer, of course, is no:
template<typename T>
void do_action(T&& obj) {
do_something_with_obj(???);
// We are not using obj after this call.
}
Should we use move or not in this case?
Being Generic in Templates | 167
The answer is yes: we should move if T is a value, and, no, we should not move if T is a
reference.
C++ provides us with a tool to do exactly this: std::forward.
std::forward is a function template that always takes an explicit template parameter
and a function parameter: std::forward<T>(obj).
Forward looks at the type of T, and if it's an l-value reference, then it simply returns
a reference to the obj, but if it's not, then it is equivalent to calling std::move on the
object.
Let's see it in action:
template<typename T>
void do_action(T&& obj) {
use_printoncopyormove_obj(std::forward<T>(obj));
}
Note
A template can have many type parameters. Forwarding references can apply to
any of the type parameters independently.
168 | Generic Programming and Templates
This is important because the caller of the templated code might know whether it
is better to pass values or pass references, and our code should work regardless of
whether there is a requirement to ask for a specific ref-ness.
We also saw how we can still maintain the advantages of moving, which is required
for some types that do not support copying. This can make our code run much faster,
even for types that support copying, without complicating our code: when we have
forwarding references we use std::forward where we would have used std::move.
Activity 17: Ensuring Users are Logged in When Performing Actions on the
Account
We want to allow the users of our e-commerce website to perform arbitrary actions
(for the scope of this activity, they will be adding and removing items) on their shopping
carts.
Before performing any action, we want to make sure that the user is logged in. Now,
let's follow these instructions:
1. Ensure that there is a UserIdentifier type for identifying the user, a Cart type that
represents the shopping cart of the user, and a CartItem type that represents any
item in the cart:
struct UserIdentifier {
int userId = 0;
};
struct Cart {
std::vector<Item> items;
};
2. Ensure that there is also a function with the signature bool isLoggedIn(const
UserIdentifier& user) and a function to retrieve the cart for an user, Cart
getUserCart(const UserIdentifier& user):
bool isLoggedIn(const UserIdentifier& user) {
return user.userId % 2 == 0;
}
3. In most of our code, we only have access to the UserIdentifier for a user, and
we want to make sure that we always check whether the user is logged in before
doing any action on the cart.
4. To solve this problem, we decide to write a function template called execute_
on_user_cart, which takes the user identifier, an action, and a single parameter.
The function will check if the user is logged in and if so, retrieve their cart, then
perform the action of passing the cart and the single parameter:
template<typename Action, typename Parameter>
void execute_on_user_cart(UserIdentifier user, Action action, Parameter&&
parameter) {
if(isLoggedIn(user)) {
Cart cart = getUserCart(user);
action(cart, std::forward<Parameter>(parameter));
} else {
std::cout << "The user is not logged in" << std::endl;
}
}
5. One of the actions we want to perform is void remove_item(Cart,
CartItem). A second action we want to perform is void add_items(Cart,
std::vector<CartItem>):
void removeItem(Cart& cart, Item cartItem) {
auto location = std::find(cart.items.begin(), cart.items.end(),
cartItem);
if (location != cart.items.end()) {
cart.items.erase(location);
}
std::cout << "Item removed" << std::endl;
}
Note
A parameter of a function template can be used to accept functions as parameters.
170 | Generic Programming and Templates
The aim is to create a function that performs the necessary checks on whether the
user is logged in so that throughout our program we can use it to perform safely
any actions that are required by our business on the user cart, without the risk of
forgetting to check the logged status of the user.
6. We can also move the types that are not forwarding references:
template<typename Action, typename Parameter>
void execute_on_user_cart(UserIdentifier user, Action action, Parameter&&
parameter) {
if(isLoggedIn(user)) {
Cart cart = getUserCart(user);
action(std::move(cart), std::forward<Parameter>(parameter));
}
}
7. Examples of how the execute_on_user_cart function can be used with the actions
we described earlier in the activity is as follows:
UserIdentifier user{/* initialize */};
execute_on_user_cart(user, remove_item, CartItem{});
std::vector<CartItem> items = {{"Item1"}, {"Item2"}, {"Item3"}}; // might
be very long
execute_on_user_cart(user, add_items, std::move(items));
8. The developers in our software can write the functions they need to execute on
the cart, and call execute_on_user_cart to safely execute them.
Note
The solution for this activity can be found on page 302.
Variadic Templates
We just saw how we can write a template that accepts parameters independently from
their ref-ness.
But the two functions we talked about from the standard library, std::invoke and
std::async, have an additional property: they can accept any number of arguments.
In a similar way, std::tuple, a type similar to a std::array but that can contain values of
different types, can contain an arbitrary number of types.
Variadic Templates | 171
A template that has a parameter pack is called a variadic template, since it is a template
that accepts a varying number of parameters.
When instantiating a variadic template, any number of arguments can be provided to
the parameter pack by separating them with a comma:
do_action<int, std:string, float>();
do_action<>();
MyStruct<> myStruct0;
MyStruct<float, int> myStruct2;
Types will contain the list of arguments that are provided when instantiating the
template.
A parameter pack by itself is a list of types and the code cannot interact with it directly.
The variadic template can use the parameter pack by expanding it, which happens by
appending … to a pattern.
172 | Generic Programming and Templates
When a pattern is expanded, it is repeated as many times as there are types in its
parameter pack, separating it with a comma. Of course, to be expanded, a pattern must
contain at least a parameter pack. If multiple parameters are present in the pattern, or
the same parameter is present several times, they are all expanded at the same time.
The simplest pattern is the name of the parameter pack: Types….
For example: to let a function accept multiple arguments, it would expand the
parameter pack in the function arguments:
template<typename… MyTypes>
void do_action(MyTypes… my_types);
do_action();
do_action(1, 2, 4.5, 3.5f);
When we call the function, the compiler automatically deduces the types of the
parameter pack. In the last call, MyTypes will contain int, double, and float, and the
signature of the generated function would be void do_action(int __p0, int __p1,
double __p2, float __p3).
Note
A parameter pack in the list of template parameters can only be followed by
template parameters that have a default value, or those that are deduced by the
compiler.
Most commonly, the parameter pack is the last in the list of template parameters.
The function parameter my_types is called a function parameter pack and needs to be
expanded as well so that it can access the single parameters.
For example: let's write a variadic struct:
template<typename… Ts>
struct Variadic {
Variadic(Ts… arguments);
};
Variadic Templates | 173
Here, we have a variadic function that takes a parameter pack and expands it when
calling the constructor of another variadic struct.
The function parameter packs, which is the function variadic parameter, can be
expanded only in some locations—the most common is as parameters when calling a
function.
The template parameter packs, which is a type variadic parameter, can be expanded
in template argument lists: the list of arguments between <> when instantiating a
template.
As we mentioned previously, the pattern for the expansion might be more complex than
just the name of the argument.
For example: we can access type aliases declared in the type or we can call a function
on the parameter:
template<typename… Containers>
std::tuple<typename Containers::value_type…> get_front(Containers…
containers) {
return std::tuple<typename Containers::value_type…>(containers.front()…);
}
This will call the modify function for each argument and pass the result to do_things.
In this section, we saw how the variadic parameter functionality of C++ lets us write
functions and classes that work with any number and type of parameters.
While it is not a common everyday task to write variadic templates, almost every
programmer uses a variadic template in their day-to-day coding, since it makes it so
much easier to write powerful abstractions, and the standard library makes vast use of
it.
Additionally, in the right situation, variadic templates can allow us to write expressive
code that works in the multitude of situations we need.
Activity 18: Safely Performing Operations on the User Cart with an Arbitrary
Number of Parameters
In the previous activity, we saw a function, execute_on_user_cart, which allows us to
execute arbitrary functions that take an object of type Cart and a single parameter.
In this activity, we want to expand on the supported types of actions we can perform on
the shopping cart of the user by allowing any function that takes an object of type Cart
and an arbitrary number of arguments:
1. Expand the previous activity to accept any number of the parameter with any kind
of ref-ness and pass it to the action provided.
2. Write variadic templates and learn how to expand them:
template<typename Action, typename... Parameters>
void execute_on_user_cart(UserIdentifier user, Action action,
Parameters&&... parameters) {
if(isLoggedIn(user)) {
Cart cart = getUserCart(user);
Writing Easy-to-Read Templates | 175
action(std::move(cart), std::forward<Parameters>(parameters)...);
}
}
Note
The solution for this activity can be found on page 303.
Type Alias
Type aliases allow the user to give a name to a type. They are declared with using name
= type.
After the declaration, everywhere Name is used is going to be equivalent to having used
Type.
This is very powerful for three reasons:
• It can give a shorter and more meaningful name to complex types
• It can declare a nested type to simplify access to it
• It allows you to avoid having to specify the typename keyword in front of a
dependent type
176 | Generic Programming and Templates
Following this, we could create instances of HighScoreBoard directly, with little typing
and clearly specify the intent:
HighScoreBoard highScoreBoard;
We now also have a single place to update if we want to change the way in which we
want to sort the accounts. For example: if we also wanted to consider how long the user
has been registered in the service, we could change the comparator the comparator.
Every user of the type alias will be updated, without the risk of forgetting to update one
location.
Writing Easy-to-Read Templates | 177
Additionally, we clearly have a location where we can put the documentation on the
decision made for using the type we picked.
Note
When using type aliases, give a name that represents what the type is for, not how
it works. UserAccountSortedContainerByBalance is a not a good name because it
tells us how the type works instead of what its intention is.
The second case is extremely useful for allowing code to introspect the class, that is,
looking into some of the details of the class:
template<typename T>
class SortedContainer {
public:
T& front() const;
};
template<typename T>
class ReversedContainer {
public:
T& front() const;
}
We have several containers, which mostly support the same operations. We would like
to write a template function that takes any container and returns the first element,
front:
template<typename Container>
??? get_front(const Container& container);
How can we find out what type is returned?
178 | Generic Programming and Templates
A common pattern is to add a type alias inside the class, like so:
template<typename T>
class SortedContainer {
using value_type = T; // type alias
T& front() const;
};
Now the function can access the type of the contained element:
template<typename Container>
typename Container::value_type& get_front(const Container& container);
Note
Remember that value_type depends on the Container type, so it is a dependent
type. When we use dependent types, we must use the typename keyword in front.
This way, our code can work with any type that declares the nested type value_type.
The third use case, that is, to avoid having to type the typename keyword repeatedly, is
common when interacting with code that follows the previous pattern.
For example: we can have a class that accepts a type:
template<typename Container>
class ContainerWrapper {
using value_type = typename Container::value_type;
}
In the rest of the class, we can use value_type directly, without having to type typename
anymore. This allows us to avoid a lot of repetitions.
The three techniques can also be combined. For example: you can have the following:
template<typename T>
class MyObjectWrapper {
using special_type = MyObject<typename T::value_type>;
};
Summary | 179
Summary
In this chapter, the students were introduced to templates in C++. We saw that
templates exist to create high-level abstractions that work independently from
the types of the objects at zero overhead at runtime. We explained the concept of
type requirements: the requirements a type must satisfy to work correctly with the
templates. We then showed the students how to write function templates and class
templates, mentioning dependent types as well, to give the students the tools to
understand a class of errors that happen when writing template code.
We then showed how templates can work with non-type parameters, and how
templates can be made easier to use by providing default template arguments, thanks to
template argument deduction.
We then showed the students how to write more generic templates, thanks to the
forwarding reference, std::forward, and the template parameter pack.
Finally, we concluded with some tools to make templates easier to read and more
maintainable.
In the next chapter, we will cover standard library containers and algorithms.
Standard Library
5
Containers and
Algorithms
Chapter Objectives
Introduction
The core of C++ is its Standard Template Library (STL), which represents a set of
important data structures and algorithms that facilitates the programmer's task and
improves code efficiency.
The components of the STL are parametric so that they can be reused and combined
in different ways. The STL is mainly made up of container classes, iterators, and
algorithms.
Containers are used to store collections of elements of a certain type. Usually, the
type of the container is a template parameter, which allows the same container class
to support arbitrary elements. There are several container classes, each of them with
different characteristics and features.
Iterators are used to traverse the elements of a container. Iterators offer the
programmer a simple and common interface to access containers of a different type.
Iterators are similar to raw pointers, which can also iterate through elements using the
increment and the decrement operators, or can access a specific element using the
de-reference (*) operator.
Algorithms are used to perform standard operations on the elements stored in the
containers. They use iterators to traverse the collections, since their interface is
common to all the containers, so that the algorithm can be agnostic about the container
it's operating on.
Algorithms treat functions as parameters that are provided by the programmer in
order to be more flexible in the operation that's being performed. It is common to see
an algorithm applied to a container of objects of a user-defined type. This algorithm,
to execute correctly, needs to know how to treat the object in detail. For this reason,
the programmer provides a function to the algorithm to specify the operations to be
executed on the objects.
Sequence Containers
Sequence containers, sometimes referred to as sequential containers, are a particular
class of containers where the order in which their elements are stored is decided by
the programmer rather than by the values of the elements. Every element has a certain
position that is independent of its value.
Sequence Containers | 183
Figure 5.1: Table presenting the sequence container classes and their descriptions
Array
The array container is a fixed-size data structure of contiguous elements. It recalls the
static array that we saw in Chapter 1, Getting Started:
An array's size needs to be specified at compile time. Once defined, the size of the array
cannot be changed.
When an array is created, the size elements it contains are initialized next to each
other in memory. While elements cannot be added or removed, their values can be
modified.
Arrays can be randomly accessed using the access operator with the corresponding
element's index. To access an element at a given position, we can use the operator []
or the at() member function. The former does not perform any range checks, while the
latter throws an exception if the index is out of range. Moreover, the first and the last
element can be accessed using the front() and back() member functions.
These operations are fast: since the elements are contiguous, we can compute the
position in memory of an element given its position in the array, and access that
directly.
The size of the array can be obtained using the size() member function. Whether the
container is empty can be checked using the empty() function, which returns true if
size() is zero.
The array class is defined in the <array> header file, which has to be included before
usage.
Vector
The vector container is a data structure of contiguous elements whose size can be
dynamically modified: it does not require to specify its size at creation time:
Figure 5.3: Vector elements are contiguous, and their size can grow dynamically
A vector stores the elements it contains in a single section of memory. Usually, the
section of memory has enough space for more elements than the number of elements
stored in the vector. When a new element is added to the vector, if there is enough
space in the section of memory, the element is added after the last element in the
vector. If there isn't enough space, the vector gets a new, bigger section of memory
and copies all the existing elements into the new section of memory, then it deletes
the old section of memory. To us, it will seem like the size of the section of memory has
increased:
Appending or deleting the last element is fast, while inserting or removing other
elements of the vector is considered slow, as it requires moving all the elements to
make space for the new element or to keep all the elements contiguous:
Figure 5.5: Elements being moved during insertions or deletions inside a vector
Vectors, just like arrays, allow efficient access of elements at random positions. A
vector's size is also retrieved with the size() member function, but this should not be
confused with capacity(). The former is the actual number of elements in the vector,
and the latter returns the maximum number of elements that can be inserted in the
current section of memory.
Sequence Containers | 187
For example, in the preceding diagram, initially, the array had a size of 4 and a capacity
of 8. So, even when an element had to be moved to the right, the vector's capacity
did not change, as we never had to get a new, bigger section of memory to store the
elements.
The operation of getting a new section of memory is called reallocation. Since
reallocation is considered an expensive operation, it is possible to reserve enough
memory for a given number of elements by enlarging a vector's capacity using the
reserve() member function. The vector's capacity can also be reduced to fit the number
of elements using the shrink_to_fit() function in order to release memory that is not
needed anymore.
Note
Vector is the most commonly used container for a sequence of elements and is
often the best one performance-wise.
int main()
{
std::vector<int> myvector;
myvector.push_back(100);
// Both front and back of vector contains a value 100
myvector.push_back(10);
// Now, the back of the vector holds 10 as a value, the front holds 100
myvector.front() -= myvector.back();
// We subtracted front value with back
188 | Standard Library Containers and Algorithms
std::cout << "Front of the vector: " << myvector.front() << std::endl;
std::cout << "Back of the vector: " << myvector.back() << std::endl;
}
Output:
Front of the vector: 90
Back of the vector: 10
Deque
The deque container (pronounced deck) is short for "double-ended queue." Like vector,
it allows for fast, direct access of deque elements and fast insertion and deletion at
the back. Unlike vector, it also allows for fast insertion and deletion at the front of the
deque:
Figure 5.6: Deque elements can be added and removed at the start and the end
List
The list container is a data structure of nonadjacent elements that can be dynamically
grown:
Figure 5.7: List elements are stored in different sections of memory, and have connecting links
Sequence Containers | 189
Figure 5.8: C is to be inserted between A and B. A's successor and B's predecessor link must be updated
to point to C (orange). C's link to the predecessor and successor are updated to points A and B (green)
When an element is removed from the list, we need to update the successor link of
the predecessor node to point to the successor of the removed node. Similarly, the
predecessor link of the successor node needs to be updated to point to the predecessor
of the removed node.
In the preceding diagram, if we were to remove C, we would have to update A's
successor to point to C's successor (B), and B's predecessor to point to C's predecessor
(A).
190 | Standard Library Containers and Algorithms
Unlike vectors, lists do not provide random access. Elements are accessed by linearly
following the chain of elements: starting from the first, we can follow the successor link
to find the next node, or from the last node we can follow the predecessor link to find
the previous node, until we reach the element we are interested into.
The advantage of list is that insertion and removal are fast at any position, if we
already know the node at which we want to insert or remove. The disadvantage of this
is that getting to a specific node is slow.
The interface is similar to a vector, except that lists don't provide operator[].
Forward-List
The forward_list container is similar to the list container, with the difference that its
nodes only have the link to the successor. For this reason, it is not possible to iterate
over a forward_list in backward order:
Figure 5.9: Forward-list elements are like List elements, but only have one-way connecting links
The sequence must be provided in curly brackets, and the elements need to be comma-
separated. This is called an initializer list:
#include <vector>
int main()
{
// initialize the vector with 3 numbers
std::vector<int> numbers = {1, 2, 3};
}
This works for any of the containers we have seen in this chapter.
Note
The solution for this activity can be found on page 304.
With this activity, we learned how we can store an arbitrary number of accounts.
192 | Standard Library Containers and Algorithms
Associative Containers
Associative containers are containers that allow for the fast lookup of elements.
Additionally, the elements are always kept in a sorted order. The order is determined
by the value of the element and a comparison function. The comparison function is
called a comparator, and by default this is the operator<, although the user can supply
a Functor (function object) as a parameter to specify how the elements should be
compared. The <functional> header contains many such objects that can be used to
sort the associative containers, like std::less or std::less.
Set and multiset have size() and empty() function members to check how many
elements are contained and whether any elements are contained.
Insertion and removal is done through the insert() and erase() functions. Because
the order of the elements is determined by the comparator, they do not take a position
argument like they do for sequential containers. Both insertion and removal are fast.
Since sets are optimized for element lookup, they provide special search functions. The
find() function returns the position of the first element equal to the provided value, or
the position past the end of the set when the element is not found. When we look for an
element with find, we should always compare it with the result of calling end() on the
container to check whether the element was found.
Let's examine the following code:
#include <iostream>
#include <set>
int main() {
std::set<int> numbers;
numbers.insert(10);
if (numbers.find(10) != numbers.end()) {
std::cout << "10 is in numbers" << std::endl;
}
}
Finally, count() returns the number of elements equal to the value provided.
The set and multiset classes are defined in the <set> header file.
Example of a set with a custom comparator:
#include <iostream>
#include <set>
#include <functional>
int main() {
std::set<int> ascending = {5,3,4,2,1};
std::cout << "Ascending numbers:";
for(int number : ascending) {
std::cout << " " << number;
}
std::cout << std::endl;
194 | Standard Library Containers and Algorithms
Figure 5.12: Map and multimap store a sorted group of keys, which is associated to a value
Associative Containers | 195
Map allows you to associate a single value to a key, while multimap allows you to
associate multiple values to the same key.
The map and multimap classes are defined in the <map> header file.
To insert values into a map, we can call insert(), providing a pair containing the key
and the value. Later in this chapter, we will see more about pairs. The function also
returns a pair, containing the position at which the element was inserted, and a Boolean
set to true if the element was inserted, or false if an element with the same key already
exists.
Once values are inserted into the map, there are several ways to look up a key/value
pair in a map.
Similar to set, map provides a find() function, which looks for a key in the map and
returns the position of the key/value pair if it exists, or the same result of calling end().
From the position, we can access the key with position->first and the value with
position->second:
#include <iostream>
#include <string>
#include <map>
int main()
{
std::map<int, std::string> map;
map.insert(std::make_pair(1, "some text"));
auto position = map.find(1);
if (position != map.end() ) {
std::cout << "Found! The key is " << position->first << ", the value
is " << position->second << std::endl;
}
}
An alternative to accessing a value from a key is to use at(), which takes a key and
returns the associated value.
196 | Standard Library Containers and Algorithms
3. Insert the balances of the users inside the map by using insert and std::make_
pair. The first argument is the key, while the second one is the value:
balances.insert(std::make_pair("Alice",50));
4. Use the find function, providing the name of the user to find the position of the
account in the map. Compare it with end() to check whether a position was found.
5. Now, look for the account of Alice. We know Alice has an account, so there is no
need to check whether we found a valid position. We can print the value of the
account using ->second:
auto alicePosition = balances.find("Alice");
std::cout << "Alice balance is: " << alicePosition->second << std::endl;
Note
The solution for this activity can be found on page 305.
Unordered Containers
Unordered associative containers differ from associative containers in that the
elements have no defined order. Visually, unordered containers are often imagined as
bags of elements. Because the elements are not sorted, unordered containers do not
accept a comparator object to provide an order to the elements. On the other hand, all
the unordered containers depend on a hash function.
he user can provide a Functor (function object) as a parameter to specify how the keys
should be hashed:
Typically, unordered containers are implemented as hash tables. The position in the
array is determined using the hash function, which given a value returns the position
at which it should be stored. Ideally, most of the elements will be mapped into different
positions, but the hash function can potentially return the same position for different
elements. This is called a collision. This problem is solved by using linked lists to chain
elements that map into the same position, so that multiple elements can be stored in
the same position. Because there might be multiple elements at the same position, the
position is often called bucket.
Implementing unordered containers using a hash table allows us to find an element
with a specific value in constant time complexity, which translates to an even faster
lookup when compared to associative containers:
Figure 5.14: When an element is added to the set, its hash is computed to decide in which bucket the
element should be added. The elements inside a bucket are stored as nodes of a list.
Container Adaptors | 199
When a key/value pair is added to the map, the hash of the key is computed to decide
in which bucket the key/value pair should be added:
Figure 5.15: Representation of computing the bucket of an element from the key, and storing the key/
value pair as nodes in a list.
Unordered associative containers and ordered associative containers provide the same
functionalities, and the explanations in the previous section apply to the unordered
associative containers as well. Unordered associative containers can be used to get
better performances when the order of the elements is not important.
Container Adaptors
Additional container classes that are provided by the STL library are container adaptors.
Container adaptors provide constrained access policies on top of the containers we
have looked at in this chapter.
200 | Standard Library Containers and Algorithms
Container adaptors have a template parameter that the user can provide to specify the
type of container to wrap:
Stack
The stack container implements the LIFO access policy, where the elements are
virtually stacked one on the top of the other so that the last inserted element is always
on top. Elements can only be read or removed from the top, so the last inserted element
is the first that gets removed. A stack is implemented using a sequence container class
internally, which is used to store all the elements and emulate the stack behavior.
The access pattern of the stack data structure happens mainly through three core
member functions: push(), top(), and pop(). The push() function is used to insert an
element into the stack, top() used to access the element on top of the stack, and pop()
is used to remove the top element.
The stack class is defined in the <stack> header file.
Queue
The queue class implements the FIFO access policy, where the elements are enqueued
one after the other, so that elements inserted before are ahead of elements inserted
after. Elements are inserted at the end of the queue and removed at the start.
The interface of the queue data structure is composed of the push(), front(), back(),
and pop() member functions.
The push() function is used to insert an element into the queue(); front() and back()
return the next and last elements of the queue, respectively; the pop() is used to
remove the next element from the queue.
The queue class is defined in the <queue> header file.
Container Adaptors | 201
Priority Queue
Finally, the priority queue is a queue where the elements are accessed according to
their priority, in descending order (highest priority first).
The interface is similar to the normal queue, where push() inserts a new element and
top() and pop() access and remove the next element. The difference is in the way
the next element is determined. Rather than being the first inserted element, it is the
element that has the highest priority.
By default, the priority of the elements is computed by comparing the elements with
the operator<, so that an element that is less than another comes after it. A user-
defined sorting criterion can be provided to specify how to sort the elements by
priority in regard to their priority in the queue.
The priority queue class is also defined in the <queue> header file.
5. Fill the code inside the following two functions to store the user form and process
it:
void storeRegistrationForm(std::stack<RegistrationForm>& stack,
RegistrationForm form) {
}
void endOfDayRegistrationProcessing(std::stack<RegistrationForm>& stack) {
}
We'll see that the registration forms are processed in reverse order as the users are
registered.
Note
The solution for this activity can be found at page 306.
Unconventional Containers
Up until now, we've seen containers that are used to store groups of elements of the
same type.
The C++ standard defines some other types that can contain types but offer a different
set of functionalities from the containers we saw previously.
These types are as follows:
1. String
2. Pair and tuple
3. Optional
4. Variant
Strings
A string is a data structure that's used to manipulate mutable sequences of contiguous
characters. The C++ string classes are STL containers: they behave similarly to vectors,
but provide additional functionalities that ease the programmer to perform common
operations of sequences of characters easily.
Unconventional Containers | 203
There exist several string implementations in the standard library that are useful for
different lengths of character sets, such as string, wstring, u16string, and u32string. All
of them are a specialization of the basic_string base class and they all have the same
interface.
The most commonly used type is std::string.
All types and functions for strings are defined in the <string> header file.
A string can be converted into a null-terminating string, which is an array of characters
that terminate with the special null character (represented with '\0') via the use of the
data() or c_str() functions. Null-terminating strings, also called C-strings, are the way
to represent sequences of character in the C language and they are often used when
the program needs to interoperate with a C library; they are represented with the const
char * type and are the type of the literal strings in our programs.
int main()
{
std::string str = "C++ Fundamentals.";
std::cout << str << std::endl;
str.erase(5,10);
Unconventional Containers | 205
str.clear();
std::cout << "Cleared: " << str << std::endl;
}
Output:
C++ Fundamentals.
Erased: C++ Fs.
Cleared:
C++ also provides many convenience functions to convert a string into numeric values
or vice versa. For example, the stoi() and stod() functions (which stand for string-
to-int and string-to-double) are used to convert string to int and double, respectively.
Instead, to convert a value into a string, it is possible to use the overloaded function
to_string().
Let's demystify these functions using the following code:
#include <iostream>
#include <string>
using namespace std;
int main()
{
std::string str = "55";
std::int strInt = std::stoi(str);
double strDou = std::stod(str);
std::string valToString = std::to_string(strInt);
Output:
55
55
55
55
Pairs are used by unordered map, unordered multimap, map, and multimap containers
to manage their key/value elements.
Tuples are similar to pairs. The constructor allows you to provide a variable number
of template arguments. Elements are accessed with the get<N>() function only, which
returns the nth element inside the tuple, and there is a convenience function to create
them similar to that for pair, named make_tuple().
Additionally, tuples have another convenience function that's used to extract values
from them. The tie() function allows for the creation of a tuple of references, which is
useful in assigning selected elements from a tuple to specific variables.
std::optional | 207
Let's understand how to use the make_tuple() and get() functions to retrieve data from
a tuple:
#include <iostream>
#include <tuple>
#include <string>
int main()
{
std::tuple<std::string, int, float> james = std::make_tuple("James", 7,
1.90f);
std::cout << "Name: " << std::get<0>(james) << ". Agent number: " <<
std::get<1>(james) << ". Height: " << std::get<2>(james) << std::endl;
}
Output:
Name: James. Agent number: 7. Height: 1.9
std::optional
optional<T> is a that's used to contain a value that might be present or not.
The class takes a template parameter, T, which represents the type that the
std::optional template class might contain. Value type means that the instance of the
class contains the value. Copying optional will create a new copy of the contained data.
At any point in the execution of the program, optional<T> either contains nothing, when
it's empty, or contains a value of type T.
Optional is defined in the <optional> header.
Let's imagine our application is using a class named User for managing registered users.
We would like to have a function that gets us the information of a user from their email:
User getUserByEmail(Email email);.
But what happens when a user is not registered? That is, when we can determine that
our system does not have the associated User instance?
Some would suggest throwing an exception. In C++, exceptions are used for exceptional
situations, ones that should almost never happen. A user not being registered on our
website is a perfectly normal situation.
208 | Standard Library Containers and Algorithms
In these situations, we can use the optional template class to represent the fact that we
might not have the data:
std::optional<User> tryGetUserByEmail(Email email);
Let's look at the following example to understand how the has_value() and value()
functions work:
#include <iostream>
#include <optional>
int main()
{
// We might not know the hour. But if we know it, it's an integer
std::optional<int> currentHour;
if (not currentHour.has_value()) {
std::cout << "We don't know the time" << std::endl;
}
currentHour = 18;
if (currentHour) {
std::cout << "Current hour is: " << currentHour.value() << std::endl;
}
}
Output:
We don't know the time
Current hour is: 18
std::optional | 209
The optional template comes with additional convenience features. We can assign the
std::nullopt value to optional to make it explicit when we want it empty, and we can
use the make_optional value to create an optional from a value. Additionally, we can
use the dereference operator, *, to access the value of optional without throwing an
exception if the value is not present. In such cases, we will access invalid data, so we
need to be sure that optional contains a value when we use *:
std::optional<std::string> maybeUser = std::nullopt;
if (not maybeUser) {
std::cout << "The user is not present" << std::endl;
}
maybeUser = std::make_optional<std::string>("email@example.com");
if (maybeUser) {
std::cout << "The user is: " << *maybeUser << std::endl;
}
Another handy method is value_or(defaultValue). This function takes a default value
and returns the value contained by optional if it currently holds a value, otherwise it
returns the default value. Let's explore the following example:
#include <iostream>
#include <optional>
int main()
{
std::optional<int> x;
std::cout << x.value_or(10) << std::endl;
//Will return value of x as 10
x = 15;
std::cout << x.value_or(10)<< std::endl;
//Will return value of x as 15
}
Output:
10
15
210 | Standard Library Containers and Algorithms
You can see that the number of overloads grows quickly when there are more
arguments that we might not want to pass.
std::variant
variant is a value type that's used to represent a choice of types. The class takes a list of
types, and the variant will be able to contain one value of any of those types.
It is often referred to as tagged union, because similar to a union, it can store multiple
types, with only one present at a time. It also keeps track of which type is currently
stored.
During the execution of a program, variant will contain exactly one of the possible
types at a time.
Like optional, variant is a value type: when we create a copy of variant, the element
that is currently stored is copied into the new variant.
std::variant | 211
To interact with std::variant, the C++ standard library gives us two main functions:
• holds_alternative<Type>(variant): It returns true if the variant is currently
holding the provided type, if not then false.
• get(variant): There are two versions: get<Type>(variant) and
get<Index>(variant).
get<Type>(variant) gets the value of the type that's currently stored inside
the variant. Before calling this function, the caller needs to be sure that holds_
alternative<Type>(variant) returns true.
get<Index>(variant) gets the value of the index type that's currently stored inside
variant. Like before, the caller needs to be sure that variant is holding the correct type.
For example, with std::variant<string, float> variant, calling get<0>(variant) will
give us the string value, but we need to be sure that variant is currently storing a string
at the moment. Usually, it is preferable to access the elements with get<Type>() so
that we are explicit on the type that we expect and that if the order of the types in the
variant changes, we will still get the same result:
struct Taxi {
int lane;
int numPassengers;
};
struct Flying {
float speed;
};
3. The airplane should have three methods:
• startTaxi(): This method takes the lane the airplane should go on and the
number of passengers on board. The airplane can start taxi only if it is at the
gate.
• takeOff(): This method takes the speed at which the airplane should fly. The
airplane can start flying only if it is in the taxi state.
• currentStatus(): This method prints the current status of the airplane.
Note
The solution for this activity can be found on page 306.
Iterators | 215
Iterators
In this chapter, we've mentioned multiple times that elements have a position in a
container: for example, we said that we can insert an element in a specific position in a
list.
Iterators are the way in which the position of an element in a container is represented.
They provide a consistent way to operate on elements of the container, abstracting the
details of the container to which the elements belong.
An iterator always belongs to a range. The iterator representing the start of the range,
can be accessed by the begin() function, while the iterator representing the end of the
range, non-inclusive, can be obtained with the end() function. The range where the first
element is included, but where the last one is excluded, is referred to as half-open.
The interface that the iterator must offer is composed of four functions:
1. The * operator provides access to the element at the position currently referenced
by the iterator.
2. The ++ operator is used to move forward to the next element.
3. Then, the == operator is used to compare two iterators to check whether they are
pointing to the same position.
Note that two iterators can only be compared if they are part of the same range:
they must represent the position of elements of the same container.
4. Finally, the = operator is used to assign an iterator.
Every container class in C++ must specify the type of iterator that it provides to access
its elements as a member type alias named iterator. For example, for a vector of
integer, the type would be std::vector<int>::iterator.
Let's see how we could use iterators to iterate over all the elements of a container (a
vector, in this case):
#include <iostream>
#include <vector>
int main()
{
216 | Standard Library Containers and Algorithms
• Forward iterators are very similar to input iterators but provide additional
guarantees.
The same iterator can be dereferenced several times to access the element it
points to.
Additionally, when we increment or dereference a forward iterator, the other
copies are not invalidated: if we make a copy of a forward iterator, we can advance
the first one, and the second can still be used to access the previous element.
Two iterators that refer to the same element are guaranteed to be equal.
• Bidirectional iterators are also forward iterators with the additional ability to
iterate backward over the elements using the operator-- (position decrement)
member function.
• Random-access iterators are also bidirectional iterators with the additional ability
to directly access any position without the need of a linear scan, in constant time.
Random-access iterators are provided by the operator[] member function to
access elements at generic indexes and the binary operator+ and operator- to step
forward and backward of any quantity.
Many of the iterators we will talk about are defined in the <iterator> header.
Reverse Iterators
Sometimes, we need to iterate though a collection of elements in reverse order.
C++ provides an iterator that allows us to do this: the reverse iterator.
A reverse iterator wraps a bidirectional iterator and swaps the operation increment with
the operation of decrement, and vice versa.
Because of this, when we are iterating a reverse iterator in the forward direction, we are
visiting the elements in a range in backward order.
We can reverse the range of a container by calling the following methods on a
container:
Code that works on normal iterators, it will also work with reverse iterators.
For example, we can see how similar the code is to iterate in reverse order.
220 | Standard Library Containers and Algorithms
Insert Iterators
Insert iterators, also called inserters, are used to insert new values into a container
rather than overwrite them.
There exist three types of inserters, which differ on the position in the container at
which they insert the elements.
Iterators | 221
Some algorithms, which we are going to see later in this chapter, require an iterator for
storing data. Insert iterators are usually used with such algorithms.
Stream Iterators
Stream iterators allow us to use streams as a source to read elements from or as a
destination to write elements to:
Because we don't have a container in this case, we cannot call the end() method to get
the end iterator. A default constructed stream iterator counts as the end of any stream
range.
Let's look at a program that reads space-separated integers from the standard input.
Iterator Invalidation
As we said, iterators represent the position of elements in a container.
This means that they are tightly tied with the container, and changes to the container
might move the elements: this means that iterators pointing to such an element can no
longer be used – they are invalidated.
It is extremely important to always check the invalidation contract when using iterators
with containers, as it is not specified what happens when using an invalidated iterator.
More commonly, invalid data is accessed or the program crashes, leading to bugs that
are hard to find.
If we keep in mind how the containers are implemented, as we saw earlier in this
chapter, we can more easily remember when an iterator is invalidated.
For example, we said that when we insert an element in a vector, we might have to
get more memory to store the element, in which case all the previous elements are
moved to the newly obtained memory. This means that all the iterators pointing to the
elements are now pointing to the old location of the elements: they are invalidated.
On the other hand, we saw that when we insert an element into the list, we only have
to update the predecessor and successor nodes, but the elements are not moved. This
means that the iterators to the elements remain valid:
#include <iostream>
#include <vector>
#include <list>
int main()
Iterators | 223
{
std::vector<int> vector = {1};
auto first_in_vec = vector.begin();
std::cout << "Before vector insert: " << *first_in_vec << std::endl;
vector.push_back(2);
// first_number is invalidated! We can no longer use it!
std::list<int> list = {1};
auto first_in_list = list.begin();
list.push_back(2);
// first_in_list is not invalidated, we can use it.
std::cout << "After list insert: " << *first_in_list << std::endl;
}
Output:
Before vector insert: 1
After list insert: 1
3. The element of the array is accessed using the dereference operator (*) on the
iterator:
for (auto pos = numbers.begin(); pos != numbers.end(); ++pos)
{
std::cout << "Balance: " << *pos << std::endl;
}
Note
Algorithms operate on ranges, so they normally take a pair of iterators: first and
last.
As we said earlier in this chapter, the last iterator denotes the element past the end
of the range – it is not part of the range.
This means that when we want to operate on a full container, we can pass begin()
and end() as arguments to the algorithm, but if we want to operate on a shorter
sequence, we must be sure that our last iterator is past the last item we want to
include in the range.
Algorithms Provided by the C++ Standard Template Library | 225
Lambda
Most of the algorithms accept a unary or binary predicate: a Functor (function object),
which accepts either one or two parameters. These predicates allow the user to
specify some of the actions that the algorithm requires. What the actions are vary from
algorithm to algorithm.
As we saw at the end of Chapter 3, Classes, to write a function object, we have to create
a class and overload the operator().
This can be very verbose, especially when the functor should perform a simple
operation.
To overcome this with C++, the user has to write a lambda expression, also called just a
lambda.
A lambda expression creates a special function object, with a type known only by the
compiler, that behaves like a function but can access the variables in the scope in which
it is created.
It is defined with a syntax very similar to the one of functions:
[captured variables] (arguments) { body }
This creates a new object that, when called with the arguments specified in the lambda
expression, executes the body of the function.
Arguments is the list of arguments the function accepts, and body is the sequence of
statements to execute when the function is invoked. They have the same meaning that
they have for functions, and the same rules we saw in Chapter 2, Functions, apply.
For example, let's create a lambda that takes two integers and returns their sum:
#include <iostream>
int main()
{
auto sum_numbers = [] (int a, int b) { return a + b; };
std::cout << sum_numbers(10, 20) << std::endl;
}
Output:
30
By default, the body of the lambda can only reference the variables that are defined in
the argument list and inside the body, like for functions.
226 | Standard Library Containers and Algorithms
Additionally, lambdas can capture a variable in the local scope, and use it in their body.
Captured variables entail a list of variable names that can be referenced in the body of
the lambda.
When a variable is captured, it is stored inside the created function object, and it can be
referenced in the body.
By default, the variables are captured by value, so they are copied inside the function
object:
#include <iostream>
int main()
{
int addend = 1;
auto sum_numbers = [addend](int b) { return addend + b; };
addend = 2;
std::cout << sum_numbers(3) << std::endl;
}
Output:
4
When we created the lambda, we captured addend by value: it was copied into the
sum_numbers object. Even if we modified the value of addend, we did not change the copy
stored inside sum_numbers, so when sum_numbers is executed, it sums 1 to b.
In some situations, we want to be able to modify the value of a variable in the scope in
which the lambda is created, or we want to access the actual value, not the value that
the variable had when the lambda was created.
In that case, we can capture by reference by prepending & to the variable name.
Note
When we capture by reference, we need to make sure that the variable that's been
captured by reference is still valid when the lambda is invoked, otherwise the body
of the function accesses an invalid object, resulting in bugs.
Prefer to capture by value when it is possible.
Algorithms Provided by the C++ Standard Template Library | 227
int main()
{
int multiplier = 1;
auto multiply_numbers = [&multiplier](int b) { return multiplier * b; };
multiplier = 2;
std::cout << multiply_numbers(3) << std::endl;
}
Output:
6
Here, we capture the multiplier variable by reference: only a reference to it was stored
into multiply_numbers.
When we invoke multiply_numbers, the body accesses the current value of multiplier,
and since multiplier was changed to 2, that is the value that's used by the lambda.
A lambda can capture multiple variables, and each one can be either captured by value
or by reference, independently one from the other.
Read-Only Algorithms
Read-only algorithms are algorithms that inspect the elements stored inside a container
but do not modify the order of the elements of the container.
228 | Standard Library Containers and Algorithms
The following are the most common operations that inspect the elements of a range:
Figure 5.22: Table presenting the operations that inspect elements of a range
int main()
{
std::vector<int> vector = {1, 2, 3, 4};
bool allLessThen10 = std::all_of(vector.begin(), vector.end(), [](int
value) { return value < 10; });
std::cout << "All are less than 10: " << allLessThen10 << std::endl;
bool someAreEven = std::any_of(vector.begin(), vector.end(), [](int
value) { return value % 2 == 0; });
std::cout << "Some are even: " << someAreEven << std::endl;
bool noneIsNegative = std::none_of(vector.begin(), vector.end(), [](int
value) { return value < 0; });
std::cout << "None is negative: " << noneIsNegative << std::endl;
Algorithms Provided by the C++ Standard Template Library | 229
Modifying Algorithms
Modifying algorithms are algorithms that modify the collections they iterate on:
int main()
{
std::vector<std::string> vector = {"Hello", "C++", "Morning",
"Learning"};
std::vector<std::string> longWords;
std::vector<int> lengths;
std::transform(longWords.begin(), longWords.end(), std::back_
inserter(lengths), [](const std::string& s) { return s.length(); });
Mutating Algorithms
Mutating algorithms are algorithms that change the order of elements:
int main()
{
std::vector<int> vector = {1, 2, 3, 4, 5, 6};
std::random_device randomDevice;
std::mt19937 randomNumberGenerator(randomDevice());
std::shuffle(vector.begin(), vector.end(), randomNumberGenerator);
std::cout << "Values: ";
232 | Standard Library Containers and Algorithms
Sorting Algorithms
This class of algorithms rearranges the order of elements within a container in a
specific order:
int main()
{
std::vector<int> vector = {5, 2, 6, 4, 3, 1};
std::sort(vector.begin(), vector.end());
std::cout << "Values: ";
std::for_each(vector.begin(), vector.end(), [](int value) { std::cout <<
value << " "; });
std::cout << std::endl;
}
Output:
Values: 1 2 3 4 5 6
Algorithms Provided by the C++ Standard Template Library | 233
int main()
{
std::vector<int> vector = {1, 2, 3, 4, 5, 6};
Numeric Algorithms
This class of algorithms combines numeric elements using a linear operation in
different ways:
int main()
{
std::vector<int> costs = {1, 2, 3};
4. After removing the old accounts, we need to sort the balances in descending
order. By default, std::sort uses an ascending order, so we need to provide a
lambda to change the order:
std::sort(newAccounts.begin(), newAccounts.end(), [](const
UserAccount& lhs, const UserAccount& rhs) { return lhs.balance > rhs.
balance; } );
Now that the data is sorted, we can print it:
for(const UserAccount& account : newAccounts) {
std::cout << account.balance << std::endl;
}
}
5. We can now invoke our function with the following test data:
int main()
{
std::map<std::string, UserAccount> users = {
{"Alice", UserAccount{500, 15}},
{"Bob", UserAccount{1000, 50}},
{"Charlie", UserAccount{600, 17}},
{"Donald", UserAccount{1500, 4}}
};
computeAnalytics(users);
}
Summary
In this chapter, we introduced sequential containers – containers whose elements can
be accessed in sequence. We looked at the array, vector, deque, list, and forward_list
sequential containers.
We saw what functionality they offer and how we can operate on them, and we saw how
they are implemented and how storage works for vector and list.
We followed this up with associative containers, containers that allow the fast lookup
of their elements, always kept in order. Set, multiset, map, and multimap are part of this
category.
We looked at the operations they support and how map and multimap are used to
associate a value to a key. We also saw their unordered version, which does not keep
elements in order but provides higher performance. Unordered_set and unordered_map
are in this category.
Summary | 237
Lesson Objectives
• Implement interfaces
In this chapter, you will learn how to use the advanced features of C++ to create dynamic
programs.
240 | Object-Oriented Programming
Introduction
In earlier chapters, we learned about templates that are used to create functions and
classes that work with arbitrary types. This avoids duplication of work. However, using
templates is not applicable in all cases, or may not be the best approach. The limitation
of templates is that their types need to be known when the code is compiled.
In real-world cases, this is not always possible. A typical example would be a program
that determines what logging infrastructure to use depending on the value of a
configuration file.
Consider the following problems:
• While developing the application and executing tests, the application would use a
logger that prints detailed information.
• On the other hand, when the application is deployed to the PCs of its users, the
application would use a logger that prints error summaries and notifies the
developers if there are any errors.
Inheritance
Inheritance allows the combination of one or more classes. Let's look at an example of
inheritance:
class Vehicle {
public:
TankLevel getTankLevel() const;
void turnOn();
};
In this example, the Car class inherits from the Vehicle class, or, we can say Car derives
from Vehicle. In C++ terminology, Vehicle is the base class, and Car is the derived class.
Inheritance | 241
When defining a class, we can specify the classes it derives from by appending :,
followed by one or more classes, separated by a comma:
class Car : public Vehicle, public Transport {
}
When specifying the list of classes to derive from, we can also specify the visibility of
the inheritance – private, protected, or public.
The visibility modifier specifies who can know about the inheritance relationship
between the classes.
The methods of the base class can be accessed as methods of the derived class based on
the following rules:
Car car;
car.turnOn();
When the inheritance is public, the code external to the class knows that Car derives
from Vehicle. All the public methods of the base class are accessible as public method of
the derived class by the code in the program. The protected methods of the base class
can be accessed as protected by the methods of the derived class. When inheritance
is protected, all the public and protected members are accessible as protected by
the derived class. Only the derived class and classes that derive from it know about
inheritance; external code sees the two classes as unrelated.
Finally, when deriving with a private modifier, all the public and protected methods and
fields of the base class are accessible by the derived class as private.
The private methods and fields of a class are never accessible outside of that class.
Accessing the fields of the base class follows the same rules.
Let's see a summary:
Figure 6.1: Base class methods and the access level they provide
242 | Object-Oriented Programming
The class Citrus can access the public and protected methods of class Fruit, whereas
class Orange will be able to access both Citrus' and Fruit's public and protected
methods (Fruit's public methods are accessible through Citrus).
The specifier is not mandatory. If it is omitted, it defaults to public for structs and to
private for classes.
Note
If you use inheritance to group together some functionality when implementing a
class, it is often correct to use private inheritance, as that is a detail of how you
are implementing the class, and it is not part the public interface of the class.
If, instead, you want to write a derived class that can be used in place of the base
class, use public inheritance.
When inheriting from a class, the base class gets embedded into the derived class. This
means that all the data of the base class also becomes part of the derived class in its
memory representation:
Figure 6.2: Representation of the derived class and the base class
A question might come up at this point – we are embedding the base class inside the
derived class. This means that we need to initialize the base class when we initialize
the derived class, otherwise, part of the class would be left uninitialized. When do we
initialize the base class?
When writing the constructor of the derived class, the compiler will implicitly call the
default constructor of the base class before any initialization takes place.
If the base class does not have a default constructor but has a constructor that accepts
parameters, then the derived class constructor can explicitly call it in the initialization
list. Otherwise, there will be an error.
Inheritance | 245
In a similar way to how the compiler calls the constructor of the base class when the
derived class is constructed, the compiler takes care of always calling the destructor of
the base class after the destructor of the derived class has run:
class A {
public:
A(const std::string& name);
};
class B: public A {
public:
B(int number) : A("A's name"), d_number(number) {}
private:
int d_number;
};
}
When B's constructor is called, the A needs to be initialized. Since A doesn't have
a default constructor, the compiler cannot initialize it for us: we have to call A's
constructor explicitly.
The copy constructor and the assignment operator generated by the compiler take
care of calling the constructor and operator of the base class.
When, instead, we write our implementation of the copy constructor and the
assignment operators, we need to take care of calling the copy constructor and
assignment operator.
Note
In many compilers, you can enable additional warnings that notify you if you forget
to add the calls to the base constructor.
246 | Object-Oriented Programming
Note
Using an is a test to understand whether a relationship can use inheritance can
fail in some cases: for example, a square inheriting from a rectangle. When
the width of the rectangle is doubled, the area of the rectangle doubles, but the
area of the square quadruples. This means that code that expects to interact with
rectangles might get surprising results when using a square, even if the square,
mathematically, is a rectangle.
A more general rule is to use the Liskov Substitution Principle: if the A class inherits
from B, we could replace the A class anywhere the B class is used, and the code would
still behave correctly.
Up to now, we have seen examples of single inheritance: a derived class has a single
base class. C++ supports multiple inheritance: a class can derive from multiple classes.
Let's look at an example:
struct A {
};
struct B {
};
struct C : A, B {
};
In this example, the C struct derives both from A and from B.
Inheritance | 247
The rules on how inheritance works are the same for single and multiple inheritance:
the methods of all the derived classes are visible based on the visibility access specified,
and we need to make sure to call the appropriate constructors and assign an operator
for all of the base classes.
Note
It is usually best to have a shallow inheritance hierarchy: there should not be many
levels of derived classes.
When using a multi-level inheritance hierarchy or multiple inheritance, it's more likely
that you'll encounter some problems, such as ambiguous calls.
A call is ambiguous when the compiler cannot clearly understand which method to call.
Let's explore the following example:
struct A {
void foo() {}
};
struct B {
void foo() {}
};
struct C: A, B {
void bar() { foo(); }
};
In this example, it is not clear which foo() to call, A's or B's. We can disambiguate that by
prepending the name of the class followed by two columns: A::foo().
248 | Object-Oriented Programming
class FutureCppDev {
public:
FutureCppDev(){
std::cout << "Welcome to the C++ Developer Community." <<
std::endl;
}
};
3. Now, add the Student class as illustrated here:
class Student : public DataScienceDev, public FutureCppDev {
public:
Student(){
std::cout << "Student is a Data Developer and C++ Developer." <<
std::endl;
}
};
4. Now, invoke the Student class in the main function:
int main(){
Student S1;
return 0;
}
Inheritance | 249
5. The Hero class should have a public method to cast a spell. Use the value from the
Spell class.
6. The Enemy class should have a public method to swing a sword, which prints
Swinging sword.
7. Implement the main method, which calls these methods in various classes:
int main()
{
Position position{"Enemy castle"};
Hero hero;
Enemy enemy;
}
The output will be as follows:
Moved to position Enemy castle
Moved to position Enemy castle
Casting spell fireball
Swinging sword
Note
The solution for this activity can be found on page 309.
Polymorphism
In the previous section, we mentioned that inheritance is a solution that allows you to
change the behavior of code while a program is running. This is because inheritance
enables polymorphism in C++.
Polymorphism means many forms and represents the ability of objects to behave in
different ways.
We mentioned earlier that templates are a way to write code that works with many
different types at compilation time and, depending on the types used to instantiate the
template, the behavior will change.
Polymorphism | 251
This kind of pattern is called static polymorphism – static because it is known during
compilation time. C++ also supports dynamic polymorphism – having the behavior of
methods change while the program is running. This is powerful because we can react
to information we obtain only after we have compiled our program, such as user input,
values in configurations, or the kind of hardware the code is running on. This is possible
thanks to two features – dynamic binding and dynamic dispatch.
Dynamic Binding
Dynamic binding is the ability for a reference or a pointer of a base type to point to an
object of a derived type at runtime. Let's explore the following example:
struct A {
};
struct B: A{
};
struct C: A {
};
A& ref1 = b;
A& ref2 = c;
A* ptr = nullptr;
if (runtime_condition()) {
ptr = &b;
} else {
252 | Object-Oriented Programming
ptr = &c;
}
Note
To allow dynamic binding, the code must know that the derived class derives from
the base class.
If the inheritance's visibility is private, then only code inside the derived class will
be able to bind the object to a pointer or reference of the base class.
If the inheritance is protected, then the derived class and every class deriving from
it will be able to perform dynamic binding. Finally, if the inheritance is public, the
dynamic binding will always be allowed.
This creates the distinction between the static type and the dynamic (or run-time) type.
The static type is the type we can see in the source code. In this case, we can see that
ref1 has a static type of a reference to the A struct.
The dynamic type is the real type of the object: the type that has been constructed in
the object's memory location at runtime. For example, the static type of both ref1 and
ref2 is a reference to the A struct, but the ref1 dynamic type is B, since ref1 refers to a
memory location in which an object of type B has been created, and the ref2 dynamic
type is C for the same reason.
As said, the dynamic type can change at runtime. While the static type of a variable is
always the same, its dynamic type can change: ptr has a static type, which is a pointer
to A, but its dynamic type could change during the execution of the program:
A* ptr = &b; // ptr dynamic type is B
ptr = &c; // ptr dynamic type is now C
It is important to understand that only references and pointers can be assigned values
from a derived class safely. If we were to assign an object to a value type, we would get a
surprising result – the object would get sliced.
We said earlier that a base class is embedded inside a derived class. Say, for example, we
were to try and assign to a value, like so:
B b;
A a = b;
Polymorphism | 253
The code would compile, but only the embedded part of A inside of B would be copied
– when we declare a variable of type A, the compiler dedicates an area of the memory
big enough to contain an object of type A, so there cannot be enough space for B. When
this happens, we say that we sliced the object, as we took only a part of the object when
assigning or copying.
Note
It is not the intended behavior to slice the object. Be mindful of this interaction and
try to avoid it.
This behavior happens because C++ uses static dispatch by default for function and
method calls: when the compiler sees a method call, it will check the static type
of the variable on which the method is called, and it will execute the respective
implementation. In case of slicing, the copy constructor or assignment operator of A is
called, and it only copies the part of A inside B, ignoring the remaining fields.
As said before, C++ supports dynamic dispatch. This is done by marking a method with a
special keyword: virtual.
If a method is marked with the virtual keyword, when the method is called on a
reference or a pointer, the compiler will execute the implementation of the dynamic
type instead of the static type.
These two features enable polymorphism – we can write a function that accepts a
reference to a base class, call methods on this base class, and the methods of the
derived classes will be executed:
void safeTurnOn(Vehicle& vehicle) {
if (vehicle.getFuelInTank() > 0.1 && vehicle.batteryHasEnergy()) {
vehicle.turnOn();
}
}
We can then call the function with many different types of vehicles, and the appropriate
methods will be executed:
Car myCar;
Truck truck;
safeTurnOn(myCar);
safeTurnOn(truck);
254 | Object-Oriented Programming
A typical pattern is to create an interface that only specifies the methods that are
required for some functionality.
Classes that need to be used with such functionality must derive the interface and
implement all the required methods.
Virtual Methods
We've learned the advantages of dynamic dispatch in C++ and how it can enable us to
execute the methods of a derived class by calling a method on a reference or pointer to
a base class.
In this section, we will take an in-depth look at how to tell the compiler to perform
dynamic dispatch on a method. The way to specify that we want to use dynamic
dispatch for a method is to use the virtual keyword.
The virtual keyword is used in front of a method when declaring it:
class Vehicle {
public:
virtual void turnOn();
};
We need to remember that the compiler decides how to perform method dispatch
based on the static type of the variable that is used when calling the method.
This means that we need to apply the virtual keyword to the type we are using in the
code. Let's examine the following exercise to explore the virtual keyword.
If the signature does not match, we will create an overload for the function. The
overload will be callable from the derived class, but it will never be executed with a
dynamic dispatch from a base class, for example:
struct Base {
virtual void foo(int) = 0;
};
struct Derived: Base {
/* This is an override: we are redefining a virtual method of the base
class, using the same signature. */
void foo(int) { }
When a class overrides a virtual method of the base class, the method of the most
derived class will be executed when the method is called on a base class. This is true
even if the method is called from inside the base class, for example:
struct A {
virtual void foo() {
std::cout << "A's foo" << std::endl;
}
};
struct B: A {
virtual void foo() override {
std::cout << "B's foo" << std::endl;
}
};
struct C: B {
virtual void foo() override {
Virtual Methods | 257
int main() {
B b;
C c;
A* a = &b;
Note
Always use the override keyword when you are overriding a method. It is easy
to change the signature of the base class and forget to update all the locations
where we overrode the method. If we do not update them, they will become a new
overload instead of an override!
In the example, we also used the virtual keyword for each function. This is not
necessary, since a virtual method on a base class makes every method with the same
signature in the derived classes virtual as well.
It is good to be explicit virtual keyword, but if we are already using the override
keyword, it might be redundant – in these cases, the best way is to follow the coding
standard of the project you are working on.
258 | Object-Oriented Programming
The virtual keyword can be applied to any method. Since the constructor is not a
method, the constructor cannot be marked as virtual. Additionally, dynamic dispatch is
disabled inside constructors and destructors.
The reason is that when constructing a hierarchy of derived classes, the constructor of
the base class is executed before the constructor of the derived class. This means that
if we were to call the virtual method on the derived class when constructing the base
class, the derived class would not be initialized yet.
Similarly, when calling the destructor, the destructors of the whole hierarchy are
executed in reverse order; first the derived and then the base class. Calling a virtual
method in the destructor would call the method on a derived class that has already
been destructed, which is an error.
While the constructor cannot be marked as virtual, the destructor can. If a class defines
a virtual method, then it should also declare a virtual destructor.
Declaring a destructor virtual is extremely important when classes are created on
dynamic memory, or the heap. We are going to see later in this chapter how to manage
dynamic memory with classes, but for now, it is important to know that if a destructor
is not declared virtual, then an object might be only partially destructed.
Note
If a method is marked virtual, then the destructor should also be marked virtual.
};
260 | Object-Oriented Programming
Note
The solution for this activity can be found on page 311.
Interfaces in C++
In the previous section, we saw how to define a method that is virtual, and how the
compiler will do dynamic dispatch when calling it.
We have also talked about interfaces throughout the chapter, but we never specified
what an interface is.
Interfaces in C++ | 261
An interface is a way for the code to specify a contract that the caller needs to provide
to be able to call some functionality. We looked at an informal definition when talking
about the templates and the requirements they impose on the types used with them.
Functions and methods which accepts parameters as interface are a way of saying: in
order to perform my actions, I need these functionalities; it's up to you to provide them.
To specify an interface in C++, we can use an Abstract Base Class (ABC).
Let's dive into the name; the class is:
• Abstract: This means that it cannot be instantiated
• Base: This means it is designed to be derived from
Any class that defines a pure virtual method is abstract. A pure virtual method is a
virtual method that ends with = 0, for example:
class Vehicle {
public:
virtual void turnOn() = 0;
};
A pure virtual method is a method that does not have to be defined. Nowhere in the
previous code have we specified the implementation of Vehicle::turnOn(). Because of
this, the Vehicle class cannot be instantiated, as we do not have any code to call for its
pure virtual methods.
We can instead derive from the class and override the pure virtual method. If a class
derives from an abstract base class, it can be either of the following:
• Another abstract base class if it declares an additional pure virtual method, or if it
does not override all the pure virtual methods of the base class
• A regular class if it overrides all the pure virtual methods of the base class
262 | Object-Oriented Programming
Note
If you try to instantiate an abstract base class, the compiler will give an error
specifying which methods are still pure virtual, thus making the class abstract.
Functions and methods that require specific methods can accept references and
pointers to abstract base classes, and instances of concrete classes that derive from
them can be bound to such references.
Note
It is good practice for the consumer of the interface to define the interface.
A function, method, or class that requires some functionality to perform its actions
should define the interface. Classes that should be used with such entities should
implement the interface.
Interfaces in C++ | 263
Since C++ does not provide a specialized keyword for defining interfaces and interfaces
are simply abstract base classes, there are some guidelines that it's best practice to
follow when designing an interface in C++:
• An abstract base class should not have any data members or fields.
The reason for this is that an interface specifies behavior, which should be
independent of the data representation. It derives that abstract base classes
should only have a default constructor.
• An abstract base class should always define a virtual destructor.
The definition of a destructor should be the default one: virtual ~Interface() =
default. We are going to see why it is important for the destructor to be virtual
later.
• All the methods of an abstract base class should be pure virtual.
The interface represents an expected functionality that needs to be implemented;
a method which is not pure is an implementation. The implementation should be
separate from the interface.
• All of the methods of an abstract base class should be public.
Similar to the previous point, we are defining a set of methods that we expect to
call. We should not limit which classes can call the method only to classes deriving
from the interface.
• All the methods of an abstract base class should be regarding a single functionality.
If our code requires multiple functionalities, separate interfaces can be created,
and the class can derive from all of them. This allows us to compose interfaces
more easily.
Consider disabling the copy and move constructors and assignment operators on the
interface. Allowing the interface to be copied can cause the slicing problem we were
describing before:
Car redCar;
Car blueCar;
With the last assignment, we only copied the Vehicle part, since the copy constructor
of the Vehicle class has been called. The copy constructor is not virtual, so the
implementation in Vehicle is called, and since it only knows about the data members
of the Vehicle class (which should be none), the ones defined inside Car have not been
copied! This results in problems that are very hard to identify.
A possible solution is to disable the interface copy and move construct and assign
operator: Interface(const Interface&) = delete; and similar. This has the drawback of
disabling the compiler from creating the copy constructor and assign operators of the
derived classes.
An alternative is to declare copy/move constructor/assignment protected so that only
derived classes can call them, and we don't risk assigning interfaces while using them.
class UserProfileStorage {
public:
virtual UserProfile getUserProfile(const UserId& id) const = 0;
protected:
UserProfileStorage() = default;
UserProfileStorage(const UserProfileStorage&) = default;
UserProfileStorage& operator=(const UserProfileStorage&) = default;
};
Interfaces in C++ | 265
Note
The solution for this activity can be found at page 312.
266 | Object-Oriented Programming
Dynamic Memory
In this chapter, we have come across the term dynamic memory. Now let's understand
in more detail what dynamic memory is, what problems it solves, and when to use it.
Dynamic memory is the part of the memory that the program can use to store objects,
for which the program is responsible for maintaining the correct lifetime.
It is usually also called the heap and is often the alternative to the stack, which instead
is handled automatically by the program. Dynamic memory can usually store much
larger objects than the stack, which usually has a limit.
A program can interact with the operating system to get pieces of dynamic memory
that it can use to store objects, and later it must take care to return such memory to the
operating system.
Historically, developers would make sure they called the appropriate functions to get
and return memory, but modern C++ automates most of this, so it is much easier to
write correct programs nowadays.
In this section, we are going to show how and when it is recommended to use dynamic
memory in a program.
Let's start with an example: we want to write a function that will create a logger. When
we execute tests, we create a logger specifically for the test called TestLogger, and when
we run our program for users, we want to use a different logger, called ReleaseLogger.
We can see a good fit for interfaces here – we can write a logger abstract base class
that defines all the methods needed for logging and have TestLogger and ReleaseLogger
derive from it.
All our code will then use a reference to the logger when logging.
How can we write such a function?
As we learned in Chapter 2, Functions, we cannot create the logger inside the function
and then return a reference to it, since it would be an automatic variable and it would
be destructed just after the return, leaving us with a dangling reference.
We cannot create the logger before calling the function and let the function initialize
it either, since the types are different, and the function knows which type should be
created.
We would need some storage that is valid until we need the logger, to put the logger in
it.
Dynamic Memory | 267
Given only an interface, we cannot know the size of the classes implementing it, since
multiple classes could implement it and they could have different sizes. This prevents
us from reserving some space in memory and passing a pointer to such space to the
function, so that it could store the logger in it.
Since classes can have different sizes, the storage not only needs to remain valid longer
than the function, but it also needs to be variable. That is dynamic memory!
In C++, there are two keywords to interact with dynamic memory – new and free.
The new expression is used to create a new object in dynamic memory – it is composed
by the new keyword, followed by the type of the object to create and the parameters to
pass to the constructor, and returns a pointer to the requested type:
Car* myCar = new myCar();
The new expression requests a piece of dynamic memory big enough to hold the object
created and instantiates an object in that memory. It then returns a pointer to such an
instance.
The program can now use the object pointed to by myCar until it decides to delete it.
To delete a pointer, we can use the delete expression: it is composed by the delete
keyword followed by a variable, which is a pointer:
delete myCar;
The delete keyword calls the destructor of the object pointed to by the pointer
provided to it, and then gives the memory we initially requested back to the operating
system.
Deleting pointers to automatic variables lead to an error as follows:
Car myCar; // automatic variable
delete &myCar; // This is an error and will likely crash the program
It is of absolute importance that, for each new expression, we call the delete expression
only once, with the same returned pointer.
If we forget to call the delete function on an object returned by calling the new function,
we will have two major problems:
• The memory will not be returned to the operating system when we do not need it
anymore. This is known as a memory leak. If this repeatedly happens during the
execution of the program, our program will take more and more memory, until it
consumes all the memory it can get.
• The destructor of the object will not be called.
268 | Object-Oriented Programming
We saw in previous chapters that, in C++, we should make use of RAII and get the
resources we need in the constructor and return them in the destructor.
If we do not call the destructor, we might not return some resources. For example, a
connection to the database would be kept open, and our database would struggle due
to too many connections being open, even if we are using only one.
The problem that arises if we call delete multiple times on the same pointer is that all
the calls after the first one will access memory they should not be accessing.
The result can range from our program crashing to deleting other resources our
program is currently using, resulting in incorrect behavior.
We can now see why it is extremely important to define a virtual destructor in the base
class if we derive from it: we need to make sure that the destructor of the runtime
type is called when calling the delete function on the base object. If we call delete on a
pointer to the base class while the runtime type is the derived class, we will only call the
destructor of the base class and not fully destruct the derived class.
Making the destructor of the base class virtual will ensure that we are going to call the
derived destructor, since we are using dynamic dispatch when calling it.
Note
For every call to the new operator, there must be exactly one call to delete with the
pointer returned by new!
We need to call delete[] on the pointer returned by new[] when we do not need the
objects anymore.
Note
For every call to new[], there must be exactly one call to delete[] with the pointer
returned by new[].
The new operator and new[] function calls, and delete and delete[] function
calls, cannot be intermixed. Always pair the ones for an array or the ones for single
elements!
Now that we have seen how to use dynamic memory, we can write the function to
create our logger.
The function will call the new expression in its body to create an instance of the correct
class, and it will then return a pointer to the base class so that the code calling it does
not need to know about the type of logger created:
Logger* createLogger() {
if (are_tests_running()) {
TestLogger* logger = new TestLogger();
return logger;
} else {
ReleaseLogger logger = new ReleaseLogger("Release logger");
return logger;
}
}
From these two points, we can can see why it is error prone to manage memory
manually, and why it should be avoided whenever possible.
Let's look at an example of how to call the function correctly:
Logger* logger = createLogger();
myOperation(logger, argument1, argument2);
delete logger;
If myOperation does not call delete on the logger, this is a correct use of dynamic
memory. Dynamic memory is a powerful tool, but doing it manually is risky, error prone,
and easy to get wrong.
Fortunately, modern C++ provides some facilities to make all this much easier to do. It is
possible to write entire programs without ever using new and delete directly.
We will see how in the next section.
Usually, ownership is associated with the scope a function or method, since the lifetime
of automatic variables is controlled by it:
void foo() {
int number;
do_action(number);
}
In this case, the scope of foo() owns the number object, and it will make sure it is
destroyed when the scope exits.
Alternatively, classes might own objects when they are declared as value types between
the data members of the class. In that case, the lifetime of the object will be the same as
the lifetime of the class:
class A {
int number;
};
number will be constructed when the A class is constructed and will be destroyed when
the A class is destroyed. This is automatically done because the field number is embedded
inside the class and the constructor and destructor of the class will automatically
initialize number.
When managing objects in dynamic memory, ownership is not enforced by the compiler
anymore, but it is helpful to apply the concept of ownership to the dynamic memory as
well – the owner is who decides when to delete the object.
A function could be the owner of an object when the object is allocated with the new
call inside the function, as in the following example:
void foo() {
int* number = new number();
do_action(number);
delete number;
}
272 | Object-Oriented Programming
Or a class might own it, by calling new in the constructor and storing the pointer in its
fields, and calling delete on it in the destructor:
class A {
A() : number(new int(0)) {
}
~A() {
delete number;
}
int* number;
};
But the ownership of dynamic objects can also be passed around.
We looked at an example earlier with the createLogger function. The function creates
an instance of Logger and then passes the ownership to the parent scope. Now, the
parent scope is in charge of making sure the object is valid until it is accessed in the
program and deleted afterward.
Smart pointers allow us to specify the ownership in the type of the pointer and make
sure it is respected so that we do not have to keep track of it manually anymore.
Note
Always use smart pointers to represent the ownership of objects.
In a code base, smart pointers should be the pointers that control the lifetime of
objects, and raw pointers, or regular pointers, are used only to reference objects.
The unique pointer guarantees the uniqueness of ownership: the unique pointer cannot
be copied. This means that once we have created a unique pointer for an object, there
can be only one.
Additionally, when the unique pointer is destroyed, it deletes the object it owns. This
way, we have a concrete object that tells us the ownership of the created object, and we
do not have to manually make sure that only one place is calling delete for the object.
A unique pointer is a template that can take one argument: the type of the object.
We could rewrite the previous example as follows:
std::unique_ptr<Logger> logger = createLogger();
While this code would compile, we would not be respecting the guideline we mentioned
previously regarding always using smart pointers for ownership: createLogger returns a
raw pointer, but it passes ownership to the parent scope.
We can update the signature of createLogger to return a smart pointer:
std::unique_ptr<Logger>createLogger();
Now, the signature expresses our intention, and we can update the implementation to
make use of smart pointers.
As we mentioned earlier, with the use of smart pointers, code bases should not use
new and delete anywhere. This is possible because the standard library, since C++14,
offers a convenient function: std::make_unique. make_unique is a template function that
takes the type of the object to create, and creates it in dynamic memory, passing its
arguments to the object's constructor and returning a unique pointer to it:
std::unique_ptr<Logger>createLogger() {
if (are_tests_running()) {
std::unique_ptr<TestLogger> logger = std::make_unique<TestLogger>();
return logger; // logger is implicitly moved
} else {
std::unique_ptr<ReleaseLogger> logger = std::make_
unique<ReleaseLogger>("Release logger");
return logger; // logger is implicitly moved
}
}
274 | Object-Oriented Programming
Let's now see how we can rewrite the class that owns the number we showed before:
class A {
A(): number(std::make_unique<int>()) {}
std::unique_ptr<int> number;
};
Thanks to the fact that unique_ptr deletes the object automatically when it is destroyed,
we did not have to write the destructor for the class, making our code even easier.
If we need to pass a pointer to the object, without transferring ownership, we can use
the get() method on the raw pointer. Remember that raw pointers should not be used
for ownership, and code accepting the raw pointer should never call delete on it.
Thanks to these features, unique_ptr should be the default choice to keep track of the
ownership of an object.
Safe and Easy Dynamic Memory | 275
private:
std::vector<std::shared_ptr<Node>>d_connections;
};
Here, we can see that we are holding many shared_ptr instance to nodes. If we have
a shared_ptr instance to a node, we want to be sure that the node exists, but when
we remove the shared pointer, we do not care about the node anymore: it might be
deleted, or it might be kept alive if there is another node connected to it.
Similar to the unique_ptr counterpart, when we want to create a new node, we can
use the std::make_shared function, which takes the type of the object to construct as
the template argument and the arguments to pass to the constructor of the object and
returns shared_ptr to the object.
276 | Object-Oriented Programming
You might notice that there might be a problem in the example we showed: what
happens if node A is connected to node B and node B is connected to node A?
Both nodes have a shared_ptr instance to the other, and even if no other node has a
connection to them, they will remain alive because a shared_ptr instance to them exists.
This is an example of circular dependency.
When using shared pointers, we must pay attention to these cases. The standard library
offers a different kind of pointer to handle these situations: std::weak_ptr (read as weak
pointer).
weak_ptr is a smart pointer that can be used in conjunction with shared_ptr to solve the
circular dependencies that might happen in our programs.
Generally, shared_ptr is enough to model most cases where unique_ptr does not work,
and together they cover the majority of the uses of dynamic memory in a code base.
Lastly, we are not helpless if we want to use dynamic memory for arrays of which we
know the size only at runtime. unique_ptr can be used with array types, and shared_ptr
can be used with array types starting from C++17:
std::unique_ptr<int[]>ints = std::make_unique<int[]>();
std::shared_ptr<float[]>floats = std::make_shared<float[]>();
Note
The solution for this activity can be found at page 313.
4. Now let's write the main function, where we create a shared pointer to the
connection and then we call std::async with the two functions we defined above,
as illustrated:
int main()
{
std::shared_ptr<DatabaseConnection> connection = std::make_
shared<DatabaseConnection>();
std::async(std::launch::async, updateWithConnection, connection);
std::async(std::launch::async, scheduleWithConnection, connection);
}
The output is as follows:
Updating order and scheduling order processing in parallel
Schedule order processing
Updating order list
Note
The solution for this activity can be found at page 314.
Summary
In this chapter, we saw how inheritance can be used to combine classes in C++. We saw
what a base class is and what a derived class is, how to write a class that derives from
another, and how to control the visibility modifier. We talked about how to initialize a
base class in a derived one by calling the base class constructor.
We then explained polymorphism and the ability of C++ to dynamically bind a pointer
or reference of a derived class to a pointer or reference of the base class. We explained
what dispatch for functions is, how it works statically by default, and how we can make
it dynamic with the use of the virtual keyword. Following that, we explored how to
properly write virtual functions and how we can override them, making sure to mark
such overrode functions with the override keyword.
Next, we showed how to define interfaces with abstract base classes and how to
use pure virtual methods. We also provided guidelines on how to correctly define
interfaces.
Summary | 279
Lastly, we delved into dynamic memory and what problems it solves, but we also saw
how easy it is to use it incorrectly.
We concluded the chapter by showing how modern C++ makes using dynamic memory
painless by providing smart pointers that handle complex details for us: unique_ptr
to manage objects with a single owner, and shared_ptr for objects owned by multiple
objects.
All these tools can be effective at writing solid programs that can be effectively evolved
and maintained, while retaining the performance C++ is famous for.
Appendix
>
About
This section is included to assist the students to perform the activities in the book.
It includes detailed steps that are to be performed by the students to achieve the objectives of
the activities.
282 | Appendix
Activity 1: Find the Factors of 7 between 1 and 100 Using a while Loop
1. Import all the required header files before the main function:
#include <iostream>
2. Inside the main function, create a variable i of type unsigned, and initialize its value
as 1:
unsigned i = 1;
3. Now, use the while loop adding the logic where the value of i should be less than
100:
while ( i < 100){ }
4. In the scope of the while loop, use the if statement with the following logic:
if (i%7 == 0) {
std::cout << i << std::endl;
}
5. Increase the value of the i variable to iterate through the while loop to validate the
condition:
i++;
The output of the program is as follows:
7
14
21
28
...
98
2. Now, in the main function, create a bi-directional array named foo of type integer,
with three rows and three columns, as shown here:
int main()
{
int foo[3][3];
3. Now, we will use the concept of a nested for loop to iterate through each index
entry of the foo array:
for (int x= 0; x < 3; x++){
for (int y = 0; y < 3; y++){
}
}
4. In the second for loop, add the following statement:
foo[x][y] = x + y;
5. Finally, iterate over the array again to print its values:
for (int x = 0; x < 3; x++){
for (int y = 0; y < 3; y++){
std::cout << “foo[“ << x << “][“ << y << “]: “ << foo[x][y] <<
std::endl;
}
}
The output is as follows:
foo[0][0]: 0
foo[0][1]: 1
foo[0][2]: 2
foo[1][0]: 1
foo[1][1]: 2
foo[1][2]: 3
foo[2][0]: 2
foo[2][1]: 3
foo[2][2]: 4
284 | Appendix
Lesson 2: Functions
3. In the main function, create two arrays of type float and assign the following
values:
int main() {
std::array<float, 3> enemy1_location = {2, 2 ,0};
std::array<float, 3> enemy2_location = {2, 4 ,0};
4. Now, create a variable named enemy_distance of type float and use the distance
function to assign the value after calculating it:
float enemy_distance = johnny::mathlib::distance(enemy1_location,
enemy2_location);
float distance_from_center = johnny::mathlib::distance(enemy1_
location);
5. Using the circumference function of mathlib.h, calculate and assign the enemy
visual radius to view_circumference_for_enemy of type float:
using johnny::mathlib::circumference;
float view_circumference_for_enemy = circumference(ENEMY_VIEW_RADIUS_
METERS);
6. Create a variable named total_distance of type float and assign the distance
difference between the two enemies as shown in the following code:
float total_distance = johnny::mathlib::total_walking_distance({
enemy1_location,
{2, 3, 0}, // y += 1
{2, 3, 3}, // z += 3
{5, 3, 3}, // x += 3
{8, 3, 3}, // x += 3
{8, 3, 2}, // z -= 1
{2, 3, 2}, // x -= 6
{2, 3, 1}, // z -= 1
{2, 3, 0}, // z -= 1
enemy2_location
});
7. Print the output using the following print statement:
std::cout << “The two enemies are “ << enemy_distance << “m apart and
can see for a circumference of “
<< view_circumference_for_enemy << “m. To go to from one to
the other they need to walk “
<< total_distance << “m.”;
}
288 | Appendix
Lesson 3: Classes
public:
void set_latitude(float value){}
void set_longitude(float value){}
float get_latitude(){}
float get_longitude(){}
};
3. The four methods should now be implemented. The setters assign the given value
to the corresponding members they are supposed to set; the getters return the
values that are stored.
class Coordinates {
private:
float latitude;
float longitude;
Lesson 3: Classes | 289
public:
void set_latitude(float value){ latitude = value; }
void set_longitude(float value){ longitude = value; }
float get_latitude(){ return latitude; }
float get_longitude(){ return longitude; }
};
An example is as follows:
#include <iostream>
int main() {
Coordinates washington_dc;
std::cout << “Object named washington_dc of type Coordinates created.”
<< std::endl;
washington_dc.set_latitude(38.8951);
washington_dc.set_longitude(-77.0364);
std::cout << “Object’s latitude and longitude set.” << std::endl;
2. Then, the class is extended with a public constructor which takes two arguments
used to initialize the data members of the class:
class Coordinates {
public:
Coordinates(float latitude, float longitude)
: _latitude(latitude), _longitude(longitude) {}
private:
int _latitude;
int _longitude;
};
3. We can also add getters as seen previously to access the class members. An
example is as follows:
#include <iostream>
int main() {
Coordinates washington_dc(38.8951, -77.0364);
std::cout << “Object named washington_dc of type Coordinates created.”
<< std::endl;
~managed_array() {
delete[] array;
std::cout << “Array deleted.” << std::endl;
}
private:
int *array;
};
4. We can use our managed_array class as follows:
int main() {
managed_array m(10);
}
The output will be as follows:
Array of size 10 created.
Array deleted.
2. The AppleTree class is defined and contains a method called createFruit that is in
charge of creating an Apple and returning it:
#include <iostream>
class AppleTree
{
public:
Apple createFruit(){
Apple apple;
std::cout << “apple created!” << std::endl;
return apple;
}
};
3. If we compile this code, we will get an error. At this point, the Apple constructor
is private, so the AppleTree class cannot access it. We need to declare the Apple-
Tree class as a friend of Apple to allow AppleTree to access the private methods of
Apple:
class Apple
{
friend class AppleTree;
private:
Apple() {}
// do nothing
}
4. The Apple object can now be constructed using the following code:
int main() {
AppleTree tree;
Apple apple = tree.createFruit();
}
This prints the following:
apple created!
Lesson 3: Classes | 293
int x;
int y;
};
2. At this point, we are able to compare the two Point objects:
#include <iostream>
int main() {
Point p_1, p_2;
p_1.x = 1;
p_1.y = 2;
p_2.x = 2;
p_2.y = 1;
private:
int x;
};
2. Extend it with the call operator operator() which takes an int as a parameter and
returns an int. The implementation in the function body should return the addi-
tion of the previously defined x value and the parameter of the function named y:
class AddX {
public:
AddX(int x) : x(x) {}
int operator() (int y) { return x + y; }
private:
int x;
};
3. Instantiate an object of the class just defined and invoke the call operator:
int main() {
AddX add_five(5);
std::cout << add_five(4) << std::endl;
}
The output will be as follows:
9
Lesson 04: Generic Programming and Templates | 295
private:
Currency balance;
};
3. Next, we create the method addToBalance. It should be a template with one type
parameter, the other currency. The method takes a value of OtherCurrency and
converts it to the value of the currency of the current account with the to() func-
tion, specifying to which currency the value should be converted to. It then adds it
to the balance:
template<typename OtherCurrency>
void addToBalance(OtherCurrency amount) {
balance.d_value += to<Currency>(amount).d_value;
}
4. Finally, we can try to call our class in the main function with some data:
Account<GBP> gbpAccount(GBP(1000));
// Add different currencies
std::cout << “Balance: “ << gbpAccount.getBalance().d_value << “ (GBP)” <<
std::endl;
gbpAccount.addToBalance(EUR(100));
std::cout << “+100 (EUR)” << std::endl;
std::cout << “Balance: “ << gbpAccount.getBalance().d_value << “ (GBP)” <<
std::endl;
The output of the program is:
Balance: 1000 (GBP)
+100 (EUR)
Balance: 1089 (GBP)
298 | Appendix
4. For convenience, we define a function to print the class as well. We print all the
elements in the columns separated by spaces, with one column per line:
template<typename T, size_t R, size_t C>
std::ostream& operator<<(std::ostream& os, Matrix<T, R, C> matrix) {
os << ‘\n’;
for(int r=0; r < R; r++) {
for(int c=0; c < C; c++) {
os << matrix.get(r, c) << ‘ ‘;
}
os << “\n”;
}
return os;
}
5. In the main function, we can now use the functions we have defined:
Matrix<int, 3, 2> matrix({
1, 2,
3, 4,
5, 6
});
std::cout << “Initial matrix:” << matrix << std::endl;
matrix.get(1, 1) = 7;
std::cout << “Modified matrix:” << matrix << std::endl;
The output is as follows:
Initial matrix:
1 2
3 4
5 6
Modified matrix:
1 2
3 7
5 6
300 | Appendix
Multiply multiplier;
public:
Matrix() : data({}), multiplier() {}
Matrix(std::array<T, R*C> initialValues) : data(initialValues),
multiplier() {}
};
The get() function remains the same as the previous activity.
3. We now need to make sure that the Multiply method uses the Multiply type
provided by the user to perform the multiplication.
4. To do so, we need to make sure to call multiplier(operand1, operand2) instead of
operand1 * operand2, so that we use the instance we stored inside the class:
std::array<T, R> multiply(const std::array<T, C>& vector) {
std::array<T, R> result = {};
for(int r = 0; r < R; r++) {
for(int c = 0; c < C; c++) {
result[r] += multiplier(get(r, c), vector[c]);
}
}
return result;
}
5. We can now add an example of how we can use the class:
// Create a matrix of int, with the ‘plus’ operation by default
Matrix<3, 2, int, std::plus<int>> matrixAdd({
1, 2,
3, 4,
5, 6
});
Activity 17: Ensure Users are Logged in When Performing Actions on the Ac-
count
1. We first declare a template function which takes two type parameters: an Action
and a Parameter type.
2. The function should take the user identification, the action and the parameter.
The parameter should be accepted as a forwarding reference. As a first step, it
should check if the user is logged in, by calling the isLoggenIn() function. If the
user is logged in, it should call the getUserCart() function, then call the action
passing the cart and forwarding the parameter:
template<typename Action, typename Parameter>
void execute_on_user_cart(UserIdentifier user, Action action, Parameter&&
parameter) {
if(isLoggedIn(user)) {
Cart cart = getUserCart(user);
action(cart, std::forward<Parameter>(parameter));
} else {
std::cout << “The user is not logged in” << std::endl;
}
}
3. We can test how execute_on_user_cart works by calling it in the main function:
Item toothbrush{1023};
Item toothpaste{1024};
UserIdentifier loggedInUser{0};
std::cout << “Adding items if the user is logged in” << std::endl;
execute_on_user_cart(loggedInUser, addItems,
std::vector<Item>({toothbrush, toothpaste}));
UserIdentifier loggedOutUser{1};
std::cout << “Removing item if the user is logged in” << std::endl;
execute_on_user_cart(loggedOutUser, removeItem, toothbrush);
The output is as follows:
Adding items if the user is logged in
Items added
Removing item if the user is logged in
The user is not logged in
Lesson 04: Generic Programming and Templates | 303
Activity 18: Safely Perform Operations on User Cart with an Arbitrary Number
of Parameters
1. We need to expand the previous activity to accept any number of parameters with
any kind of ref-ness and pass it to the action provided. To do so, we need to create
a variadic template.
2. Declare a template function that takes an action and a variadic number of param-
eters as template parameters. The function parameters should be the user action,
the action to perform, and the expanded template parameter pack, making sure
that the parameters are accepted as forwarding references.
3. Inside the function, we perform the same checks as before, but now we expand
the parameters when we forward them to the action:
template<typename Action, typename... Parameters>
void execute_on_user_cart(UserIdentifier user, Action action,
Parameters&&... parameters) {
if(isLoggedIn(user)) {
Cart cart = getUserCart(user);
action(cart, std::forward<Parameters>(parameters)...);
} else {
std::cout << “The user is not logged in” << std::endl;
}
}
4. Let’s test the new function in our main function:
Item toothbrush{1023};
Item apples{1024};
UserIdentifier loggedInUser{0};
std::cout << “Replace items if the user is logged in” << std::endl;
execute_on_user_cart(loggedInUser, replaceItem, toothbrush, apples);
UserIdentifier loggedOutUser{1};
std::cout << “Replace item if the user is logged in” << std::endl;
execute_on_user_cart(loggedOutUser, removeItem, toothbrush);
304 | Appendix
2. Then, create the class with a constructor that sets the current state of the airplane
to AtGate:
class Airplane {
std::variant<AtGate, Taxi, Flying> state;
public:
Airplane(int gate) : state(AtGate{gate}) {
std::cout << “At gate “ << gate << std::endl;
}
};
3. Now, implement the startTaxi() method. First, check the current state of the
airplane with std::holds_alternative<>(), and if the airplane is not in the correct
state, write an error message and return.
4. If the airplane is in the correct state, change the state to taxi by assigning it to the
variant:
void startTaxi(int lane, int numPassengers) {
if (not std::holds_alternative<AtGate>(state)) {
std::cout << “Not at gate: the plane cannot start taxi to lane “
<< lane << std::endl;
return;
}
std::cout << “Taxing to lane “ << lane << std::endl;
state = Taxi{lane, numPassengers};
}
5. We repeat the same process for the takeOff() method:
void takeOff(float speed) {
if (not std::holds_alternative<Taxi>(state)) {
std::cout << “Not at lane: the plane cannot take off with speed “
<< speed << std::endl;
return;
}
std::cout << “Taking off at speed “ << speed << std::endl;
state = Flying{speed};
}
308 | Appendix
};
};
4. Create a class Spell with the constructor that prints the name of the person
casting the spell:
class Spell {
public:
Spell(std::string name) : d_name(name) {}
return d_name;
}
private:
std::string d_name;
};
5. The class Hero should have a public method to cast a spell. Use the value from the
Spell class:
public:
void cast(Spell spell) {
// Cast the spell
std::cout << “Casting spell “ << spell.name() << std::endl;
}
6. The class Enemy should have a public method to swing a sword which prints Swing-
ing sword:
public:
void swingSword() {
// Swing the sword
std::cout << “Swinging sword” << std::endl;
}
7. Implement the main method that calls these methods in various classes:
int main()
{
Position position{“Enemy castle”};
Hero hero;
Enemy enemy;
}
};
3. We then derive a Manager class from it, overriding the method for computing the
bonus. We might also want to override getBaseSalary if we want to give a different
base salary to managers:
class Manager : public Employee {
public:
virtual int getBaseSalary() const override { return 150; }
virtual int getBonus(const Deparment& dep) const override {
if (dep.hasReachedTarget()) {
int additionalDeparmentEarnings = dep.effectiveEarning() - dep.
espectedEarning();
return 0.2 * getBaseSalary() + 0.01 * additionalDeparmentEarnings;
}
return 0;
}
};
312 | Appendix
5. Now, in the main function, call the Department, Employee, and Manager classes as
shown:
int main()
{
Department dep;
Employee employee;
Manager manager;
std::cout << “Employee: “ << employee.getTotalComp(dep) << “. Manager: “
<< manager.getTotalComp(dep) << std::endl;
}
class UserProfileStorage {
public:
virtual UserProfile getUserProfile(const UserId& id) const = 0;
protected:
UserProfileStorage() = default;
UserProfileStorage(const UserProfileStorage&) = default;
UserProfileStorage& operator=(const UserProfileStorage&) = default;
};
Lesson 6: Object-Oriented Programming | 313
3. In the main function, call the UserProfileCache class and exampleOfUsage function
as shown:
int main()
{
UserProfileCache cache;
exampleOfUsage (cache);
}
#include <iostream>
#include <memory>
#include <userprofile_activity18.h>
class UserProfileStorageFactory {
public:
std::unique_ptr<UserProfileStorage> create() const {
return std::make_unique<UserProfileCache>();
}
};
314 | Appendix
4. To model this, we use a shared_ptr. Remember that we need a copy of the shared_
ptr to exist in order for the connection to remain valid:
/* We need to get a copy of the shared_ptr so it stays alive until this
function finishes. */
void scheduleWithConnection(std::shared_ptr<DatabaseConnection>
connection) {
scheduleOrderProcessing(*connection);
}
5. Create the main function as follows:
int main()
{
std::shared_ptr<DatabaseConnection> connection = std::make_
shared<DatabaseConnection>();
std::async(std::launch::async, updateWithConnection, connection);
std::async(std::launch::async, scheduleWithConnection, connection);
}
>
Index
About
All major keywords used in this book are captured alphabetically in this section. Each one is
accompanied by the page number of where they appear.
179, 206, 210, 224-225,
A additional: 21, 72, 79,
154-155, 166, 170, 199, 268, 273-275, 290
ability: 2, 45, 79, 81, 92, 202, 204, 209, 216, 218, arises: 33, 124, 268
101, 145, 160, 179, 218, 245, 261-262, 270 arithmetic: 1, 40, 218
250-251, 274, 278 address: 15-16, 21, arrays: 1, 30, 36, 38-40, 72,
access: 16, 37-38, 40, 96, 119, 121, 210 184, 186, 268, 276, 287
50-53, 57, 61, 76-78, advantage: 73, 101, assign: 40, 62, 67, 70,
83-84, 86-89, 92, 94, 155, 166, 190 84, 91, 99, 121, 123,
96, 99, 108, 111-114, 116, advantages: 40, 44, 84, 203, 209, 215, 247,
130, 138, 145, 148, 151, 130, 144, 168, 254 252, 264, 287-288
155-156, 162, 166, 169, algorithm: 45, 67, 134, 182, assigned: 70, 126, 214, 252
172-173, 175, 178-179, 224-225, 228, 230-235 assignment: 13, 16, 69-70,
182, 184, 186, 188, aliases: 150, 173, 83, 117, 119-121, 123-124,
190-191, 195, 199-201, 175, 177, 179 245, 253, 263-264
209, 211, 215, 217-218, allocate: 37, 111, 159 associate: 90, 195, 236
225-226, 237, 241-242, allocated: 106, 111, 271, 290 associated: 11, 17, 53,
247, 268, 288-290, 292, allocation: 111, 185, 290 86, 90, 93, 98, 111,
298, 300, 304, 306 almost: 44, 174, 207 194-196, 207, 271
account: 137-138, 142-144, amount: 45, 137-138, assume: 4, 89, 121, 201
152-154, 168, 176, 191, 155, 159, 296-297 automatic: 53, 56, 59, 67,
196-197, 234-236, anymore: 32, 167, 178, 70, 106, 266-267, 271
295-297, 302, 304-305 187, 217, 267, 269,
action: 7, 96, 163,
165-172, 174-175,
271-272, 275
anywhere: 22, 65,
B
230, 271, 302-303 87-88, 90, 246, 273 balance: 137, 153-154,
actions: 40, 168-170, 174, applies: 16, 190, 262 176, 191, 196-197,
225, 261-262, 302 approach: 85, 149, 240 224, 234-236,
activity: 36, 40, 63, 72, arbitrary: 58, 143, 168, 296-297, 304-305
78, 80, 98-99, 108-109, 170-171, 174, 182, balances: 176, 197, 223,
111, 116-117, 127-130, 191, 240, 303-304 234-236, 304-305
151, 153-154, 156-157, argument: 58-61, 63-64, because: 5, 15, 44, 51, 53,
162-163, 168, 170, 71-72, 75, 97, 103, 106, 60, 67, 72-73, 76-77,
174-175, 191, 196-197, 117, 121, 127, 139-140, 79-80, 100-101, 113,
201-202, 214, 249-250, 153, 158, 160-163, 116, 124, 136-137, 144,
258, 260, 264-265, 173-174, 179, 193, 150, 161, 167-168, 177,
276-278, 282, 284-286, 197, 210, 226, 270, 193, 196-198, 210, 219,
288-291, 293-296, 298, 273, 275, 293, 305 221, 224, 243, 250-251,
300-306, 309, 311-314 arguments: 9, 43, 46, 253, 257, 261, 264, 268,
actual: 4, 15-16, 186, 226 57-60, 65, 67, 70, 72-73, 271, 273-274, 276, 291
addition: 40, 93, 95, 101, 75-76, 79, 81, 108, before: 4-5, 13, 32, 36,
126, 129, 210, 294 128, 136, 139-140, 142, 48, 53-55, 68-69, 76,
158-161, 163, 170-174, 87, 94, 102-104, 127,
129, 136, 141, 158, 161, charge: 116, 272, 292 140-141, 148, 151, 155,
164, 166, 168-169, 184, charlie: 196, 236, 305-306 184, 253-254, 273, 292
200, 211, 223, 244, 253, checked: 29-30, 32, compiler: 3-4, 7, 9, 13-14,
258, 263, 266, 272, 121, 141, 184 18-19, 25, 38-39, 45, 47,
274, 282, 295, 303 checks: 6, 136, 143, 49, 53-54, 58, 66, 75,
behave: 202, 246, 250, 270 155, 170, 184, 303 77-80, 100, 103, 108, 117,
behavior: 38, 58, 79, 93, choice: 2, 210, 274 119-120, 123, 125-126,
100, 124, 200, 250-251, classes: 5, 13, 19, 50-51, 61, 136-137, 139, 141-143,
253, 263, 268 64, 68, 81, 83-84, 87-88, 145, 147-148, 151, 155,
benefit: 110, 147, 166 93, 105-106, 108, 111-113, 159-161, 163, 165, 172,
better: 58, 69, 76-77, 116, 119, 124, 130, 133, 203, 213, 216, 225,
79, 145, 168, 199 135-137, 144, 147-150, 244-245, 247, 253-255,
binary: 125, 192, 155, 166, 171, 174, 257, 260, 262, 264, 271
218, 225, 233 182-183, 193, 195, 199, compilers: 4, 7, 10,
blueprint: 88, 90, 146 202, 206, 225, 239-244, 68, 105, 245
boolean: 11, 195, 293 246-250, 253-254, compiles: 6, 143, 151
bounds: 38, 156, 298 257-258, 262-264, complete: 53, 91, 97,
braces: 9, 22, 25, 37, 39, 47 267, 270-271, 274, 278, 191, 213, 286
brackets: 25, 86, 288, 309-310, 312 complex: 2, 26, 40, 66,
135-136, 147, 159, 191 classname: 86-87, 90, 93 70, 79, 81, 84, 88, 144,
built-in: 1, 11-12, 40, 62, collection: 88, 206, 219 160, 173, 175, 216, 279
71, 108, 130, 268 column: 38, 156, 298-299 complexity: 85,
columns: 156, 162, 247, 192, 198, 270
C 283, 298-299
combine: 84, 179, 278
compose: 90, 239, 263
composed: 9, 44, 46-47,
calculate: 59, 63, 287 combined: 130, 57, 84, 200, 210, 215, 267
caller: 44, 46, 57-58, 61, 144, 178, 182 compute: 80, 85, 157,
64-68, 73-75, 160, common: 4, 7, 13, 25, 184, 234, 258, 300
168, 211, 261, 274 44, 64, 70-71, 86, 92, computed: 55-56, 81,
capacity: 186-187, 110, 134, 150, 156-157, 145, 198-199, 201, 258
203-204, 242-243 173-174, 178, 182, 202, concept: 15, 36, 43-45,
categories: 11-12, 216, 224, 228, 237, 268 51, 74, 79, 81, 84, 88,
218, 221, 237 comparator: 148-149, 95, 101-102, 114, 130,
certain: 29, 45, 153, 182 159, 176, 192-194, 197 179, 206, 237, 240,
chapter: 136, 141, 143, 146, compare: 127, 138, 146, 176, 254, 262, 271, 283
166, 215, 258, 260 193, 197, 215, 293, 305 concrete: 139, 262, 273
character: 11, 13, 75, compared: 71, 192, condition: 25-30,
153, 156, 203-204, 198, 204, 215 32-33, 35, 78, 126,
249, 309-310 comparison: 128, 145, 208, 251, 282, 284
characters: 11, 60, 142, 176, 192, 204, 293 conditions: 26, 28, 78, 123
151-152, 202-204, compile: 3, 7-10, 19, 37, connected: 90,
237, 249, 309 47-48, 50, 77, 116, 136, 153, 275-276
connection: 151-153, 268, contrary: 50, 52, 62, 119 creation: 116, 176, 184, 206
275-278, 295, 314-315 control: 1-2, 24-26, 28, 40, current: 30, 33, 56,
consider: 143, 161, 48, 124, 130, 272, 278 59-60, 63, 77-78, 89,
176, 240, 263 convenient: 61, 75, 96, 117, 141, 147, 151,
considered: 38, 105, 86, 96, 237, 273 153-154, 186, 208,
186-187, 234 convention: 5, 7, 88 212-214, 218, 227,
consists: 2, 6, 29, 34 converted: 79, 203, 297 242-243, 296-297, 307
console: 31, 46, 48, 62-63 converts: 4, 103, 297 customers: 223, 234, 264
constant: 18-19, 21, coordinate: 85, 88-89,
28, 37, 55, 80, 139,
198, 203, 218
107-108, 112, 127, 289
copies: 117, 119, 145,
D
const-ness: 74, 185, 217-218, 253 database: 264,
95, 164, 255 correct: 6, 38, 66, 102, 268, 277, 314
construct: 29, 118-119, 154, 158, 211, 244, 266, decide: 67, 74-75, 85,
121, 123, 203, 264, 275 269-270, 286, 307 166, 169, 198-199
contain: 4, 13, 50, 90, counts: 48, 69, 221 decided: 57, 182, 268
101, 170-172, 191, course: 87, 166, 172, 281 decides: 163, 254,
202, 207, 210, 253 create: 1-2, 4, 8, 10, 36, 43, 267, 270-272
container: 61, 140, 47, 49-51, 56, 58-59, declare: 11, 13, 15, 18-19,
142-143, 145, 150-151, 61-63, 76, 78, 81, 84-85, 29, 40, 43, 49, 53, 58,
177-179, 181-184, 88-90, 92, 101-103, 108, 66, 76, 79, 81, 83-84,
187-188, 190, 192-193, 111-112, 115-116, 130, 137, 89, 94, 99, 111, 113,
199-200, 215-216, 144-145, 152-153, 156, 115-117, 142, 144-145,
219-224, 228, 232 163, 170, 175-176, 179, 175, 191, 218, 223, 253,
containers: 112, 136, 190, 196, 201, 206-207, 255, 258, 264, 292,
143, 146, 150, 173, 177, 209-210, 214, 225, 237, 295-296, 302-303, 305
179, 181-182, 190-194, 239-240, 242, 248-249, declared: 13-14, 19, 22-23,
197-199, 202, 206, 222, 254, 256, 258, 260, 30, 36-37, 45-46,
224, 236-237, 304 264, 266-270, 273, 50, 53, 57, 67, 76-77,
contains: 4, 9, 77, 90, 98, 275-278, 282-287, 289, 80, 87, 90, 94-95,
116, 119, 136, 150-151, 291, 297-298, 301, 303, 105, 111-113, 117, 120,
176, 183-185, 187, 192, 305-309, 311-315 136-137, 140, 144, 147,
207-209, 212-213, 292 created: 19, 56, 59-60, 149, 151, 155, 158, 161,
content: 4-6, 16, 37, 53, 62, 67, 70, 78, 100, 102, 165, 171, 173, 175, 179,
121, 212, 230-231 146-147, 162, 184-185, 255, 258, 271, 304
context: 45, 96, 270 190, 225-226, 237, 252, declares: 50, 77, 113,
contiguous: 36, 258, 263, 266-269, 149, 178, 255, 261
183-184, 186, 202 273-274, 289-292, 295 deduce: 14, 136,
continue: 25, 29, creates: 5-6, 53, 60, 67, 141, 148, 161
32-33, 262 102-103, 108, 116, 139, deduced: 145,
continues: 29, 32, 34 152, 173, 225, 242, 252, 160-161, 165, 172
contract: 73, 222, 261 272-273, 291, 314
deduction: 14, 30, delimited: 9, 22, 47 difficult: 51, 61, 84
158, 160, 165, 179 demystify: 3, 27, dimensions: 38, 156, 298
default: 28-29, 37, 54-55, 45, 164, 205 dispatch: 251, 253-256,
60, 62, 72, 75-77, 81, depend: 147, 179, 197 258, 260, 268, 278
87-88, 102, 105, 108, dependent: 150-151, distance: 80-81, 84-85,
158-159, 161-163, 172, 175, 178-179 88-89, 112, 287
176, 179, 192, 196, depends: 88, 136, 178, 216 document: 73, 102, 143
201, 209, 221, 226, derive: 161, 241, 246, 254, donald: 196, 236, 305
236, 244-245, 253, 261-263, 266, 268, 311 double: 11, 87, 92, 112,
263-264, 268, 272, 274, derived: 87, 151, 161, 115, 134, 161, 163,
278, 300-301, 312 240-247, 249, 251-258, 172-173, 205
defaults: 12, 75, 244 261, 264, 268, 270, duration: 53, 80, 91
define: 1, 6-7, 9, 11, 17-18, 274, 278, 309 dynamic: 237, 239,
35, 40, 47-48, 50, 55, 78, describe: 85, 121, 136, 309 251-254, 256, 258, 260,
81, 83-84, 86, 88, 90, described: 38, 40, 266-273, 276, 278-279
93, 97-99, 108, 111, 124, 170, 179, 213
128-129, 135-136, 147,
150, 179, 246, 259-260,
destroy: 106, 109, 270, 272
destroyed: 54-56, 67,
E
262-263, 268, 278, 282, 106, 110, 122, 271, earlier: 36, 61, 65-66, 70,
288, 290, 294, 299, 311 273-275, 314 102, 128, 137, 139, 170,
defined: 4, 6-7, 11, 15, destructor: 106-109, 213, 222, 224, 240, 250,
28, 48, 50, 54-58, 111, 121-122, 245, 252, 268, 272-274, 293
62, 77, 79-80, 87, 258, 263, 267-268, easier: 44, 68, 75, 85, 130,
89-91, 98, 108, 116, 271-272, 274, 290 144, 158, 162-163, 174,
119, 124, 126-129, 145, detail: 10, 13-14, 51, 179, 266, 270, 274, 300
184, 188-190, 193, 58, 63, 101, 112, 182, effective: 12, 134, 259, 279
195, 197, 200-201, 217, 244, 266 element: 30, 37, 39-40,
206-207, 219, 225-226, details: 45, 85, 100, 56, 71-72, 143, 148-151,
261, 264, 277-278, 137, 177, 215, 279 156, 177-178, 182,
292-294, 299, 304 determine: 1, 13, 37, 207 184-187, 189-190,
defines: 6, 11, 76, 88, determined: 37, 45, 145, 192-193, 195, 198-201,
90, 126, 129, 202, 192-193, 198, 201 206, 210, 215, 217-219,
258, 261-262, 266 determines: 240, 270, 272 222, 224, 230-231, 235,
definition: 1, 9, 13, 39, 45, developer: 2, 248-249, 270 268, 298, 304, 306
47-50, 76-77, 90-91, 94, developers: 102, 134, embedded: 12, 244,
103, 117, 127, 136-137, 170, 240, 266 252-253, 271
139, 144-145, 147, 157, diagram: 3, 31, 38, enable: 2, 51, 66, 108, 121,
261, 263, 293, 300 187, 189, 217 245, 253-254, 257
delete: 123-124, 264, difference: 4, 40, 50, enables: 44, 86, 250
267-275, 291 69, 72, 88, 119, 121, enclosed: 25, 29, 135-136
deleted: 272, 275, 291 135, 149, 155, 164, encounters: 4, 46,
deletion: 111, 188, 290 190, 201, 258, 287 125-126, 136
enemies: 80, 249, 287 execute: 1, 3, 24-26, 28, extend: 108, 129, 294, 300
enough: 11, 156, 185, 50, 58, 101, 116, 155, 164, extended: 14, 36, 206, 290
187, 253, 267-269, 169-170, 174, 182, 225, extract: 206, 235, 296
276, 298, 305 253-254, 266, 302-303
ensure: 102, 108,
168, 268, 302
executed: 2-3, 7, 10,
24-30, 34, 44, 46-47,
F
entire: 25, 110, 134, 270 53, 67, 102, 104, 147, factory: 140, 276, 313-314
equivalent: 13, 37, 88, 96, 155, 162, 167, 182, 226, faster: 60, 155, 168, 198
126, 146, 167, 175, 206 253, 255-258, 269 feature: 14, 75-76, 79-80,
errors: 33, 38, 49, 64, 136, execution: 7, 25, 29, 32-34, 104, 136, 144-145, 154
143, 158, 179, 240, 268 46, 48, 52-54, 59, 65, features: 64, 81, 134, 144,
evaluate: 19, 191, 208, 304 67, 207, 210, 252, 267 158, 175, 179, 182, 209,
evaluates: 25-26, exercise: 10, 35-36, 49, 237, 239, 251, 253, 274
30, 32-33, 35 55-56, 59, 62, 92, fields: 30, 86, 90, 92-93,
example: 4, 11, 15, 17, 19-21, 97, 107, 115, 137, 203, 101, 104, 130, 142, 147,
23, 26-27, 30-34, 37, 211-212, 218, 220-221, 149-150, 176, 241, 253,
45-47, 54-55, 57-58, 223, 234, 242, 248, 254 259, 263, 272, 309
60, 65-66, 68, 70-71, exists: 6, 29, 47, 61, 134, figure: 3, 6-9, 14, 22, 31,
73-77, 79-80, 84-85, 162, 195-196, 275-276 38, 53, 85, 88, 125,
88-89, 91, 96, 98-106, expand: 172, 174, 303 160, 165, 183-186,
112-114, 118-121, 124, expanded: 4, 172-173, 303 188-190, 192, 194,
126, 128-129, 135, 137, expect: 37, 51, 73-74, 101, 197-200, 216-217,
140, 144-150, 155, 125-126, 161, 211, 263 219, 221, 228-229,
159-161, 163, 165, expected: 66, 74, 79, 102, 231-233, 241, 244
172-173, 176, 178, 187, 105, 124, 258-259, 263 finished: 67, 70, 275
193, 205, 208-209, 211, expects: 45, 65, 246 floats: 79-80, 84-85, 276
215, 219, 222, 225, 227, explain: 1, 43, 181 follow: 26, 67, 99, 152-153,
240, 246-247, 251-252, explained: 5, 30, 156-157, 168, 179, 190,
256-257, 261-262, 266, 69, 179, 278 223, 257, 263, 300
268, 270-273, 275-276, explicit: 96, 103-104, followed: 16, 26, 33, 46-47,
289-290, 293, 301 129, 167, 203, 209, 57, 65, 76-77, 87-88,
examples: 65-66, 70, 85, 211, 257, 291 128, 135-136, 144-146,
95, 125-126, 158, 164, explore: 10, 24, 29, 31, 155, 158, 163, 172, 217,
170, 176, 213, 246 47, 51, 58, 84, 86, 88, 236, 241, 247, 267
except: 75, 95, 116, 134, 190 99, 136, 144, 209, follows: 3-5, 15, 17-21, 27,
exception: 33-34, 73, 88, 218, 247, 251, 254 29-30, 32-33, 46, 57, 71,
109, 184, 196, 207-209 explored: 81, 130, 237, 278 89-90, 93, 97, 99-100,
exceptions: 33-34, express: 20, 44, 80, 95, 175 107, 115, 126-128, 136,
109, 207 expression: 25, 28-29, 138, 148, 157, 160, 163,
executable: 2, 4, 6, 8, 35, 58, 65, 67-70, 170, 176, 178, 202, 204,
10, 40, 47, 49-50 74, 121, 126, 144, 211, 213, 219-220, 222,
225, 267-269, 274 234, 241, 243, 249-250,
255, 260, 265, 267, 211, 215-216, 219-221, header: 4-8, 29-30, 35,
273, 278, 282-283, 285, 225-226, 228, 240, 49, 55, 80-81, 90-92,
289-291, 294, 296, 259-262, 266, 274, 97, 107, 115, 137, 152, 162,
299-302, 304, 315 277-278, 284-285, 295, 184, 188-193, 195-196,
forget: 25, 84, 86, 299, 306, 308, 314 200-201, 203, 206-207,
245, 257, 267 functor: 128-129, 211-212, 218-221, 223,
former: 15, 29, 96, 109, 192, 197, 225 242, 248, 254, 282,
116, 119-120, 184, 186 functors: 83-84, 284-286, 304-306
forward: 137, 145, 165, 128-129, 294 headers: 4, 7, 9, 235, 295
167-170, 175, 179, height: 99-100,
190, 215, 217-219,
236, 302-303
G 104, 115, 207
hierarchy: 217, 242,
friend: 83-84, 113-116, 292 general: 16, 58, 160, 246 247, 258
function: 1, 7, 9-10, 40, generate: 56, 123, high-level: 2, 84, 175, 179
44-51, 53-76, 78-81, 136, 139, 141 however: 2, 20, 66, 68,
86, 89, 91-101, 104, generates: 47, 120, 137, 179 90, 94, 124, 240
106-107, 109-111, 113, 115, generic: 14, 26, 63-64, 133,
121, 127-129, 134-142,
144-146, 153, 157-158,
135-136, 147, 150, 154,
163, 179, 185, 218, 295
I
161-174, 177-179, 182, getters: 98-99, 288, 290 identical: 49, 88,
184-187, 190, 192-193, global: 22-24, 43, 95, 134, 149
195, 197-198, 200-201, 50-51, 53, 55-56, identifier: 36, 57, 75-78,
203-209, 211-213, 77, 90-91, 286 151, 155, 158, 165, 169
215, 218, 220, 222, greater: 11-12, 26, 72, identify: 17, 57, 108,
225-227, 235-237, 243, 134, 138, 159, 194 133, 155, 264, 289
248, 253, 255-258, guarantee: 49, 100, 275 if-else: 25-26, 28, 78
260, 262, 265-275, guaranteed: 11-12, imagine: 75, 77, 79, 90, 101,
277-278, 282-287, 21, 51, 72, 218 112, 128, 176, 207, 213
294-295, 297, 299-303, guarantees: 32, 218, 273 implement: 36, 44, 59,
305-306, 308, 312-315 guidelines: 71, 263, 278 80-81, 83, 86, 101,
functional: 162, 108, 130, 133-134,
192-193, 300
functions: 4-5, 9, 11, 24,
H 149, 159, 176, 239,
250, 254, 259-260,
40, 43-44, 50, 58, 60, handle: 34, 109-110, 130, 262, 267, 307, 310
65-66, 70, 72, 75-76, 145, 213, 276, 279 implicit: 66, 103, 117,
78-81, 83-84, 86-87, happen: 100, 179, 119-120, 123-124
93-95, 97-99, 101-103, 207, 213, 276 import: 35, 187, 282
105, 108, 111, 113-114, happens: 2, 54, 67-69, important: 7, 10, 15, 18,
124, 130, 133-137, 139, 72, 111, 116, 136, 161, 26, 29, 38, 45, 52, 67,
144-148, 155, 160, 163, 166, 171, 200, 207, 70, 73, 76, 90, 95, 101,
169-171, 174, 182, 184, 222, 253, 267, 276 112, 143, 168, 182, 199,
190, 193, 200-208, hardware: 2, 7, 251 222-224, 237, 246, 252,
255, 258, 263, 268, 274 256, 258, 264, 266, 271, 236, 248, 255, 294
include: 2, 4-5, 7-9, 17, 282, 295-296, 298, 301, invoked: 47, 72, 79,
19-21, 23-24, 27, 31, 303, 305-306, 308 93, 102, 225, 227
34-35, 38, 46, 48, 50, instance: 88-93, 101, 109, iostream: 2, 4, 17, 19-21,
52, 55-56, 63, 92, 97, 111-112, 116, 128, 136, 23-24, 27, 31, 34-35,
107, 137, 143, 187, 191, 139, 146-148, 152-153, 38, 46, 48, 50, 52, 55,
193, 195-196, 203-205, 162, 191, 207, 267, 269, 63, 187, 193, 195-196,
207-209, 211-212, 215, 272, 275-276, 291, 203-205, 207-209,
218, 220-228, 230-235, 295, 300-301, 313 211-212, 215, 218,
242, 248, 282, 284-286, instances: 19, 88-90, 220-222, 225-228,
289-290, 292-293, 92, 130, 176, 262, 230-235, 242, 248, 282,
295-296, 298, 300, 268, 270, 276 284-286, 289-290,
304-306, 313-314 instead: 5, 21, 26, 32, 292-293, 295-296, 313
included: 5, 29, 49, 80, 44, 51, 53, 55, 68-70, isloggedin: 168-170,
90, 184, 215, 281 73-75, 84, 89-90, 110, 174, 302-303
increase: 29-30, 122, 155, 162-163, 177, iterate: 30, 40, 112, 182,
36, 191, 282 205, 210, 244-245, 253, 190, 215, 218-220,
increment: 35, 79, 257, 261, 266, 301 223, 229, 282-283
182, 217-219 integer: 11-12, 18, 40, iteration: 29-30, 32-33, 36
indeed: 12, 14, 21, 274 57, 65-67, 69, 73, 79, iterator: 112, 181, 215-224,
indicate: 16, 21, 26 141, 144, 191, 208, 230-231, 235, 237
inherits: 240, 242-243, 211, 215, 283-286 iterators: 112, 181-182,
246, 265, 309, 313 integers: 12, 40, 54, 215-224, 230, 237
initial: 17, 37, 157, 60, 65, 72, 79-80, itself: 21, 51, 64, 98,
163, 190, 299 84, 101, 111, 134, 146, 117, 121, 171
initialize: 13, 16-18, 29, 35, 221, 223, 225, 290
37, 40, 58, 90, 100, 102,
104-105, 108, 129-130,
intended: 73, 134, 253
interact: 84-86, 101, 149,
K
156, 170, 191, 244-245, 171, 211, 246, 266-267 keyword: 12, 14, 18-19, 21,
266, 268, 271, 278, interacts: 51, 85, 150 25-26, 29-30, 32-34,
282, 290, 294, 298 interested: 45, 176, 190 65-66, 76-77, 88, 90,
insert: 104, 169, 185, 188, interface: 86, 98, 112, 182, 94-97, 113, 135, 144, 151,
190, 193, 195, 197, 200, 185, 190, 200-201, 203, 155, 175, 178, 253-255,
215, 220-223, 235, 305 215, 244, 254, 260-264, 257-258, 263, 267, 278
inside: 6, 31, 34, 46, 51, 267, 276, 312-314 keywords: 14, 27, 267
57-58, 64-66, 71, 76-78, interfaces: 239, 260,
85-87, 89-90, 93-94,
97, 104-105, 108, 111-112,
263-264, 266, 270, 278
invalid: 16, 18, 64, 69, 77,
L
144, 151, 156, 161-162, 209, 222, 227, 274 lambda: 128, 181,
178-179, 186, 196-198, invoke: 44, 89, 93, 100, 225-227, 235-237
202, 206, 211-212, 223, 112, 115, 128-129, language: 2, 4, 7, 10,
226, 228, 244, 252-253, 163-164, 170, 227, 12-13, 38, 40, 44-45,
102, 124, 203 logger: 4-6, 129, 239, 244, 252-253, 258,
languages: 11, 44-45, 109 240, 266-267, 266-271, 273, 276, 279,
latitude: 84-85, 88-89, 91, 269-270, 272-273 285, 290, 305, 313
98-99, 108, 288-290 longer: 53, 70-71, mentioned: 12, 57, 68, 70,
latter: 29, 109, 120, 222-223, 267, 274 87, 173, 215, 250, 273
184, 186 longitude: 84-85, 88-89, message: 50, 63, 78, 97,
learned: 40, 50, 88, 90-91, 91, 98-99, 108, 288-290 142, 248, 257, 284, 307
95, 99, 134, 144, 146, looked: 190, 199, method: 62, 89, 93, 105,
154, 163, 166, 191, 237, 236-237, 261, 272 112, 115-116, 125-126,
240, 254, 266, 270 lookup: 192-193, 198, 236 141-143, 145, 148-152,
length: 36-37, 39, 106, 154, 156-157, 162, 166,
124, 157, 204, 206,
230-231, 300
M 209, 214, 221, 241, 243,
246-247, 249-250,
lesson: 1, 5, 9, 13-14, 17, machine: 4, 11, 45 253-258, 260-263, 271,
19, 24, 40, 43, 50-51, maintain: 51, 168, 171 274, 276, 292, 295-298,
61, 63-64, 68, 72, 81, manage: 109, 194, 206, 300-301, 306-311, 313
83-84, 87, 95, 100, 102, 239, 258, 270, 279, 313 methods: 86, 90, 93,
105, 112, 128, 130, 133, managed: 130, 167, 291 97, 99, 101, 114, 116,
140, 147, 162, 179, 181, management: 76, 214, 306 121, 130, 147-149, 208,
183, 191, 195, 199, 204, manager: 258, 214, 219, 241-242, 247,
216, 221-222, 224-225, 260, 311-312 250-251, 253-254,
236-237, 239, 266, manages: 111-112, 213, 261-263, 266, 278, 288,
274, 278-279, 282, 284, 276, 290, 314 292, 308, 310-311
288, 295, 304, 309 manipulate: 93, 202, 237 modern: 19, 266, 270, 279
library: 4, 62, 76-77, matrix: 156-157, modifier: 83, 87-88,
80, 100, 112, 134, 136, 162-163, 298-301 139, 241, 278
143, 146, 170, 174, mechanism: 58, 109, 203 modifiers: 12, 83, 130, 139
179, 181-182, 187, 199, member: 83-84, 86-87, 89, modify: 21, 44, 51, 58-61,
203, 211, 224, 270, 91-97, 99-100, 104-105, 64, 69, 72-75, 94,
273, 276, 286, 304 111, 113, 117, 119-122, 139, 161, 174, 196,
lifetime: 52-53, 59, 127, 129, 141, 149, 184, 226, 228-229, 305
67-70, 91, 109-110, 266, 186-187, 200, 204, 215, moreover: 94, 134, 184
270-272, 276, 314 218, 290, 294, 296, 309 multimap: 194-195,
limited: 28, 36, 38, 116 members: 83, 86-94, 206, 236
linear: 24, 218, 233 98-102, 104-106, 108, multiple: 5, 13, 15, 17,
linker: 47, 49, 90 111-114, 117, 119-121, 24-26, 29, 49, 54, 77, 80,
location: 3, 8, 13, 15, 21-22, 130, 193, 206, 241, 88-90, 92, 111, 121, 130,
38, 64, 80, 84, 127, 169, 263-264, 271, 288-290 133, 139, 153, 171-172,
176-177, 222, 252, 287 memory: 11-13, 15-17, 179, 195, 198, 210, 215,
locations: 36, 108, 21-22, 36-38, 52, 60-62, 227, 242, 246-248,
127, 173, 257 64, 90, 110-111, 121, 159, 262-263, 267-268, 275,
183-189, 191, 217, 222, 277, 279, 290, 296, 314
multiplier: 75, 162, 227, 301 objectives: 1, 43, 83, options: 7, 58, 65-66
mutable: 51, 92, 202 133, 181, 239, 281 original: 64, 112, 119, 139
objects: 13, 21, 52, 54, 68, otherwise: 25-26, 29,
N 70, 73, 75, 83-84, 88, 93,
106, 112, 127, 134, 143,
73, 78, 95, 208-209,
227, 244, 274, 298
namespace: 76-78, 205, 151-152, 159, 166, 179, output: 17, 19-24, 27, 31,
254, 285-286, 304 182, 192, 204, 212, 250, 34, 39, 47-50, 78, 93,
namespaces: 43, 266, 268-269, 271-272, 97-98, 107, 109, 115,
76-79, 81, 285 275, 279, 293, 295 120, 129, 138, 157, 163,
necessary: 14, 17, 19, 84, obtained: 14, 184, 215, 222 167, 188, 194, 204-209,
105, 121, 170, 257 offers: 24, 40, 75, 81, 211, 213, 218-220,
needed: 14-15, 38, 44, 134, 158, 273, 276 222-223, 225-227, 229,
159, 187, 213, 266 omitted: 30, 75, 244 231-234, 243, 249-250,
negative: 12, 228-229, 270 operand: 126, 162, 301 255, 260, 265, 278,
nested: 23, 31, 39-40, 76, operate: 80, 84-86, 90, 282-287, 291, 294,
83, 111-112, 175, 178, 283 109, 124, 215, 224, 236 296-297, 299-302, 304
non-const: 20-21, operation: 13, 24, 40, 46, outside: 22, 33-34, 50,
69, 95, 161 61, 64, 68, 74, 79, 100, 60, 87, 89-90, 94,
non-member: 98, 113, 127 126, 140, 157, 162-163, 113, 156, 241, 308
non-type: 154-155, 175, 182, 187, 216, 219, overcome: 61, 225, 270
158-159, 179 225, 233, 277, 301, 308 overload: 79-80, 83, 102,
normal: 33, 101, 166, operations: 17, 24, 44-46, 119, 127, 134, 149, 212,
201, 207, 219 84-86, 98-99, 109-110, 225, 256-257, 293
notice: 38, 104, 139, 276 134, 156, 162, 174, 177, overloaded: 43, 95,
numeric: 11-12, 205, 233 181-182, 184, 202, 218, 102-103, 125, 149, 205
numerical: 78, 124, 134 224, 228, 236, 277, 288, overloads: 128,
298, 303-304, 314 149, 171, 210
O operator: 15-16, 35, 89,
91-94, 119-121, 123-130,
override: 255-257,
260-262, 265,
object: 2, 4, 7-8, 15, 134, 149, 159, 162, 176, 278, 311, 313
17-22, 52-54, 59, 182, 184, 190-192, 196,
61-64, 67-71, 73-74, 88,
90, 94-97, 100-104,
201, 204, 209, 212,
215, 218, 224-225,
P
106, 108-110, 112, 114, 245, 247, 253, 264, parameter: 7, 46-47,
116-117, 119-121, 124, 268-269, 293-294, 57-59, 61, 63-65,
126-129, 139, 142-143, 299, 304, 308, 312 71-72, 74-75, 99-100,
152-153, 159, 166-167, operators: 83, 117, 123-126, 102-103, 111, 121,
174, 182, 192, 197, 212, 149, 176, 182, 204, 125-127, 129, 135-136,
225-227, 251-253, 255, 216-217, 245, 263-264 139-140, 145, 148,
258, 263, 267-268, option: 60, 71, 159 150, 153-155, 158-164,
270-275, 289-295 optional: 9, 12, 29-30, 39, 166-167, 169-174, 179,
202, 207-210, 213, 237 182, 192, 197, 200, 207,
288, 290, 293-294, pointers: 1, 15, 17, 19-21, 293, 298, 301, 303
296-297, 300, 302-303 40, 64, 70, 182, prints: 46, 49, 59-60, 62,
parameters: 7, 45-46, 252, 262, 267, 270, 71, 106, 115, 118, 120,
48-49, 57-58, 60, 272-274, 276, 279 129-130, 214, 235, 240,
62-63, 65, 72-73, 79, points: 15-16, 64, 80-81, 249-250, 292, 309-310
81, 99-100, 102-103, 127, 176, 189, 218-219, private: 78, 83, 87-88,
126, 134-136, 139-140, 270, 272, 274, 293 99, 101, 104-105, 108,
144-147, 149, 151, position: 36-37, 110-114, 116-117, 119-120,
154-156, 158, 160-162, 55-56, 108, 156, 182, 122, 125, 129-130, 241,
164, 167, 169-176, 179, 184-185, 190-191, 244-245, 249, 252,
182, 206, 225, 244, 193, 195, 197-198, 259, 275, 288-292,
255, 261, 267, 295, 215, 217-218, 220, 294, 297, 309-310
298, 300, 302-303 222-223, 229, 249-250, problem: 5, 51, 58, 73,
parent: 76-77, 255, 272-273 304-305, 309-310 142, 150, 169, 171,
particular: 19, 22, 182 positions: 38, 108-109, 111, 198, 263, 268, 276
passed: 59-64, 71, 117, 186, 198, 219, 289-290 problems: 5, 33, 70, 76,
125, 167, 235, 272 possible: 13, 15-18, 20, 23, 134, 155, 175, 240, 247,
pattern: 5, 171-173, 178, 26, 28, 51, 67, 74-75, 264, 266-267, 279
200, 217, 251, 254 103, 127, 134, 144, 161, process: 3-4, 6, 10,
perform: 6, 35, 40, 74, 167, 171, 187, 190, 205, 40, 45, 85, 201-202,
79, 86, 96, 99, 108, 210, 212, 227, 240, 251, 277, 306-307
127, 129, 137, 155, 160, 264, 270, 273, 277, 314 produce: 4, 6, 10, 26, 38
162, 168-170, 174, 182, powerful: 79, 81, 130, produces: 35, 45, 49
184, 202, 211-212, 218, 136, 174-175, 251, 270 profile: 264-265, 277, 313
220-221, 225, 242, 252, practice: 25, 51, 55, program: 2-4, 6-7, 9-10,
254, 259, 261-262, 264, 67, 75-76, 102, 109, 18, 22, 24-25, 29, 32-34,
277, 281, 301, 303, 308 149, 161, 262-263 38, 40, 44-49, 51-53,
performed: 13, 17, 45, preferred: 62, 166, 188 55, 57-59, 62-63, 65-67,
64, 182, 224, 281 present: 12, 29, 45, 69-70, 77, 80, 84, 87-88,
performs: 45, 61, 101, 172, 196, 207-210 91-92, 97, 100, 107-108,
117, 136, 170 prevent: 5, 18, 75, 111, 115, 133-134, 136,
person: 59, 284, 309 116, 121, 123-124 140, 153, 155, 159, 170,
placed: 21, 31, 36, 94 prevents: 29, 136, 267 203, 207, 210-214, 218,
places: 44, 95, 128 previous: 15-16, 19, 26-27, 220-222, 234, 240-242,
please: 63, 284, 286 35-38, 56, 60, 62-63, 248, 250-252, 254, 260,
pointed: 15-16, 21-22, 65, 67, 70-72, 84, 90, 266-268, 270, 272, 282,
70, 96, 119, 267, 275 95, 97, 100-101, 103, 284, 286, 296-297
pointer: 15-18, 20-22, 113-115, 126-127, 134, programmer: 11-12,
53, 58, 64, 70, 73, 96, 144, 146, 156, 158, 162, 15, 24, 30, 33, 44,
111, 119, 146, 251-254, 174, 178-179, 190, 199, 58, 84-85, 87, 108,
267-270, 272-278 216, 218, 222, 250, 110, 174, 182, 202
260-263, 268, 270, 273,
programs: 1-2, 51-52, 81,
84, 92, 130, 203, 237,
Q regardless: 26, 32, 72,
163-164, 168, 258
239, 266, 270, 276, 279 qualifier: 18, 40, 51, 72, 255 registered: 176, 202,
properties: 45, 61, 207, 213, 234
93, 101, 239
property: 45, 101, 170
R regular: 144, 147, 149,
155, 261, 272
protected: 83, 87, 105, radius: 80, 98, 286-287 related: 15, 112, 116
113-114, 241-243, random: 100, 186, release: 111, 122,
252, 264, 312 190, 218, 231 187, 269, 273
provide: 24, 44, 57-58, rather: 182, 201, 220 remain: 121, 222, 267,
65, 75, 79, 84, 86, reaches: 53-54, 275-276, 315
90, 110, 112, 116, 145, 65-68, 110, 223 remains: 2, 17-18, 71,
154, 158-159, 161-162, reason: 7, 14, 18, 35, 60, 275, 277, 301
171, 176, 190, 193, 197, 67, 69, 72-74, 141-142, remember: 15, 55, 61, 64,
199-200, 202, 206, 145, 150-151, 159, 81, 84, 127, 149, 178, 222,
210, 215, 217-218, 224, 161, 182, 190, 252, 235, 254, 274, 308, 315
236-237, 241, 261, 263, 255, 258, 263, 285 remove: 73-74, 169-170,
274, 284, 296, 298, 313 reasons: 18, 51, 64, 189-190, 200-201, 204,
provided: 14, 25, 37, 39, 112, 161, 175 230, 235, 237, 275, 306
44, 59-62, 64, 73, rectangle: 99-100, removed: 159, 169,
79-80, 109-110, 123, 102-104, 106, 124, 246 184-185, 188-189, 200,
127, 136, 145, 152, 154, reference: 17, 19-21, 53, 230-231, 275, 304
160, 162, 165-166, 171, 58, 61-65, 67-74, 79, repeat: 29, 44, 90, 307
174, 176, 182, 190-191, 95, 117, 119, 121, 139, replace: 14, 246, 303-304
193-194, 196, 199, 201, 145, 156-158, 161, replaced: 4, 136, 274
204, 211, 216, 218, 224, 163-167, 179, 226-227, represent: 11-12, 33,
234, 267, 278, 286, 235, 251-255, 266, 44-45, 75, 84-85,
293, 295, 301, 303 272, 274, 278, 284-285, 108-109, 124, 137,
provides: 25, 63, 73, 75, 298, 300, 302 156, 203, 208, 210,
84, 104, 112, 128, 150, referenced: 20, 215, 226 213-215, 222, 270, 272
158, 167, 182, 195, 205, references: 1, 15, 17, 19-21, represents: 64, 96, 101,
208, 215-216, 219, 224, 40, 61, 63, 69-70, 72-74, 109, 127, 153, 155,
236, 260, 270, 313 136, 158, 164-165, 168, 177, 182, 207, 213,
public: 86-88, 91, 93-94, 167-168, 170, 206, 246, 250, 263, 275
97-99, 101-104, 106-109, 252, 262, 285, 303 requested: 111, 267, 298
112-113, 115-117, 119-121, referred: 4, 7, 13-14, 17, 33, require: 151, 184, 221, 262
126, 128-129, 142, 38, 65, 69, 86, 89, 94, required: 35, 92, 94,
150, 152-154, 156, 177, 98, 164, 182, 210, 215 97, 107, 115, 119, 123,
240-245, 248-250, refers: 13, 22, 52-54, 61, 134, 137, 162, 168, 170,
252, 254-255, 259-265, 89, 117, 147, 160, 252 188, 203, 210-211, 213,
275-276, 288-294, 296, ref-ness: 72, 164, 168, 221, 235, 248, 254,
298, 301, 307-313 170, 174, 255, 303 282, 285-286, 304
213-214, 225, 247, 250,
requires: 13, 16, 71, 86,
101, 140, 162, 171, 176,
S 258, 262-264, 266,
186, 188, 225, 262-263 search: 77, 193, 233 268, 270, 272-274,
reserve: 187, 191, 305 second: 4-5, 38, 55, 282, 285, 288, 294,
resources: 121, 130, 268 78, 95, 102-103, 105, 296-297, 302-303, 310
respected: 102, 143, 272 118, 122-123, 126, 144, showed: 102, 130, 179,
respective: 8, 253, 277 155-156, 169, 177, 274, 276, 278
result: 5, 16, 23, 38, 44, 195-197, 204, 206, signature: 46, 48, 57,
69-70, 128, 157, 162-163, 218, 235, 243, 283, 73-74, 76, 119, 144, 168,
174, 193, 195, 211, 252, 285, 296, 298, 305 172, 213, 255-257, 273
258, 268, 293, 300-301 section: 15, 29-30, 40, similar: 20, 29, 32, 38,
results: 6, 66, 246, 264 47, 57, 65, 67, 70, 90, 55, 69, 73, 90, 101-102,
retrieve: 15, 98, 136, 144, 146, 158, 174, 109, 112, 119, 128, 136,
168-169, 196, 207, 185-187, 199, 218, 250, 147-149, 161, 164, 170,
213, 264-265, 313 254, 260, 266, 270, 281 182, 185, 190, 192,
return: 2, 7, 9-11, 15, 40, selected: 34, 79, 206 195, 201, 206, 210,
43, 45-48, 52-53, semantic: 61-62, 68, 74 218-219, 225, 245-246,
55-56, 63-68, 70-72, 74, separate: 51, 84, 263 263-264, 275
79, 89, 91-92, 98-100, separated: 13, 75, simple: 2, 40, 66, 107,
106, 116, 124, 128-129, 77, 241, 299 129, 182, 225, 274
134-135, 137-140, 142, sequence: 24, 27, 30, 44, situation: 18, 25,
144-146, 152-153, 47, 55-56, 112, 182-183, 68, 174, 207
156-157, 159, 161-162, 187, 190-191, 200, snippet: 5, 16, 140
166, 168, 173, 198, 200, 217, 224-225, 236 software: 2, 44, 76, 170
209-210, 213, 225-230, sequences: 30, 143, solution: 13, 36, 40, 49,
234-236, 242-243, 202-203, 237 51, 63, 72, 74, 78, 80,
248-249, 255, 259-260, sequential: 29, 99, 109, 111, 117, 128,
265-266, 268-269, 181-182, 193, 236 130, 153-154, 157, 163,
273, 276, 284-286, several: 8, 18, 27, 44, 49, 170-171, 175, 191, 197,
288-289, 292-296, 65-66, 68, 79-80, 84, 202, 214, 235, 250, 260,
298-301, 307, 310-314 102, 137, 151, 172, 176-177, 264-265, 277-278, 300
returned: 46, 65-72, 182, 195, 203, 213, 218, sorted: 159, 192,
74, 143, 145, 177, 237, 264, 275, 314 194, 197, 236
223, 267-269 shared: 90, 149, 270, source: 2, 4, 7, 10, 14, 40,
reverse: 31, 106, 201-202, 275-279, 314-315 70, 121, 153, 221, 252
219-221, 258 should: 6-7, 17, 26, 40, 45, special: 6, 100, 117, 135,
runtime: 33, 36, 38, 155, 53, 60, 72-75, 77-78, 147, 156, 178, 193,
179, 251-252, 268, 276 84, 88, 99, 101, 105, 203, 225, 253
108, 122-124, 129, 137, specific: 5, 15, 22, 35,
143, 150, 154, 156, 159, 37, 45, 60, 75, 77, 85,
162, 166-168, 175, 186, 90, 102, 116, 134, 139,
192-193, 197-199, 207, 141, 147, 149-150, 153,
168, 175, 182, 190, 198,
206, 215, 232, 262
stored: 11, 15-17, 21, 56,
90, 99, 101, 156, 162,
T
specifier: 14, 83, 87, 182-183, 185, 188, 191, talked: 40, 170, 260, 278
99, 103, 108, 113, 198, 210-212, 217, 223, template: 128, 130, 133,
243-244, 288-289 226-228, 288, 295, 301 135-156, 158-163,
specifies: 45, 90, 103, stores: 11, 84, 153-154, 165-167, 169-179,
150-151, 241, 254, 263 185, 214, 235, 259 182, 200, 206-209,
specify: 8, 12, 14-15, 26, 49, stream: 63, 98, 224, 250, 273, 275,
57, 75, 77, 87, 102, 113, 109-110, 217, 221 295-300, 302-303
124, 136, 141, 145, 148, streams: 4, 109, 221 templated: 133, 135-136,
158-159, 175-176, 182, struct: 88-89, 96, 99-101, 144, 148-149, 163, 168
184, 192, 197, 200-201, 106, 112, 118, 123, templates: 14, 63-64,
206, 215, 225, 241, 254, 137-138, 142, 153, 159, 130, 133-137, 139-141,
257, 261, 268, 272 164, 168, 171-173, 201, 143-146, 149-150,
square: 81, 92, 212-214, 234, 246-247, 154, 158, 160-161, 163,
102-104, 246 249, 251-252, 256, 166, 170, 174-175, 179,
standard: 4, 7, 12, 14, 264, 309, 312, 314 240, 250, 261, 295
49-50, 62, 76, 98, structs: 88, 105, temporary: 30, 63, 67,
100, 112, 136, 141, 143, 137-138, 244 69-70, 73, 121, 161
146, 170, 174, 179, structure: 6, 11, 15, 26, 36, testlogger: 266, 269, 273
181-182, 202-203, 40, 44, 86, 176, 183-184, thanks: 80, 85, 179,
211, 221, 224, 237, 257, 188-189, 192, 200, 202 213, 216, 251, 274
270, 273, 276, 304 structures: 60, 176, 182 therefore: 127, 141, 151
started: 1, 40, 50, 81, studio: 3, 7, 10 though: 32, 61, 73,
130, 183, 216, 282 subsequent: 6, 25, 158 106, 110, 219
starts: 4, 38, 46, 48, 53, summary: 40, 81, 130, through: 18, 26, 73,
76, 135, 144, 155, 191 179, 236, 241, 278 98-99, 109, 112, 182,
statement: 9, 13-15, 22, support: 12, 25, 68, 80, 190, 193, 200, 217, 220,
25-30, 32-35, 37, 47, 87, 140, 153, 166, 168, 242, 282-283, 288
50, 54, 56, 66-69, 208, 177, 182, 212, 236, 296 throughout: 6,
248, 282-283, 286-287 supported: 7, 155, 160, 174 80, 170, 260
statements: 1, 24-26, supports: 11, 19, 66, together: 4, 7-9, 44, 49,
29, 32-34, 40, 47, 50, 153, 246, 251, 253 51, 76-77, 81, 84, 86,
58, 211, 225, 291 supposed: 45, 72, 99, 288 102, 244, 262, 276
static: 37, 83, 90-93, syntax: 13-16, 18-21, 26, transform: 139, 230, 235
111-112, 130, 149-153, 29-30, 32-33, 37-38, turnon: 93-94, 240-241,
183, 251-255, 295 40, 46, 75, 87, 90, 253-255, 261-262
status: 7, 9, 170, 214 93, 128, 144, 146, 148, typename: 135, 138-140,
storage: 13, 90, 236, 151, 158, 179, 225 145-156, 158-161,
265-267, 276, 313-314 system: 3, 7, 10, 100, 165-167, 169-179,
108, 207, 214, 258, 295-300, 302-303
266-267, 289, 306 typical: 150, 213, 240, 254
U variables: 1, 11, 13-15,
22-24, 43, 50-57, 59-60,
unique: 2, 6, 192, 270, 74, 78, 81, 84-86, 89-92,
272-276, 279, 313-314 100, 104-105, 145, 206,
unless: 51, 60-61, 225-227, 237, 267, 271
74, 77, 188 variant: 30, 202, 208,
unlike: 17, 155, 188, 190 210-213, 237, 306-308
unordered: 197-199, various: 44, 72, 213,
206, 236 218, 237, 250, 310
unsigned: 12, 35-36, vector: 143, 150, 157,
155, 282 162-163, 168-170,
update: 85, 176, 189, 191, 173, 184-188, 190-191,
222, 257, 273, 277, 304 215-216, 218, 220,
useful: 24, 81, 102, 112, 222-223, 228-236, 275,
128, 144-146, 155, 177, 300-302, 304-305
179, 203, 206, 210, 270 vectors: 79, 143, 185-186,
userid: 168, 264-265, 190, 202, 204
312-314 vehicle: 240-243, 246,
username: 153, 196, 253-255, 261-264
201, 234, 305-306 version: 21, 81, 108, 236
utilize: 1, 43, 83, 233 versions: 95, 160, 211
virtual: 105, 253-264,
V 268, 278, 311-312
visibility: 97, 241,
values: 11-12, 24, 35, 37, 40, 247, 252, 278
43, 51, 55, 57-58, 63, 65, visible: 22, 64, 137, 247
67, 69-70, 72, 77, 81, 84, visual: 3, 7, 10, 287
90, 99-101, 104, 108, 111,
151, 155, 158, 168, 170,
176, 182, 184, 190, 195,
W
205-206, 210, 212-213, website: 168, 201, 207
220, 231-232, 251-252,
283, 287-290, 298, 304
variable: 1, 6, 11, 13-24,
29-30, 35-37, 40,
50-56, 58, 60-62,
68-69, 71, 74-78,
86, 88, 90-91, 100,
125-126, 150-151,
203, 206, 226-227,
252-255, 266-267,
282, 284, 286-287