Objectives
In this chapter, you’ll:
■ Construct programs modularly from functions.
■ Use common math library functions and learn about math functions and constants added in C++20, C++17 and C++11.
■ Declare functions with function prototypes.
■ View many key C++ Standard Library headers.
■ Use random numbers to implement game-playing apps.
■ Declare constants in scoped enum
s and use constants without their type names via C++20’s using enum
declarations.
■ Make long numbers more readable with digit separators.
■ Understand the scope of identifiers.
■ Use inline functions, references and default arguments.
■ Define overloaded functions that perform different tasks based on the number and types of their arguments.
■ Define function templates that can generate families of overloaded functions.
■ Write and use recursive functions.
■ Use the C++17 and C++20 [[nodiscard]]
attribute to indicate that a function’s return value should not be ignored.
■ Zajnropc vrq lnfylun-lhqtomh uyqmmhzg tupb j dvql psrpu iw dmwwqnddwjqz.
Outline
5.4 Function Definitions and Function Prototypes
5.5 Order of Evaluation of a Function’s Arguments
5.6 Function-Prototype and Argument-Coercion Notes
5.6.1 Function Signatures and Function Prototypes
5.6.3 Argument-Promotion Rules and Implicit Conversions
5.7 C++ Standard Library Headers
5.8 Case Study: Random-Number Generation
5.8.2 Rolling a Six-Sided Die 60,000,000 Times
5.8.3 Randomizing the Random-Number Generator with srand
5.8.4 Seeding the Random-Number Generator with the Current Time
5.8.5 Scaling and Shifting Random Numbers
5.9 Case Study: Game of Chance; Introducing Scoped enum
s
5.10 C++11’s More Secure Nondeterministic Random Numbers
5.13 References and Reference Parameters
5.15 Unary Scope Resolution Operator
5.19 Example Using Recursion: Fibonacci Series
5.21 C++17 and C++20: [[nodiscard]]
Attribute
In this chapter, we introduce custom functions. We overview a portion of the C++ Standard Library’s math functions and introduce new functions and constants added in C++20, C++17 and C++11. We introduce function prototypes and discuss how the compiler uses them, if necessary, to convert the type of an argument in a function call to the type specified in a function’s parameter list. We also present an overview of the C++ Standard Library’s headers.
Next, we demonstrate simulation techniques with random number generation. We develop a version of a popular casino dice game that uses most of the C++ capabilities we’ve presented so far. In the game, we show how to declare constants in scoped enum
s and discuss C++20’s new using enum
declarations for accessing scoped enum
constants directly without their type name.
We then present C++’s scope rules, which determine where identifiers can be referenced in a program. We discuss features that help improve program performance—inline functions that can eliminate the overhead of a function call and reference parameters that can be used to pass large data items to functions efficiently.
Many of the applications you develop will have more than one function of the same name. This technique, called function overloading, is used to implement functions that perform similar tasks for arguments of different types or different numbers of arguments. We consider function templates—a mechanism for concisely defining a family of overloaded functions. We introduce recursive functions that call themselves, either directly, or indirectly through another function.
We present C++17’s [[nodiscard]]
attribute for indicating that a function’s return value should not be ignored. This helps compilers warn you when the return value is not used in your program. We also discuss C++20’s [[nodiscard]]
enhancement that allows you to specify a reason why the return value should not be ignored. Cujuumt, ul znkfehdf jsy lagqynb-ovrbozi mljapvao thqt w wjtz qarcv aj wazkrvdqxbu.
You are viewing an early-access “rough cut” of C++20 for Programmers. We prepared this content carefully, but it has not yet been reviewed or copy edited and is subject to change. As we complete each chapter, we’ll post it here. Please send any corrections, comments, questions and suggestions for improvement to [email protected]
and I’ll respond promptly. Check here frequently for updates.
As an O’Reilly Online Learning subscriber, you also have access to the “sneak peek” of our new C++20 Fundamentals LiveLessons videos at:
https://learning.oreilly.com/videos/c-20-fundamentals-parts/9780136875185
Co-author Paul Deitel immediately records each video lesson as we complete each rough-cut e-book chapter. Lessons go live on O’Reilly Online Learning a few days later. Again, check here frequently for updates.
You typically write C++ programs by combining
• prepackaged functions and classes available in the C++ Standard Library,
• functions and classes available in a vast array of open-source and proprietary third-party libraries, and
• new functions and classes you and your colleagues write.
The C++ Standard Library provides a rich collection of functions and classes for math, string processing, regular expressions, input/output, file processing, dates, times, containers (collections of data), algorithms for manipulating containers, memory management, concurrent programming, asynchronous programming and many other operations.
Functions and classes allow you to separate a program’s tasks into self-contained units. You’ve used a combination of C++ Standard Library features, open-source library features and the main
function in every program so far. In this chapter, you’ll begin defining custom functions, and starting in Chapter 10, you’ll define custom classes.
There are several motivations for using functions and classes to create program components:
• Software reuse. For example, in earlier programs, we did not have to define how to create and manipulate string
s or how to read a line of text from the keyboard—C++ provides these capabilities via the <string>
header’s string
class and getline
function, respectively.
• Avoiding code repetition.
• Dividing programs into meaningful functions and classes makes programs easier to test, debug and maintain.
To promote reusability, every function should perform a single, well-defined task, and the function’s name should express that task effectively. We’ll say lots more about software reusability in our treatment of object-oriented programming. C++20 introduces another program component called modules, which we will discuss in later chapters.
In our objects-natural case study sections, you’ve created objects of interesting classes then called their member functions to perform useful tasks. Functions like main
that are not member functions are called global functions.
The <cmath>
header provides many global functions for common mathematical calculations. For example,
sqrt(900.0)
calculates the square root of 900.0
and returns the result, 30.0
. Function sqrt
takes a double
argument and returns a double
result. There’s no need to create any objects before calling function sqrt
. All functions in the <cmath>
header are global functions in the std
namespace. Each is called simply by specifying the function name followed by parentheses containing the arguments.
Function arguments may be constants, variables or more complex expressions. Some popular math library functions are summarized in the following table, where the variables x
and y
are of type double
.
11 17 C++11 added dozens of new math functions to the <cmath>
header. Some were entirely new, and some were additional versions of existing functions for use with arguments of type float
or long double
, rather than double
. The two-argument hypot
function, for example, calculates the hypotenuse of a right triangle. C++17 added a three-argument version of hypot
to calculate the hypotenuse in three-dimensional space. For a complete list of all the <cmath>
header’s functions, see
https://en.cppreference.com/w/cpp/numeric/math
or the Section 26.8.1 of the C++ standard document
https://open-std.org/JTC1/SC22/WG21/docs/papers/2020/n4861.pdf
<numbers>
HeaderThough C++ has always had common mathematical functions, the C++ standard did not define common mathematical constants. Some C++ implementations defined M_PI
(for p) and M_E
(for e) and other mathematical constants via preprocessor macros.1 When the preprocessor executes in those implementations, it replaces these macro names with corresponding double
floating-point values. Unfortunately, these preprocessor macros were not present in every C++ implementation. C++20’s new <numbers>
header2 standardizes the following mathematical constants commonly used in many scientific and engineering applications:
1. We discuss the preprocessor and macros in Appendix E.
For the engineering and scientific communities and other mathematical fields, C++17 added scores of mathematical special functions3 to the <cmath>
header. You can see the complete list and brief examples of each at:
https://en.cppreference.com/w/cpp/numeric/special_functions
Each mathematical special function in the following table has versions for float
, double
and long double
arguments, respectively:
In this section, we create a user-defined function called maximum
that returns the largest of its three int
arguments. When the application in Fig. 5.1 executes, the main
function first reads three integers from the user. Then, the output statement (lines 15–16) calls maximum
, which is defined in lines 20–34. In line 33, function maximum
returns the largest value to the point of the maximum
call (line 16), which is an output statement that displays the result. The sample outputs show that maximum
determines the largest value regardless of whether it’s the first, second or third argument.
1 // fig05_01.cpp 2 // maximum function with a function prototype. 3 #include <iostream> 4 #include <iomanip> 5 using namespace std; 6 7 int maximum(int x, int y, int z); // function prototype 8 9 int main() { 10 cout << "Enter three integer values: "; 11 int int1, int2, int3; 12cin >> int1 >> int2 >> int3;
13 14 // invoke maximum 15 cout << "The maximum integer value is: " 16<< maximum(int1, int2, int3) << endl;
17}
18 19 // returns the largest of three integers 20 int maximum(int x, int y, int z) { 21 int maximumValue{x}; // assume x is the largest to start 22 23 // determine whether y is greater than maximumValue 24 if (y > maximumValue) { 25 maximumValue = y; // make y the new maximumValue 26}
27 28 // determine whether z is greater than maximumValue 29 if (z > maximumValue) { 30 maximumValue = z; // make z the new maximumValue 31}
32 33 return maximumValue; 34}
Enter three integer grades:
86 67 75
The maximum integer value is: 86
Enter three integer grades:
67 86 75
The maximum integer value is: 86
Enter three integer grades:
67 75 86
The maximum integer value is: 86
maximum
Typically, a function definition’s first line specifies its return type, function name and parameter list, which is enclosed in required parentheses. The parameter list specifies additional information that the function needs to perform its task. The function’s first line is also known as the function’s header. A parameter list may contain zero or more parameters, each declared with a type and a name. Two or more parameters are specified using a comma-separated list of parameters. Function maximum
’s header indicates that the function has three int
parameters named x
, y
and z
. When you call a function, each parameter receives the corresponding argument’s value from the function call.
Function maximum
first assumes that parameter x
has the largest value, so line 21 initializes local variable maximumValue
to parameter x
’s value. Of course, parameter y
or z
may contain the actual largest value, so each of these must be compared with maximum-Value
. Lines 24–26 determine whether y
is greater than maximumValue
and, if so, assign y
to maximumValue
. Lines 29–31 determine whether z
is greater than maximumValue
and, if so, assign z
to maximumValue
. At this point, the largest value is in maximumValue
, so line 33 returns that value to the caller.
maximum
You must either define a function before using it or declare it, as in line 7:
int maximum(int x, int y, int z); // function prototype
This function prototype describes the interface to the maximum
function without revealing its implementation. A function prototype tells the compiler the function’s name, return type and the types of its parameters. Line 7 indicates that maximum
returns an int
and requires three int
parameters to perform its task. The types in the function prototype should be the same as those in the corresponding function definition’s header (line 20). The function prototype ends with a required semicolon. We’ll see that the names in the function prototype’s parameters do not need to match those in the function definition.
Parameter names in function prototypes are optional (the compiler ignores them), but it’s recommended that you use these names for documentation purposes.
maximum
’s Function PrototypeWhen compiling the program, the compiler uses the prototype to
• Ensure that maximum
’s first line (line 20) matches its prototype (line 7).
• Check that the call to maximum
(line 16) contains the correct number and types of arguments, and that the types of the arguments are in the correct order (in this case, all the arguments are of the same type).
• Ensure that the value returned by the function can be used correctly in the expression that called the function—for example, a function that does not return a value (declared with the void
return type) cannot be called on the right side of an assignment.
• Ensure that each argument is consistent with the type of the corresponding parameter—for example, a parameter of type double
can receive values like 7.35, 22 or –0.03456, but not a string like "hello"
. If the arguments passed to a function do not match the types specified in the function’s prototype, the compiler attempts to convert the arguments to those types. Section 5.6 discusses this conversion process and what happens if the conversion is not allowed.
Compilation errors occur if the function prototype, header and calls do not all agree in the number, type and order of arguments and parameters, and in the return type. For calls, the compiler checks whether the function’s return value (if any) can be used where the function was called.
When a program calls a function, the function performs its task, then returns control (and possibly a value) to the point where the function was called. In a function that does not return a result (i.e., it has a void
return type), control returns when the program reaches the function-ending right brace. A function can explicitly return control (and no result) to the caller by executing
return;
anywhere in the function’s body.
Multiple parameters are specified in function prototypes, function headers and function calls as comma-separated lists. The commas in line 16 of Fig. 5.1 that separate function maximum
’s arguments are not comma operators. The comma operator guarantees that its operands evaluate left-to-right. The order of evaluation of a function’s arguments, however, is not specified by the C++ standard. Thus, different compilers can evaluate function arguments in different orders.
Sometimes when a function’s arguments are expressions, such as those with calls to other functions, the order in which the compiler evaluates the arguments could affect the values of one or more of the arguments. If the evaluation order changes between compilers, the argument values passed to the function could vary, causing subtle logic errors.
If you have doubts about the order of evaluation of a function’s arguments and whether the order would affect the values passed to the function, assign the arguments to variables before the call, then pass those variables as arguments to the function.
A function prototype is required unless the function is defined before it’s used. When you use a standard library function like sqrt
, you do not have access to the function’s definition, so it cannot be defined in your code before you call the function. Instead, you must include its corresponding header (in this case, <cmath>
), which contains the function’s prototype.
If a function is defined before it’s called, then its definition also serves as the function’s prototype, so a separate prototype is unnecessary. If a function is called before it’s defined, and that function does not have a function prototype, a compilation error occurs.
Always provide function prototypes, even though it’s possible to omit them when functions are defined before they’re used. Providing the prototypes avoids tying the code to the order in which functions are defined (which can easily change as a program evolves).
A function’s name and its argument types together are known as the function signature or simply the signature. The function’s return type is not part of the function signature. The scope of a function is the region of a program in which the function is known and accessible. Functions in the same scope must have unique signatures. We’ll say more about scope in Section 5.11.
In Fig. 5.1, if the function prototype in line 7 had been written
void maximum(int x, int y, int z);
the compiler would report an error, because the void
return type in the function prototype would differ from the int
return type in the function header. Similarly, such a prototype would cause the statement
cout << maximum(6, 7, 0);
to generate a compilation error because that statement depends on maximum
returning a value to be displayed. Function prototypes help you find many types of errors at compile-time, which is always better than finding them at run time.
An important feature of function prototypes is argument coercion—i.e., forcing arguments to the appropriate types specified by the parameter declarations. For example, a program can call a function with an integer argument, even though the function prototype specifies a double
parameter—the function will still work correctly, provided this is not a narrowing conversion (discussed in Section 3.8.3). A compilation error occurs if the arguments in a function call cannot be implicitly converted to the expected types specified in the function’s prototype.
4. Promotions and conversions are complex topics discussed in Sections 7.3 and 7.4 of the C++ standard document. “Working Draft, Standard for Programming Language C.” Accessed May 11, 2020. http://open-std.org/JTC1/SC22/WG21/docs/papers/2020/n4861.pdf
.
11 Sometimes, argument values that do not correspond precisely to the parameter types in the function prototype can be converted by the compiler to the proper type before the function is called. These conversions occur as specified by C++’s promotion rules, which indicate the implicit conversions allowed between fundamental types. An int
can be converted to a double
. A double
can also be converted to an int
, but this narrowing conversion truncates the double
’s fractional part—recall from Section 3.8.3 that C++11 list initializers do not allow narrowing conversions. Keep in mind that double
variables can hold numbers of much greater magnitude than int
variables, so the loss of data in a narrowing conversion can be considerable.
Values might also be modified when converting large integer types to small integer types (e.g., long
to short
), signed to unsigned, or unsigned to signed. Variables of unsigned
integer types can represent values from 0 to approximately twice the positive range of the corresponding signed integer types. The unsigned
types are used primarily for bit manipulation (Chapter 22). They should not be used to ensure that a value is non-negative.5
5. C++ Core Guidelines. Accessed May 11, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Ri-expects
.
The promotion rules also apply to expressions containing values of two or more data types—that is, mixed-type expressions. Each value’s type in a mixed-type expression is promoted to the expression’s “highest” type (actually a temporary copy of each value is created and used—the original values remain unchanged). The following table lists the arithmetic data types in order from “highest type” to “lowest type.”
Converting values to lower fundamental types can cause errors due to narrowing conversions. Therefore, a value can be converted to a lower fundamental type only by explicitly assigning the value to a variable of lower type (some compilers will issue a warning in this case) or by using a cast operator. Function argument values are converted to the parameter types in a function prototype as if they were being assigned directly to variables of those types. If a square
function with an int
parameter is called with a double
argument, the argument is converted to int
(a lower type and thus a narrowing conversion), and square
could return an incorrect value. For example, square(4.5)
would return 16
, not 20.25
. Some compilers warn you about the narrowing conversion. For example, Microsoft Visual C++ issues the warning,
'argument': conversion from 'double' to 'int', possible loss of data
If you must perform an explicit narrowing conversion, the C++ Core Guidelines recommend using a narrow_cast
operator6 from the Guidelines Support Library (GSL)—we’ll use this in Figs. 5.5 and 5.6. This library has several implementations. Microsoft provides an open-source version that has been tested on numerous platform/compiler combinations, including our three preferred compilers and platforms. You can download their GSL implementaton from:
https://github.com/Microsoft/GSL
6. C++ Core Guidelines. Accessed May 10, 2020. http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-narrowing
.
For your convenience, we provided the GSL library with this book’s code examples in the subfolder libraries/GSL
.
The GSL is a header-only library, so you can use it in your programs simply by including the header "gsl/gsl"
. You must point your compiler to the GSL
folder’s include
sub-folder, so the compiler knows where to find the include file, as you did when you used class BigNumber
at the end of Section 3.12. The following statement uses the narrow_cast
operator (from namespace gsl
) to convert the double value 7.5
to the int
value 7
:
gsl::narrow_cast<int>(7.5)
As with the other named cast operators, like static_cast
, the value in parentheses is converted to the type in angle brackets, <>
.
The C++ Standard Library is divided into many portions, each with its own header. The headers contain the function prototypes for the related functions that form each portion of the library. The headers also contain definitions of various class types and functions, as well as constants needed by those functions. A header “instructs” the compiler on how to interface with library and user-written components.
The following table lists some common C++ Standard Library headers, many of which are discussed later in this book. The term “macro” that’s used several times is discussed in detail in Appendix E, Preprocessor. For a complete list of the 96 C++20 standard library headers, visit
https://en.cppreference.com/w/cpp/header
On that page, you’ll see approximately three dozen additional headers that are marked as either deprecated or removed. Deprecated headers are ones you should no longer use, and removed headers are no longer included in the C++ standard library.
We now take a brief and hopefully entertaining diversion into a popular programming application, namely simulation and game playing. In this and the next section, we develop a game-playing program that includes multiple functions.
rand
FunctionThe element of chance can be introduced into computer applications by using the C++ Standard Library function rand
. Consider the following statement:
i = rand();
The function rand
generates an integer between 0 and RAND_MAX
(a symbolic constant defined in the <cstdlib>
header). You can determine the value of RAND_MAX
for your system simply by displaying the constant. If rand
truly produces integers at random, every number between 0 and RAND_MAX
has an equal chance (or probability) of being chosen each time rand
is called.
The range of values produced directly by the function rand
often is different than what a specific application requires. For example, a program that simulates coin tossing might need only 0 for “heads” and 1 for “tails.” A program that simulates rolling a six-sided die would require random integers in the range 1 to 6. A program that randomly predicts the next type of spaceship (out of four possibilities) that will fly across the horizon in a video game might require random integers in the range 1 through 4.
To demonstrate rand
, Fig. 5.2 simulates ten rolls of a six-sided die and displays the value of each roll. The function prototype for the rand
function is in <cstdlib>
. To produce integers in the range 0 to 5, we use the remainder operator (%
) with rand
as follows:
rand() % 6
This is called scaling. The number 6 is called the scaling factor. We then shift the range of numbers produced by adding 1 to our previous result. Figure 5.2 confirms that the results are in the range 1 to 6. If you execute this program more than once, you’ll see that it produces the same “random” values each time. We’ll show how to fix this in Figure 5.4.
1 // fig05_02.cpp 2 // Shifted, scaled integers produced by 1 + rand() % 6. 3 #include <iostream> 4 #include <cstdlib> // contains function prototype for rand 5 using namespace std; 6 7 int main() { 8 for (int counter{1}; counter <= 10; ++counter) { 9 // pick random number from 1 to 6 and output it 10 cout << (1 + rand() % 6) << " "; 11}
12 13cout << endl;
14}
6 6 5 5 6 5 1 1 5 3
To show that values produced by rand
occur with approximately equal likelihood, Fig. 5.3 simulates 60,000,000 rolls of a die.7 Each integer in the range 1 to 6 should appear approximately 10,000,000 times (one-sixth of the rolls). The program’s output confirms this. The face
variable’s definition in the switch
’s initializer (line 19) is preceded by const
. This is a good practice for any variable that should not change once it’s initialized—this enables the compiler to report errors if you accidentally modify the variable.
7. When co-author Harvey Deitel first implemented this example for his classes in 1976, he performed only 600 die rolls—6000 would have taken too long. On our system, this program took approximately five seconds to complete 60,000,000 die rolls! 600,000,000 die rolls took approximately one minute. The die rolls occur sequentially. In our concurrency chapter, we’ll explore how to parallelize this application to take advantage of today’s multi-core computers.
1 // fig05_03.cpp 2 // Rolling a six-sided die 60,000,000 times. 3 #include <iostream> 4 #include <iomanip> 5 #include <cstdlib> // contains function prototype for rand 6 using namespace std; 7 8 int main() { 9 int frequency1{0}; // count of 1s rolled 10 int frequency2{0}; // count of 2s rolled 11 int frequency3{0}; // count of 3s rolled 12 int frequency4{0}; // count of 4s rolled 13 int frequency5{0}; // count of 5s rolled 14 int frequency6{0}; // count of 6s rolled 15 16 // summarize results of 60,000,000 rolls of a die 17 for (int roll{1}; roll <= 60'000'000; ++roll) { 18 // determine roll value 1-6 and increment appropriate counter 19 switch (const int face{1 + rand() % 6}; face) { 20 case 1: 21 ++frequency1; // increment the 1s counter 22 break; 23 case 2: 24 ++frequency2; // increment the 2s counter 25 break; 26 case 3: 27 ++frequency3; // increment the 3s counter 28 break; 29 case 4: 30 ++frequency4; // increment the 4s counter 31 break; 32 case 5: 33 ++frequency5; // increment the 5s counter 34 break; 35 case 6: 36 ++frequency6; // increment the 6s counter 37 break; 38 default: // invalid value 39 cout << "Program should never get here!"; 40}
41}
42 43 cout << "Face" << setw(13) << "Frequency" << endl; // output headers 44 cout << " 1" << setw(13) << frequency1 45 << " 2" << setw(13) << frequency2 46 << " 3" << setw(13) << frequency3 47 << " 4" << setw(13) << frequency4 48 << " 5" << setw(13) << frequency5 49 << " 6" << setw(13) << frequency6 << endl; 50}
Face Frequency
1 9999294
2 10002929
3 9995360
4 10000409
5 10005206
6 9996802
As the output shows, we can simulate rolling a six-sided die by scaling and shifting the values rand
produces. The default case (lines 38–39) in the switch
should never execute because the switch’s controlling expression (face
) always has values in the range 1–6. After we study arrays in Chapter 6, we show how to replace the entire switch in Fig. 5.3 elegantly with a single-line statement. Many programmers provide a default
case in every switch
statement to catch errors even if they feel confident that their programs are error-free.
Before C++14, you’d represent the integer value 60,000,000 as 60000000
in a program. Typing numeric literals with many digits can be error-prone. To make numeric literals more readable and help reduce errors, you can use C++14’s digit separator '
(a single-quote character) between groups of digits—60'000'000
(line 17) represents the integer value 60,000,000. You might wonder why single-quote characters are used rather than commas. If we use 60,000,000
in line 17, C++ treats the commas as comma operators, and the value of 60,000,000
would be the rightmost expression (000
). The loop-continuation condition would immediately be false—a logic error in this program.
srand
Executing the program of Fig. 5.2 again produces
6 6 5 5 6 5 1 1 5 3
This is the same sequence of values shown in Fig. 5.2. How can these be random numbers?
Function rand
actually generates pseudorandom numbers. Repeatedly calling rand
produces a sequence of numbers that appears to be random. However, the sequence repeats itself each time the program executes. When debugging a simulation program, random-number repeatability is essential for proving that corrections to the program work properly. Once a program has been thoroughly debugged, it can be conditioned to produce a different sequence of random numbers for each execution. This is called randomizing and can be accomplished with the C++ Standard Library function srand
from the header <cstdlib>
. Function srand
takes an unsigned
integer argument and seeds the rand
function to produce a different sequence of random numbers for each execution.
srand
Figure 5.4 demonstrates function srand
. The program produces a different sequence of random numbers each time it executes, provided that the user enters a different seed. We used the same seed in the first and third sample outputs, so the same series of 10 numbers is displayed in each of those outputs.
Security For security, ensure that your program seeds the random-number generator differently (and only once) each time the program executes; otherwise, an attacker would easily be able to determine the sequence of pseudorandom numbers that would be produced.
1 // fig05_04.cpp 2 // Randomizing the die-rolling program. 3 #include <iostream> 4 #include <iomanip> 5 #include <cstdlib> // contains prototypes for functions srand and rand 6 using namespace std; 7 8 int main() { 9 int seed{0}; // stores the seed entered by the user 10 11 cout << "Enter seed: "; 12cin >> seed;
13 srand(seed); // seed random number generator 14 15 // loop 10 times 16 for (int counter{1}; counter <= 10; ++counter) { 17 // pick random number from 1 to 6 and output it 18 cout << (1 + rand() % 6) << " "; 19}
20 21cout << endl;
22}
Enter seed:
67
6 1 4 6 2 1 6 1 6 4
Enter seed:
432
4 6 3 1 6 3 1 5 4 2
Enter seed:
67
6 1 4 6 2 1 6 1 6 4
To randomize without having to enter a seed each time, we can use a statement like
srand(gsl::narrow_cast<unsigned int>(time(0)));
This causes the computer to read its clock to obtain the value for the seed. Function time
(with the argument 0
as written in the preceding statement) typically returns the current time as the number of seconds since January 1, 1970, at midnight Greenwich Mean Time (GMT). This value (which is of type time_t
) is converted to unsigned int
and used as the seed to the random-number generator. The narrow_cast
(from the Guidelines Support Library) in the preceding statement eliminates a compiler warning that’s issued if you pass a time_t
value to a function that expects an unsigned int
.8 The function prototype for time
is in <ctime>
.
8. Our code originally used a static_cast
rather than a narrow_cast
. The C++ Core Guidelines checker in Microsoft Visual C++ reported that narrowing conversions should be performed with narrow_cast
operators.
Previously, we simulated rolling a six-sided die with the statement
int face{1 + rand() % 6};
which always assigns an integer (at random) to variable face
in the range 1 ≤ face
≤ 6. The width of this range (i.e., the number of consecutive integers in the range) is 6, and the starting number in the range is 1. Referring to the preceding statement, we see that the width of the range is determined by the number used to scale rand
with the remainder operator (i.e., 6), and the starting number of the range is equal to the number (i.e., 1) that is added to the expression rand % 6
. We can generalize this result as
int
variableName{
shiftingValue + rand() %
scalingFactor};
where the shiftingValue is equal to the first number in the desired range of consecutive integers, and the scalingFactor is equal to the width of the desired range of consecutive integers.
enum
sOne of the most popular games of chance is a dice game known as “craps,” which is played in casinos and back alleys worldwide. The rules of the game are straightforward:
A player rolls two dice. Each die has six faces. These faces contain 1, 2, 3, 4, 5 and 6 spots. After the dice have come to rest, the sum of the spots on the two upward faces is calculated. If the sum is 7 or 11 on the first roll, the player wins. If the sum is 2, 3 or 12 on the first roll (called “craps”), the player loses (i.e., the “house” wins). If the sum is 4, 5, 6, 8, 9 or 10 on the first roll, then that sum becomes the player’s “point.” To win, you must keep rolling the dice until you “make your point.” The player loses by rolling a 7 before making the point.
In the rules, notice that the player must roll two dice on the first roll and all subsequent rolls. We will define a rollDice
function to roll the dice and compute and display their sum. The function will be defined once, but may be called multiple times—once for the game’s first roll and possibly many more times if the player does not win or lose on the first roll. Below are the outputs of several sample executions showing:
• winning on the first roll by rolling a 7,
• losing on the first roll by rolling a 12,
• winning on a subsequent roll by “making the point” before rolling a 7, and
• losing on a subsequent roll by rolling a 7 before “making the point.”
Player rolled 2 + 5 = 7 Player wins
Player rolled 6 + 6 = 12 Player loses
Player rolled 3 + 3 = 6 Point is 6 Player rolled 5 + 3 = 8 Player rolled 4 + 5 = 9 Player rolled 2 + 1 = 3 Player rolled 1 + 5 = 6 Player wins
Player rolled 1 + 3 = 4 Point is 4 Player rolled 4 + 6 = 10 Player rolled 2 + 4 = 6 Player rolled 6 + 4 = 10 Player rolled 2 + 3 = 5 Player rolled 2 + 4 = 6 Player rolled 1 + 1 = 2 Player rolled 4 + 4 = 8 Player rolled 4 + 3 = 7 Player loses
The craps program (Fig. 5.5) simulates the game using two functions—main
and roll-Dice
—and the switch
, while
, if…else
, nested if…else
and nested if
statements. Function rollDice
’s prototype (line 9) indicates that the function takes no arguments (empty parentheses) and returns an int
(the sum of the dice).
1 // fig05_05.cpp 2 // Craps simulation. 3 #include <iostream> 4 #include <cstdlib> // contains prototypes for functions srand and rand 5 #include <ctime> // contains prototype for function time 6 #include "gsl/gsl" // Guidelines Support Library for narrow_cast 7 using namespace std; 8 9 int rollDice(); // rolls dice, calculates and displays sum 10 11 int main() {
enum
sThe player may win or lose on the first roll or any subsequent roll. The program tracks this with the variable gameStatus
, which line 19 declares to be of the new type Status
. Line 13 declares a user-defined type called a scoped enumeration which is introduced by the keywords enum class
, followed by a type name (Status
) and a set of identifiers representing integer constants:
12 // scoped enumeration with constants that represent the game status 13 enum class Status {keepRolling, won, lost}; // all caps in constants 14 15 // randomize random number generator using current time 16 srand(gsl::narrow_cast<unsigned int>(time(0))); 17 18 int myPoint{0}; // point if no win or loss on first roll 19 Status gameStatus{Status::keepRolling}; // game is not over 20
The underlying values of these enumeration constants are of type int
, start at 0
and increment by 1
, by default (you’ll soon see how to change this). In the Status
enumeration, the constant keepRolling
has the value 0, won
has the value 1, and lost
has the value 2. The identifiers in an enum class
must be unique, but separate enumeration constants can have the same value. Variables of user-defined type Status
can be assigned only the constants declared in the enumeration.
By convention, you should capitalize the first letter of an enum class
’s name and the first letter of each subsequent word in a multi-word enum class
name (e.g., ProductCode
). The constants in an enum class
should use the same naming conventions as variables.9,10
9. C++ Core Guidelines. Accessed May 11, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Renum-caps
.
10. In legacy C++ code, you’ll commonly see enum
constants in all uppercase letters—that practice is now deprecated.
To reference a scoped enum
constant, qualify the constant with the scoped enum
’s type name (in this example, Status
) and the scope-resolution operator (::
), as shown in line 19, which initializes gameStatus
to Status::keepRolling
. For a win, the program sets gameStatus
to Status::won
. For a loss, the program sets gameStatus
to Status::lost
.
The following switch
determines whether the player wins or loses on the first roll:
21 // determine game status and point (if needed) based on first roll 22 switch (const int sumOfDice{rollDice()}; sumOfDice) { 23 case 7: // win with 7 on first roll 24 case 11: // win with 11 on first roll 25 gameStatus = Status::won; 26 break; 27 case 2: // lose with 2 on first roll 28 case 3: // lose with 3 on first roll 29 case 12: // lose with 12 on first roll 30 gameStatus = Status::lost; 31 break; 32 default: // did not win or lose, so remember point 33 myPoint = sumOfDice; // remember the point 34 cout << "Point is " << myPoint << endl; 35 break; // optional at end of switch 36 } 37
17 The switch
’s initializer (line 22) creates the variable sumOfDice
and initializes it by calling function rollDice
. If the roll is 7
or 11
, line 25 sets gameStatus
to Status::won
. If the roll is 2
, 3
, or 12
, line 30 sets gameStatus
to Status::lost
. For other values, gameStatus
remains unchanged (Status::keepRolling
), line 33 saves sumOfDice
in myPoint
, and line 34 displays the myPoint
.
After the first roll, if gameStatus
is Status::keepRolling
, execution proceeds with the following while
statement:
38 // while game is not complete 39 while (Status::keepRolling == gameStatus) { // not won or lost 40 // roll dice again and determine game status 41 if (const int sumOfDice{rollDice()}; sumOfDice == myPoint) { 42 gameStatus = Status::won; 43 } 44 else { 45 if (sumOfDice == 7) { // lose by rolling 7 before point 46 gameStatus = Status::lost; 47 } 48 } 49 } 50
17 In each iteration of the while
, the if
statement’s initializer (line 41) calls rollDice
to produce a new sumOfDice
. If sumOfDice
matches myPoint
, the program sets gameStatus
to Status::won
(line 42), and the loop terminates. If sumOfDice
is 7
, the program sets gameStatus
to Status::lost
(line 46), and the loop terminates. Otherwise, the loop continues executing.
When the preceding loop terminates, the program proceeds to the following if…else
statement, which prints "Player wins"
if gameStatus
is Status::won
or "Player loses"
if gameStatus
is Status::lost
:
51 // display won or lost message 52 if (Status::won == gameStatus) { 53 cout << "Player wins" << endl; 54 } 55 else { 56 cout << "Player loses" << endl; 57 } 58 } 59
rollDice
To roll the dice, function rollDice
uses the die-rolling calculation shown previously to initialize variables die1
and die2
, then calculates their sum
. Lines 67–68 display die1
’s, die2
’s and sum
’s values before line 69 returns sum
.
60 // roll dice, calculate sum and display results 61 int rollDice() { 62 const int die1{1 + rand() % 6}; // first die roll 63 const int die2{1 + rand() % 6}; // second die roll 64 const int sum{die1 + die2}; // compute sum of die values 65 66 // display results of this roll 67 cout << "Player rolled " << die1 << " + " << die2 68 << " = " << sum << endl; 69 return sum; 70 }
enum
sQualifying an enum class
’s constant with its type name and ::
explicitly identifies the constant as being in the scope of the specified enum class
. If another enum class
contains the same identifier, it’s always clear which constant is being used because the type name and ::
are required. In general, you should use unique values for an enum
’s constants to help prevent hard-to-find logic errors.
Another popular scoped enumeration is
enum class Months {jan = 1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec};
which creates user-defined enum class
type Months
with enumeration constants representing the months of the year. The first value in the preceding enumeration is explicitly set to 1
, so the remaining values increment from 1
, resulting in the values 1
through 12
. Any enumeration constant can be assigned an integer value in the enumeration definition. Subsequent enumeration constants each have a value 1 higher than the preceding constant until the next explicit setting.
Enumerations also can be defined with the keyword enum
followed by a type name and a set of integer constants represented by identifiers, as in
enum Status {keepRolling, won, lost};
The constants in such an enum
are unscoped—you can refer to them simply by their names keepRolling
, won
and lost
. If two or more unscoped enum
s contain constants with the same names, this can lead to naming conflicts and compilation errors.
enum
’s ConstantsAn enumeration’s constants have integer values. An unscoped enum
’s underlying type depends on its constants’ values and is guaranteed to be large enough to store its constants’ values. A scoped enum
’s underlying integral type is int
, but you can specify a different type by following the type name with a colon (:
) and the integral type. For example, we can specify that the constants in the enum class Status
should have type long
, as in
enum class Status : long {keepRolling, won, lost};
using enum
DeclarationIf the type of an enum class
’s constants is obvious, based on the context in which they’re used—such as in our craps example—C++20’s using enum
declaration11,12 allows you to reference an enum class
’s constants without the type name and scope-resolution operator (::
). For example, adding the following statement after the enum class
declaration:
using enum Status;
12. At the time of this writing, this feature works only in Microsoft’s Visual C++ compiler.
would allow the rest of the program to use keepRolling
, won
and lost
, rather than Status::keepRolling
, Status::won
and Status::lost
, respectively. You also may use an individual enum class
constant with a declaration of the form
using enum Status::keepRolling;
This would allow your code to use keepRolling
without the Status::
qualifier.
Security Function rand
—which was inherited into C++ from the C Standard Library—does not have “good statistical properties” and can be predictable.13 This makes programs that use rand
less secure. C++11 provides a more secure library of random-number capabilities that can produce nondeterministic random numbers—a set of random numbers that can’t be predicted. Such random-number generators are used in simulations and security scenarios where predictability is undesirable. These capabilities are located in the C++ Standard Library’s <random>
header.
13. “Do not use the rand()
function for generating pseudorandom numbers.” Accessed May 9, 2020. https://wiki.sei.cmu.edu/confluence/display/c/MSC30-C.+Do+not+use+the+rand%28%29+function+for+generating+pseudorandom+numbers
.
Random-number generation is a sophisticated topic for which mathematicians have developed many algorithms with different statistical properties. For flexibility based on how random numbers are used in programs, C++11 provides many classes that represent various random-number generation engines and distributions. An engine implements a random-number generation algorithm that produces pseudorandom numbers. A distribution controls the range of values produced by an engine, the value’s types (e.g., int
, double
, etc.) and the value’s statistical properties. We’ll use the default random-number generation engine—default_random_engine
—and a uniform_int_distribution
, which evenly distributes pseudorandom integers over a specified value range. The default range is from 0 to the maximum value of an int
on your platform.
Figure 5.6 uses the default_random_engine
and the uniform_int_distribution
to roll a six-sided die. Line 14 creates a default_random_engine
object named engine
. Its constructor argument seeds the random-number generation engine with the current time. If you don’t pass a value to the constructor, the default seed will be used, and the program will produce the same sequence of numbers each time it executes—this is useful for testing purposes. Line 15 creates randomInt
—a uniform_int_distribution
object that produces int
values (specified by <int>
) in the range 1 to 6 (specified by the initializer {1, 6}
). The expression randomInt(engine)
(line 20) returns one int
in the range 1 to 6.
1 // fig05_06.cpp 2 // Using a C++11 random-number generation engine and distribution 3 // to roll a six-sided die more securely. 4 #include <iostream> 5 #include <iomanip> 6 #include <random> // contains C++11 random number generation features 7 #include <ctime> 8 #include "gsl/gsl" 9 using namespace std; 10 11 int main() { 12 // use the default random-number generation engine to 13 // produce uniformly distributed pseudorandom int values from 1 to 6 14 default_random_engine engine{gsl::narrow_cast<unsigned int>(time(0))}; 15 const uniform_int_distribution<int> randomInt{1, 6}; 16 17 // loop 10 times 18 for (int counter{1}; counter <= 10; ++counter) { 19 // pick random number from 1 to 6 and output it 20 cout << setw(10) << randomInt(engine); 21}
22 23cout << endl;
24}
2 1 2 3 5 6 1 5 6 4
The notation <int>
in line 15 indicates that uniform_int_distribution
is a class template. In this case, any integer type can be specified in the angle brackets (<
and >
). In Chapter 19, we discuss how to create class templates, and various other chapters show how to use existing class templates from the C++ Standard Library. For now, you can use class template uniform_int_distribution
by mimicking the syntax shown in the example.
The portion of a program where an identifier can be used is known as its scope. For example, when we declare a local variable in a block, it can be referenced only
• from the point of its declaration in that block and
• in nested blocks that appear within that block after the variable’s declaration.
This section discusses block scope and global namespace scope. Parameter names in function prototypes have function-prototype scope—they’re known only in the prototype in which they appear. Later we’ll see other scopes, including class scope in Chapter 11, and function scope and namespace scope in Chapter 23.
Identifiers declared inside a block have block scope, which begins at the identifier’s declaration and ends at the terminating right brace (}
) of the enclosing block. Local variables have block scope, as do function parameters (even though they’re declared outside the block’s braces). Any block can contain variable declarations. When blocks are nested and an identifier in an outer block has the same name as an identifier in an inner block, the one in the outer block is “hidden” until the inner block terminates. The inner block “sees” its own local variable’s value and not that of the enclosing block’s identically named variable.
Accidentally using the same name for an identifier in an inner block that’s used for an identifier in an outer block when, in fact, you want the identifier in the outer block to be active for the duration of the inner block, is typically a logic error. Avoid variable names in inner scopes that hide names in outer scopes. Most compilers will warn you about this.
Local variables also may be declared static
. Such variables also have block scope, but unlike other local variables, a static
local variable retains its value when the function returns to its caller. The next time the function is called, the static
local variable contains the value it had when the function last completed execution. The following statement declares static
local variable count
and initializes to 1:
static int count{1};
By default, all static
local variables of numeric types are initialized to zero. The following statement declares static
local variable count
and initializes it to 0
:
static int count;
Default initialization of non-fundamental-type variables depends on the type—for example, a string
’s default value is the empty string (""
). We’ll say more about default initialization in later chapters.
An identifier declared outside any function or class has global namespace scope. Such an identifier is “known” to all functions after its declaration in the source-code file. Function definitions, function prototypes placed outside a function, class definitions and global variables all have global namespace scope. Global variables are created by placing variable declarations outside any class or function definition. Such variables retain their values throughout a program’s execution.
Declaring a variable as global rather than local allows unintended side effects to occur when a function that does not need access to the variable accidentally or maliciously modifies it. Except for truly global resources such as cin
and cout
, you should avoid global variables. This is an example of the principle of least privilege, which is fundamental to good software engineering. It states that code should be granted only the amount of privilege and access that it needs to accomplish its designated task, but no more. An example of this is the scope of a local variable, which should not be visible when it’s not needed. A local variable is created when the function is called, used by that function while it executes then goes away when the function returns. The principle of least privilege makes your programs more robust by preventing code from accidentally (or maliciously) modifying variable values that should not be accessible to it.
In general, variables should be declared in the narrowest scope in which they need to be accessed. Variables used only in a particular function should be declared as local variables in that function rather than as global variables.
Figure 5.7 demonstrates scoping issues with global variables, local variables and static
local variables. We show portions of the program with their corresponding outputs for discussion purposes. Line 10 declares and initializes global variable x
to 1. This global variable is hidden in any block (or function) that declares a variable named x
.
1 // fig05_07.cpp 2 // Scoping example. 3 #include <iostream> 4 using namespace std; 5 6 void useLocal(); // function prototype 7 void useStaticLocal(); // function prototype 8 void useGlobal(); // function prototype 9 10 int x{1}; // global variable 11
main
In main
, line 13 displays the value of global variable x
. Line 15 initializes local variable x
to 5
. Line 17 outputs this variable to show that the global x
is hidden in main
. Next, lines 19–23 define a new block in main
in which another local variable x
is initialized to 7
(line 20). Line 22 outputs this variable to show that it hides x
in the outer block of main
as well as the global x
. When the block exits, the variable x
with value 7
is destroyed automatically. Next, line 25 outputs the local variable x
in the outer block of main
to show that it’s no longer hidden.
12 int main() { 13 cout << "global x in main is " << x << endl; 14 15 const int x{5}; // local variable to main 16 17 cout << "local x in main's outer scope is " << x << endl; 18 19 { // block starts a new scope 20 const int x{7}; // hides both x in outer scope and global x 21 22 cout << "local x in main's inner scope is " << x << endl; 23 } 24 25 cout << "local x in main's outer scope is " << x << endl; 26
global x in main is 1 local x in main's outer scope is 5 local x in main's inner scope is 7 local x in main's outer scope is 5
To demonstrate other scopes, the program defines three functions—useLocal
, useStaticLocal
and useGlobal
—each of which takes no arguments and returns nothing. The rest of main
(shown below) calls each function twice in lines 27–32. After executing functions useLocal
, useStaticLocal
and useGlobal
twice each, the program prints the local variable x
in main
again to show that none of the function calls modified the value of x
in main
, because the functions all referred to variables in other scopes.
27 useLocal(); // useLocal has local x 28 useStaticLocal(); // useStaticLocal has static local x 29 useGlobal(); // useGlobal uses global x 30 useLocal(); // useLocal reinitializes its local x 31 useStaticLocal(); // static local x retains its prior value 32 useGlobal(); // global x also retains its prior value 33 34 cout << " local x in main is " << x << endl; 35 } 36
local x is 25 on entering useLocal local x is 26 on exiting useLocal local static x is 50 on entering useStaticLocal local static x is 51 on exiting useStaticLocal global x is 1 on entering useGlobal global x is 10 on exiting useGlobal local x is 25 on entering useLocal local x is 26 on exiting useLocal local static x is 51 on entering useStaticLocal local static x is 52 on exiting useStaticLocal global x is 10 on entering useGlobal global x is 100 on exiting useGlobal local x in main is 5
useLocal
Function useLocal
initializes local variable x
to 25
(line 39). When main
calls useLocal
(lines 27 and 30), the function prints the variable x
, increments it and prints it again before the function returns program control to its caller. Each time the program calls this function, the function recreates local variable x
and reinitializes it to 25
.
37 // useLocal reinitializes local variable x during each call 38 void useLocal() { 39 int x{25}; // initialized each time useLocal is called 40 41 cout << " local x is " << x << " on entering useLocal" << endl; 42 ++x; 43 cout << "local x is " << x << " on exiting useLocal" << endl; 44 } 45
useStaticLocal
Function useStaticLocal
declares static
variable x
and initializes it to 50
. Local variables declared as static
retain their values even when they’re out of scope (i.e., the function in which they’re declared is not executing). When line 28 in main
calls useStaticLocal
, the function prints its local x
, increments it and prints it again before the function returns program control to its caller. In the next call to this function (line 31), static
local variable x
contains the value 51
. The initialization in line 50 occurs only the first time useStaticLocal
is called (line 28).
46 // useStaticLocal initializes static local variable x only the 47 // first time the function is called; value of x is saved 48 // between calls to this function 49 void useStaticLocal() { 50 static int x{50}; // initialized first time useStaticLocal is called 51 52 cout << " local static x is " << x << " on entering useStaticLocal" 53 << endl; 54 ++x; 55 cout << "local static x is " << x << " on exiting useStaticLocal" 56 << endl; 57 } 58
useGlobal
Function useGlobal
does not declare any variables. Therefore, when it refers to variable x
, the global x
(line 10, preceding main
) is used. When main
calls useGlobal
(line 29), the function prints the global variable x
, multiplies it by 10 and prints it again before the function returns to its caller. The next time main
calls useGlobal
(line 32), the global variable has its modified value, 10
.
59 // useGlobal modifies global variable x during each call 60 void useGlobal() { 61 cout << " global x is " << x << " on entering useGlobal" << endl; 62 x *= 10; 63 cout << "global x is " << x << " on exiting useGlobal" << endl; 64 }
Implementing a program as a set of functions is good from a software engineering standpoint, but function calls involve execution-time overhead. C++ provides inline functions to help reduce function-call overhead. Placing inline
before a function’s return type in the function definition advises the compiler to generate a copy of the function’s body code in every place where the function is called (when appropriate) to avoid a function call. This often makes the program larger. The compiler can ignore the inline
qualifier. Reusable inline
functions are typically placed in headers so that their definitions can be inlined in each source file that uses them.
If you change an inline
function’s definition, you must recompile any code that calls that function. Though compilers can inline code for which you have not explicitly used inline
, the ISO’s C++ Core Guidelines indicate that you should declare “small and time-critical” functions inline
.14 ISO also provides an extensive FAQ on the subtleties of using inline functions:
https://isocpp.org/wiki/faq/inline-functions
14. C++ Core Guidelines. Accessed May 11, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rf-inline
.
Figure 5.8 uses inline
function cube
(lines 9–11) to calculate the volume of a cube.)
1 // fig05_08.cpp
2 // inline function that calculates the volume of a cube.
3 #include <iostream>m
4 using namespace std;
5
6 // Definition of inline function cube. Definition of function appears
7 // before function is called, so a function prototype is not required.
8 // First line of function definition also acts as the prototype.
9 inline double cube(double side) {
10 return side * side * side; // calculate cube
11 }
12
13 int main() {
14 double sideValue; // stores value entered by user
15 cout << "Enter the side length of your cube: ";
16 cin >> sideValue; // read value from user
17
18 // calculate cube of sideValue and display result
19 cout << "Volume of cube with side "
20 << sideValue << " is " << cube(sideValue) << endl;
21 }
Enter the side length of your cube:
3.5
Volume of cube with side 3.5 is 42.875
PERF Two ways to pass arguments to functions in many programming languages are pass-by-value and pass-by-reference. When an argument is passed by value, a copy of the argument’s value is made and passed to the called function. Changes to the copy do not affect the original variable’s value in the caller. This prevents the accidental side effects that so greatly hinder the development of correct and reliable software systems. So far, each argument in the book has been passed by value. One disadvantage of pass-by-value is that, if a large data item is being passed, copying that data can take a considerable amount of execution time and memory space.
This section introduces reference parameters—the first of the two means C++ provides for performing pass-by-reference.15 When a variable is passed by reference, the caller gives the called function the ability to access that variable in the caller directly and to modify the variable.
15. Chapter 7 discusses pointers, which enable an alternate form of pass-by-reference in which the style of the function call clearly indicates pass-by-reference (and the potential for modifying the caller’s arguments).
PERF Pass-by-reference is good for performance reasons because it can eliminate the pass-by-value overhead of copying large amounts of data. But Pass-by-reference can weaken security—the called function can corrupt the caller’s data.
Security After this section’s example, we’ll show how to achieve the performance advantage of pass-by-reference while simultaneously achieving the software engineering advantage of protecting the caller’s data from corruption.
A reference parameter is an alias for its corresponding argument in a function call. To indicate that a function parameter is passed by reference, simply follow the parameter’s type in the function prototype by an ampersand (&
); use the same convention when listing the parameter’s type in the function header. For example, the parameter declaration
int& number
when reading from right to left is pronounced “number
is a reference to an int
.” As always, the function prototype and header must agree.
In the function call, simply mention a variable by name to pass it by reference. In the called function’s body, the reference parameter (e.g., number
) refers to the original variable in the caller, which can be modified directly by the called function.
Figure 5.9 compares pass-by-value and pass-by-reference with reference parameters. The “styles” of the arguments in the calls to function squareByValue
and function square-ByReference
are identical—both variables are simply mentioned by name in the function calls. The compiler checks the function prototypes and definitions to determine whether to use pass-by-value or pass-by-reference.
1 // fig05_09.cpp 2 // Passing arguments by value and by reference. 3 #include <iostream> 4 using namespace std; 5 6 int squareByValue(int x); // function prototype (for value pass) 7 void squareByReference(int& z); // function prototype (for reference pass) 8 9 int main() { 10 const int x{2}; // value to square using squareByValue 11 int z{4}; // value to square using squareByReference 12 13 // demonstrate squareByValue 14 cout << "x = " << x << " before squareByValue "; 15 cout << "Value returned by squareByValue: " 16<< squareByValue(x) << endl;
17 cout << "x = " << x << " after squareByValue " << endl; 18 19 // demonstrate squareByReference 20 cout << "z = " << z << " before squareByReference" << endl; 21squareByReference(z);
22 cout << "z = " << z << " after squareByReference" << endl; 23}
24 25 // squareByValue multiplies number by itself, stores the 26 // result in number and returns the new value of number 27 int squareByValue(int number) { 28 return number *= number; // caller's argument not modified 29}
30 31 // squareByReference multiplies numberRef by itself and stores the result 32 // in the variable to which numberRef refers in function main 33 void squareByReference(int& numberRef) { 34 numberRef *= numberRef; // caller's argument modified 35}
x = 2 before squareByValue
Value returned by squareByValue: 4 x = 2 after squareByValue
z = 4 before squareByReference z = 16 after squareByReference
References can also be used as aliases for other variables within a function (although they typically are used with functions as shown in Fig. 5.9). For example, the code
int count{1}; // declare integer variable count int& cRef{count}; // create cRef as an alias for count ++cRef; // increment count (using its alias cRef)
increments variable count
by using its alias cRef
. Reference variables must be initialized in their declarations and cannot be reassigned as aliases to other variables. In this sense, references are constant. All operations performed on the alias (i.e., the reference) are actually performed on the original variable. The alias is simply another name for the original variable. Unless it’s a reference to a constant (discussed below), a reference’s initializer must be an lvalue—something that can appear on the left side of an assignment, like a variable name. A reference may not be initialized with a constant or rvalue expression—that is, something might appear on the right side of an assignment, such as the result of a calculation.
const
ReferencesTo specify that a reference parameter should not be allowed to modify the corresponding argument in the caller, place the const
qualifier before the type name in the parameter’s declaration. For example, consider a displayName
function:
void displayName(std::string name) {
std::cout << name << std::endl;
}
When called, it receives a copy of its string
argument. Since string
objects can be large, this copy operation could degrade an application’s performance. For this reason, string
objects (and objects in general) should be passed to functions by reference.
Also, the displayName
function does not need to modify its argument, so following the principle of least privilege, we’d declare the parameter as
const std::string& name
PERF Security Reading this from right-to-left, the name
parameter is a reference to a string
constant. We get the performance of passing the string
by reference. Also, displayName
treats the argument as a constant, so displayName
cannot modify the value in the caller—so we get the security of pass-by-value.
Functions can return references to local variables, but this can be dangerous. When returning a reference to a local non-static
variable, the reference refers to a variable that’s discarded when the function terminates. An attempt to access such a variable yields undefined behavior, often crashing the program or corrupting data.16 References to undefined variables are called dangling references. This is a logic error for which compilers typically issue a warning. Software-engineering teams often have policies requiring that before code can be deployed, it must compile without warnings.
16. C++ Core Guidelines. Accessed May 11, 2020. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rf-dangle
.
It’s common for a program to invoke a function repeatedly with the same argument value for a particular parameter. In such cases, you can specify that such a parameter has a default argument, i.e., a default value to be passed to that parameter. When a program omits an argument for a parameter with a default argument in a function call, the compiler rewrites the function call, inserting the default value of that argument.
boxVolume
Function with Default ArgumentsFigure 5.10 demonstrates using default arguments to calculate a box’s volume. The function prototype for boxVolume
(line 7) specifies that all three parameters have default values of 1
by placing = 1
to the right of each parameter.
1 // fig05_10.cpp 2 // Using default arguments. 3 #include <iostream> 4 using namespace std; 5 6 // function prototype that specifies default arguments 7 int boxVolume(int length = 1, int width = 1, int height = 1); 8 9 int main() { 10 // no arguments--use default values for all dimensions 11 cout << "The default box volume is: " << boxVolume(); 12 13 // specify length; default width and height 14 cout << " The volume of a box with length 10, " 15 << "width 1 and height 1 is: " << boxVolume(10); 16 17 // specify length and width; default height 18 cout << " The volume of a box with length 10, " 19 << "width 5 and height 1 is: " << boxVolume(10, 5); 20 21 // specify all arguments 22 cout << " The volume of a box with length 10, " 23 << "width 5 and height 2 is: " << boxVolume(10, 5, 2) 24<< endl;
25}
26 27 // function boxVolume calculates the volume of a box 28 int boxVolume(int length, int width, int height) { 29 return length * width * height; 30}
The default box volume is: 1
The volume of a box with length 10, width 1 and height 1 is: 10
The volume of a box with length 10, width 5 and height 1 is: 50
The volume of a box with length 10, width 5 and height 2 is: 100
The first call to boxVolume
(line 11) specifies no arguments, thus using all three default values of 1. The second call (line 15) passes only a length
argument, thus using default values of 1 for the width
and height
arguments. The third call (line 19) passes arguments for only length
and width
, thus using a default value of 1 for the height
argument. The last call (line 23) passes arguments for length
, width
and height
, thus using no default values. Any arguments passed to the function explicitly are assigned to the function’s parameters from left to right. Therefore, when boxVolume
receives one argument, the function assigns the value of that argument to its length
parameter (i.e., the leftmost parameter in the parameter list). When boxVolume
receives two arguments, the function assigns the values of those arguments to its length
and width
parameters in that order. Finally, when boxVolume
receives all three arguments, the function assigns the values of those arguments to its length
, width
and height
parameters, respectively.
Default arguments must be the rightmost (trailing) arguments in a function’s parameter list. When calling a function with two or more default arguments, if an omitted argument is not the rightmost argument, then all arguments to the right of that argument also must be omitted. Default arguments must be specified with the first occurrence of the function name—typically, in the function prototype. If the function prototype is omitted because the function definition also serves as the prototype, then the default arguments should be specified in the function header. Default values can be any expression, including constants, global variables or function calls. Default arguments also can be used with inline
functions. Using default arguments can simplify writing function calls, but some programmers feel that explicitly specifying all arguments is clearer.
C++ provides the unary scope resolution operator (::
) to access a global variable when a local variable of the same name is in scope. The unary scope resolution operator cannot be used to access a local variable of the same name in an outer block. A global variable can be accessed directly without the unary scope resolution operator if the name of the global variable is not the same as that of a local variable in scope.
Figure 5.11 shows the unary scope resolution operator with local and global variables of the same name (lines 6 and 9). To emphasize that the local and global versions of variable number
are distinct, the program declares one variable int
and the other double
.
1 // fig05_11.cpp
2 // Unary scope resolution operator.
3 #include <iostream>
4 using namespace std;
5
6 int number{7}; // global variable named number
7
8 int main() {
9 double number{10.5}; // local variable named number
10
11 // display values of local and global variables
12 cout << "Local double value of number = " << number
13 << "
Global int value of number = " << ::number << endl;
14 }
Local double value of number = 10.5
Global int value of number = 7
Always use the unary scope resolution operator (::
) to refer to global variables (even if there is no collision with a local-variable name). This makes it clear that you’re accessing a global variable rather than a local variable. It also makes programs easier to modify by reducing the risk of name collisions with nonglobal variables and eliminates logic errors that might occur if a local variable hides the global variable. Avoid using variables of the same name for different purposes in a program. Although this is allowed in various circumstances, it can lead to errors.
C++ enables several functions of the same name to be defined, as long as they have different signatures. This is called function overloading. The C++ compiler selects the proper function to call by examining the number, types and order of the arguments in the call. Function overloading is used to create several functions of the same name that perform similar tasks but on data of different types. For example, many functions in the math library are overloaded for different numeric types—the C++ standard requires float
, double
and long double
overloaded versions of the math library functions in Section 5.3. Overloading functions that perform closely related tasks can make programs clearer.
square
FunctionsFigure 5.12 uses overloaded square
functions to calculate the square of an int
(lines 7–10) and the square of a double
(lines 13–16). Line 19 invokes the int
version of function square
by passing the literal value 7
. C++ treats whole-number literal values as type int
. Similarly, line 21 invokes the double
version of function square
by passing the literal value 7.5
, which C++ treats as a double
. In each case, the compiler chooses the proper function to call, based on the type of the argument. The output confirms that the proper function was called in each case.
1 // fig05_12.cpp 2 // Overloaded square functions. 3 #include <iostream> 4 using namespace std; 5 6 // function square for int values 7 int square(int x) { 8 cout << "square of integer " << x << " is "; 9 return x * x; 10}
11 12 // function square for double values 13 double square(double y) { 14 cout << "square of double " << y << " is "; 15 return y * y; 16}
17 18 int main() { 19 cout << square(7); // calls int version 20cout << endl;
21 cout << square(7.5); // calls double version 22cout << endl;
23}
square of integer 7 is 49
square of double 7.5 is 56.25
Overloaded functions are distinguished by their signatures. A signature is a combination of a function’s name and its parameter types (in order). Type-safe linkage ensures that the proper function is called and that the types of the arguments conform to the types of the parameters. To enable type-safe linkage, the compiler internally encodes each function identifier with the types of its parameters—a process referred to as name mangling. These encodings vary by compiler, so everything that will be linked to create an executable for a given platform must be compiled using the same compiler for that platform. Figure 5.13 was compiled with GNU C++.17 Rather than showing the execution output of the program as we normally would, we show the mangled function names produced in assembly language by GNU C++. 18
17. The empty-bodied main
function ensures that we do not get a linker error if we compile this code.
18. The command g++ -S fig05_13.cpp
produces the assembly language file fig05_14.s
.
1 // fig05_13.cpp 2 // Name mangling to enable type-safe linkage. 3 4 // function square for int values 5 int square(int x) { 6 return x * x; 7}
8 9 // function square for double values 10 double square(double y) { 11 return y * y; 12}
13 14 // function that receives arguments of types 15 // int, float, char and int& 16 void nothing1(int a, float b, char c, int& d) { } 17 18 // function that receives arguments of types 19 // char, int, float& and double& 20 int nothing2(char a, int b, float& c, double& d) { 21 return 0; 22}
23 24 int main() { }
_Z6squarei
_Z6squared
_Z8nothing1ifcRi
_Z8nothing2ciRfRd
main
For GNU C++, each mangled name (other than main
) begins with an underscore (_
) followed by the letter Z
, a number and the function name. The number that follows Z
specifies how many characters are in the function’s name. For example, function square
has 6 characters in its name, so its mangled name is prefixed with _Z6
. Following the function name is an encoding of its parameter list:
• For function square
that receives an int
(line 5), i
represents int
, as shown in the output’s first line.
• For function square
that receives a double
(line 10), d
represents double
, as shown in the output’s second line.
• For function nothing1
(line 16), i
represents an int
, f
represents a float
, c
represents a char
, and Ri
represents an int&
(i.e., a reference to an int
), as shown in the output’s third line.
• For function nothing2
(line 20), c
represents a char
, i
represents an int
, Rf
rep-resents a float&
, and Rd
represents a double&
.
The compiler distinguishes the two square
functions by their parameter lists—one specifies i
for int
and the other d
for double
. The return types of the functions are not specified in the mangled names. Overloaded functions can have different return types, but if they do, they must also have different parameter lists. Function-name mangling is compiler-specific. For example, Visual C++ produces the name square@@YAHH@Z
for the square function at line 5. The GNU C++ compiler did not mangle main
’s name, but some compilers do. For example, Visual C++ uses _main
.
Creating overloaded functions with identical parameter lists and different return types is a compilation error. The compiler uses only the parameter lists to distinguish between overloaded functions. Such functions need not have the same number of parameters.
A function with default arguments omitted might be called identically to another overloaded function; this is a compilation error. For example, having a program that contains both a function that explicitly takes no arguments and a function of the same name that contains all default arguments results in a compilation error when an attempt is made to use that function name in a call passing no arguments. The compiler cannot unambiguously determine which version of the function to choose.
In Chapter 14, we discuss how to overload operators to define how they should operate on objects of user-defined data types. (In fact, we’ve been using many overloaded operators to this point, including the stream insertion <<
and the stream extraction >>
operators. These are overloaded for all the fundamental types. We say more about overloading <<
and >>
to be able to handle objects of user-defined types in Chapter 14.)
Overloaded functions are normally used to perform similar operations that involve different program logic on different data types. If the program logic and operations are identical for each data type, overloading may be performed more compactly and conveniently by using function templates. You write a single function template definition. Given the argument types provided in calls to this function, C++ automatically generates separate function template specializations to handle each type of call appropriately. Thus, defining a single function template essentially defines a whole family of overloaded functions.
maximum
Function TemplateFigure 5.14 defines a maximum
function template that determines the largest of three values. All function template definitions begin with the template
keyword (line 3) followed by a template parameter list enclosed in angle brackets (<
and >
). Every parameter in the template parameter list is preceded by keyword typename
or keyword class
(they are synonyms in this context). The type parameters are placeholders for fundamental types or user-defined types. These placeholders—in this case, T
—are used to specify the types of the function’s parameters (line 4), to specify the function’s return type (line 4) and to declare variables within the body of the function definition (line 5). A function template is defined like any other function but uses the type parameters as placeholders for actual data types.
1 // Fig. 5.14: maximum.h 2 // Function template maximum header. 3 template <typename T> // or template<class T> 4T maximum(T value1, T value2, T value3) {
5 T maximumValue{value1}; // assume value1 is maximum 6 7 // determine whether value2 is greater than maximumValue 8 if (value2 > maximumValue) { 9maximumValue = value2;
10}
11 12 // determine whether value3 is greater than maximumValue 13 if (value3 > maximumValue) { 14maximumValue = value3;
15}
16 17 return maximumValue; 18}
This function template declares a single type parameter T
(line 3) as a placeholder for the type of the data to be tested by function maximum
. The name of a type parameter must be unique in the template parameter list for a particular template definition. When the compiler encounters a call to maximum
in the program source code, the compiler substitutes the argument types in the maximum
call for T
throughout the template definition, creating a complete function template specialization that determines the maximum of three values of the specified type. The values must have the same type, since we use only one type parameter in this example. Then the newly created function is compiled—templates are a means of code generation. We’ll use C++ Standard Library templates that require multiple type parameters in Chapter 16.
maximum
Figure 5.15 uses the maximum
function template to determine the largest of three int
values, three double
values and three char
values, respectively (lines 15, 24 and 33). Because each call uses arguments of a different type, “behind the scenes” the compiler creates a separate function definition for each—one expecting three int
values, one expecting three double
values and one expecting three char
values, respectively.
1 // fig05_15.cpp 2 // Function template maximum test program. 3 #include <iostream> 4 #include "maximum.h" // include definition of function template maximum 5 using namespace std; 6 7 int main() { 8 // demonstrate maximum with int values 9 cout << "Input three integer values: "; 10 int int1, int2, int3; 11cin >> int1 >> int2 >> int3;
12 13 // invoke int version of maximum 14 cout << "The maximum integer value is: " 15<< maximum(int1, int2, int3);
16 17 // demonstrate maximum with double values 18 cout << " Input three double values: "; 19 double double1, double2, double3; 20cin >> double1 >> double2 >> double3;
21 22 // invoke double version of maximum 23 cout << "The maximum double value is: " 24<< maximum(double1, double2, double3);
25 26 // demonstrate maximum with char values 27 cout << " Input three characters: "; 28 char char1, char2, char3; 29cin >> char1 >> char2 >> char3;
30 31 // invoke char version of maximum 32 cout << "The maximum character value is: " 33<< maximum(char1, char2, char3) << endl;
34 }
Input three integer values:
1 2 3
The maximum integer value is: 3
Input three double values:
3.3 2.2 1.1
The maximum double value is: 3.3
Input three characters:
A C B
The maximum character value is: C
maximum
Function Template Specialization for Type int
The function template specialization created for type int
replaces each occurrence of T
with int
as follows:
int maximum(int value1, int value2, int value3) { int maximumValue{value1}; // assume value1 is maximum // determine whether value2 is greater than maximumValue if (value2 > maximumValue) { maximumValue = value2; } // determine whether value3 is greater than maximumValue if (value3 > maximumValue) { maximumValue = value3; } return maximumValue; }
For some problems, it’s useful to have functions call themselves. A recursive function is a function that calls itself, either directly or indirectly (through another function). This section and the next present simple examples of recursion. Recursion is discussed at length in upper-level computer science courses.
We first consider recursion conceptually, then examine programs containing recursive functions. Recursive problem-solving approaches have several elements in common. A recursive function is called to solve a problem. The function knows how to solve only the simplest case(s), or so-called base case(s). If the function is called with a base case, the function simply returns a result. If the function is called with a more complex problem, it typically divides the problem into two conceptual pieces—a piece that the function knows how to do and a piece that it does not know how to do. To make recursion feasible, the latter piece must resemble the original problem, but be a slightly simpler or smaller version. This new problem looks like the original, so the function calls a copy of itself to work on the smaller problem—this is referred to as a recursive call and is also called the recursion step. The recursion step often includes the keyword return
, because its result will be combined with the portion of the problem the function knew how to solve to form the result passed back to the original caller, possibly main
.
Omitting the base case or writing the recursion step incorrectly so that it does not converge on the base case causes an infinite recursion error, typically causing a stack overflow. This is analogous to the problem of an infinite loop in an iterative (nonrecursive) solution. Accidentally having a nonrecursive function call itself, either directly or indirectly through another function, also causes an infinite recursion.
The recursion step executes while the original call to the function is still “open,” i.e., it has not yet finished executing. The recursion step can result in many more such recursive calls, as the function keeps dividing each new subproblem with which the function is called into two conceptual pieces. For the recursion to eventually terminate, each time the function calls itself with a slightly simpler version of the original problem, this sequence of smaller and smaller problems must eventually converge on the base case. At that point, the function recognizes the base case and returns a result to the previous copy of the function. Then, a sequence of returns ensues up the line until the original call eventually returns the final result to main
.19 This sounds quite exotic compared to the kind of problem-solving we’ve been using to this point. As an example of these concepts at work, let’s write a recursive program to perform a popular mathematical calculation.
19. The C++ standard document indicates that main
should not be called within a program (Section 6.9.3.1) or recursively (Section 7.6.1.2). Its sole purpose is to be the starting point for program execution.
The factorial of a non-negative integer n, written n! (pronounced “n factorial”), is the product
n ·
(n – 1) ·
(n – 2) · … · 1
with 1! equal to 1, and 0! defined to be 1. For example, 5! is the product 5 · 4 · 3 · 2 · 1, which is equal to 120.
The factorial of an integer, number
, greater than or equal to 0, can be calculated iteratively (nonrecursively) by using a for
statement as follows:
int factorial{1}; for (int counter{number}; counter >= 1; --counter) { factorial *= counter; }
A recursive definition of the factorial function is arrived at by observing the following algebraic relationship:
n! = n · (n – 1)!
For example, 5! is clearly equal to 5 * 4! as is shown by the following:
5! = 5 · 4 · 3 · 2 · 1
5! = 5 · (4 · 3 · 2 · 1)
5! = 5 · (4!)
The evaluation of 5! would proceed as shown in the following diagram, which illustrates how the succession of recursive calls proceeds until 1! is evaluated to be 1, terminating the recursion. Part (b) of the diagram shows the values returned from each recursive call to its caller until the final value is calculated and returned.
factorial
Function to Calculate FactorialsFigure 5.16 uses recursion to calculate and print the factorials of the integers 0–10. The recursive function factorial
(lines 18–25) first determines whether the terminating condition number <= 1
(i.e., the base case; line 19) is true. If number
is less than or equal to 1, the factorial
function returns 1 (line 20), no further recursion is necessary and the function terminates. If number
is greater than 1, line 23 expresses the problem as the product of number
and a recursive call to factorial
evaluating the factorial of number - 1
, which is a slightly simpler problem than the original calculation factorial(number)
.
1 // fig05_16.cpp 2 // Recursive function factorial. 3 #include <iostream> 4 #include <iomanip> 5 using namespace std; 6 7 long factorial(long number); // function prototype 8 9 int main() { 10 // calculate the factorials of 0 through 10 11 for (int counter{0}; counter <= 10; ++counter) { 12 cout << setw(2) << counter << "! = " << factorial(counter) 13<< endl;
14}
15}
16 17 // recursive definition of function factorial 18 long factorial(long number) { 19 if (number <= 1) { // test for base case 20 return 1; // base cases: 0! = 1 and 1! = 1 21}
22 else { // recursion step 23 return number * factorial(number - 1); 24}
25}
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
10! = 3628800
Function factorial
receives a parameter of type long
and returns a result of type long
. Typically, a long
is stored in at least four bytes (32 bits); such a variable can hold a value in the range –2,147,483,647 to 2,147,483,647. Unfortunately, the function factorial
produces large values so quickly that type long
does not help us compute many factorial values before reaching the maximum value of a long
. For larger integer values, we could use type long long
(Section 3.11) or a class that represents arbitrary sized integers (such as the open-source BigNumber
class we introduced in Section 3.12).
The Fibonacci series
0, 1, 1, 2, 3, 5, 8, 13, 21, …
begins with 0 and 1 and has the property that each subsequent Fibonacci number is the sum of the previous two Fibonacci numbers.
The series occurs in nature and, in particular, describes a form of a spiral. The ratio of successive Fibonacci numbers converges on a constant value of 1.618…. This number frequently occurs in nature and has been called the golden ratio or the golden mean. Humans tend to find the golden mean aesthetically pleasing. Architects often design windows, rooms and buildings whose length and width are in the ratio of the golden mean. Postcards are often designed with a golden mean length/width ratio. A web search for “Fibonacci in nature” reveals many interesting examples, including flower petals, shells, spiral galaxies, hurricanes and more.
The Fibonacci series can be defined recursively as follows:
fibonacci(0) = 0
fibonacci(1) = 1
fibonacci(n) = fibonacci(n – 1) + fibonacci(n – 2)
The program of Fig. 5.17 calculates the nth Fibonacci number recursively by using function fibonacci
. Fibonacci numbers tend to become large quickly, although slower than factorials do. Figure 5.17 shows the execution of the program, which displays the Fibonacci values for several numbers.
1 // fig05_17.cpp 2 // Recursive function fibonacci. 3 #include <iostream> 4 using namespace std; 5 6 long fibonacci(long number); // function prototype 7 8 int main() { 9 // calculate the fibonacci values of 0 through 10 10 for (int counter{0}; counter <= 10; ++counter) 11 cout << "fibonacci(" << counter << ") = " 12<< fibonacci(counter) << endl;
13 14 // display higher fibonacci values 15 cout << " fibonacci(20) = " << fibonacci(20) << endl; 16 cout << "fibonacci(30) = " << fibonacci(30) << endl; 17 cout << "fibonacci(35) = " << fibonacci(35) << endl; 18}
19 20 // recursive function fibonacci 21 long fibonacci(long number) { 22 if ((0 == number) || (1 == number)) { // base cases 23 return number; 24}
25 else { // recursion step 26 return fibonacci(number - 1) + fibonacci(number - 2); 27}
28}
fibonacci(0) = 0 fibonacci(1) = 1 fibonacci(2) = 1 fibonacci(3) = 2 fibonacci(4) = 3 fibonacci(5) = 5 fibonacci(6) = 8 fibonacci(7) = 13 fibonacci(8) = 21 fibonacci(9) = 34 fibonacci(10) = 55
fibonacci(20) = 6765 fibonacci(30) = 832040 fibonacci(35) = 9227465
The application begins with a loop that calculates and displays the Fibonacci values for the integers 0–10 and is followed by three calls to calculate the Fibonacci values of the integers 20, 30 and 35 (lines 15–17). The calls to fibonacci
in main
(lines 12 and 15–17) are not recursive calls, but the calls from line 26 of fibonacci
are recursive. Each time the program invokes fibonacci
(lines 21–28), the function immediately tests the base case to determine whether number
is equal to 0
or 1
(line 22). If this is true, line 23 returns number
. Interestingly, if number
is greater than 1, the recursion step (line 26) generates two recursive calls, each for a slightly smaller problem than the original call to fibonacci
.
fibonacci(3)
The following diagram shows how function fibonacci
would evaluate fibonacci(3)
. This figure raises some interesting issues about the order in which C++ compilers evaluate the operands of operators. This is a separate issue from the order in which operators are applied to their operands, namely, the order dictated by the rules of operator precedence and grouping. The following diagram shows that evaluating fibonacci(3)
causes two recursive calls, namely, fibonacci(2)
and fibonacci(1)
. In what order are these calls made?
Most programmers simply assume that the operands are evaluated left to right, which is the case in some programming languages. C++ does not specify the order in which the operands of many operators (including +
) are to be evaluated. Therefore, you must make no assumption about the order in which these calls execute. The calls could, in fact, execute fibonacci(2)
first, then fibonacci(1)
, or fibonacci(1)
first, then fibonacci(2)
. In this program and in most others, it turns out that the final result would be the same. However, in some programs, the evaluation of an operand can have side effects (changes to data values) that could affect the final result of the expression.
Before C++17, C++ specified the order of evaluation of the operands of only the operators &&
, ||
, comma (,
) and ?:
. The first three are binary operators whose two operands are guaranteed to be evaluated left to right. The last operator is C++’s only ternary operator— its leftmost operand is always evaluated first; if it evaluates to true, the middle operand evaluates next and the last operand is ignored; if the leftmost operand evaluates to false, the third operand evaluates next and the middle operand is ignored.
17 As of C++17, C++ now also specifies the order of evaluation of the operands for various other operators. For the operators dot (.
), []
(Chapter 6), ->
(Chapter 7), parentheses (of a function call), <<
, >>
and ->*
, the compiler evaluates the operands left-to-right. For a function call’s parentheses, this means that the compiler evaluates the function name before the arguments. The compiler evaluates the operands of assignment operators right-to-left.
Writing programs that depend on the order of evaluation of the operands of other operators can lead to logic errors. For other operators, to ensure that side effects are applied in the correct order, break complex expressions into separate statements. Recall that the &&
and ||
operators use short-circuit evaluation. Placing an expression with a side effect on the right side of the &&
or ||
operator is a logic error if that expression should always be evaluated.
A word of caution is in order about recursive programs like the one we use here to generate Fibonacci numbers. Each level of recursion in function fibonacci
has a doubling effect on the number of function calls; i.e., the number of recursive calls that are required to calculate the nth Fibonacci number is on the order of 2n. This rapidly gets out of hand. Calculating only the 20th Fibonacci number would require on the order of 220 or about a million calls, calculating the 30th Fibonacci number would require on the order of 230 or about a billion calls, and so on. Computer scientists refer to this as exponential complexity. Problems of this nature can humble even the world’s most powerful computers as n becomes large. Complexity issues in general, and exponential complexity in particular, are discussed in detail in the upper-level computer science course typically called Algorithms. Avoid Fibonacci-style recursive programs that result in an exponential “explosion” of calls.
In the two prior sections, we studied two recursive functions that can also be implemented with simple iterative programs. This section compares the two approaches and discusses why you might choose one approach over the other in a particular situation.
• Both iteration and recursion are based on a control statement: Iteration uses an iteration statement; recursion uses a selection statement.
• Both iteration and recursion involve iteration: Iteration explicitly uses an iteration statement; recursion achieves iteration through repeated function calls.
• Iteration and recursion each involve a termination test: Iteration terminates when the loop-continuation condition fails; recursion terminates when a base case is recognized.
• Counter-controlled iteration and recursion each gradually approach termination: Iteration modifies a counter until the counter assumes a value that makes the loop-continuation condition fail; recursion produces simpler versions of the original problem until the base case is reached.
• Both iteration and recursion can occur infinitely: An infinite loop occurs with iteration if the loop-continuation test never becomes false; infinite recursion occurs if the recursion step does not reduce the problem during each recursive call in a manner that converges on the base case.
To illustrate the differences between iteration and recursion, let’s examine an iterative solution to the factorial problem (Fig. 5.18). Lines 22–24 use an iteration statement rather than the selection statement of the recursive solution (lines 19–24 of Fig. 5.16). Both solutions use a termination test. In the recursive solution, line 19 (Fig. 5.16) tests for the base case. In the iterative solution, line 22 (Fig. 5.18) tests the loop-continuation condition— if the test fails, the loop terminates. Finally, instead of producing simpler versions of the original problem, the iterative solution uses a counter that’s modified until the loop-continuation condition becomes false.
1 // fig05_18.cpp 2 // Iterative function factorial. 3 #include <iostream> 4 #include <iomanip> 5 using namespace std; 6 7 unsigned long factorial(int number); // function prototype 8 9 int main() { 10 // calculate the factorials of 0 through 10 11 for (int counter{0}; counter <= 10; ++counter) { 12 cout << setw(2) << counter << "! = " << factorial(counter) 13<< endl;
14 } 15 } 16 17 // iterative function factorial 18 unsigned long factorial(int number) { 19 unsigned long result{1}; 20 21 // iterative factorial calculation 22 for (int i{number}; i >= 1; --i) { 23result *= i;
24}
25 26 return result; 27}
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
10! = 3628800
Recursion has negatives. It repeatedly invokes the mechanism, and consequently the overhead, of function calls. This can be expensive in both processor time and memory space. Each recursive call causes another copy of the function variables to be created; this can consume considerable memory. Iteration normally occurs within a function, so the overhead of repeated function calls and extra memory assignment is omitted. So why choose recursion?
PERF Any problem that can be solved recursively can also be solved iteratively (nonrecursively). A recursive approach is normally chosen when the recursive approach more naturally mirrors the problem and results in a program that’s easier to understand and debug. Another reason to choose a recursive solution is that an iterative solution may not be apparent when a recursive solution is. If possible, avoid using recursion in performance situations. Recursive calls take time and consume additional memory.
[[nodiscard]]
AttributeSome functions return values that you should not ignore. For example, Section 2.7 introduced the string
member function empty
. When you want to know whether a string
is empty you must not only call empty
, but also check its return value in a condition, such as:
if (s.empty()) { // do something because the string s is empty }
As of C++17, string
’s empty
function is declared with the [[nodiscard]]
attribute20 to tell the compiler to issue a warning when the return value is not used by the caller. Since C++17, many C++ Standard Library functions have been enhanced with [[nodiscard]]
so the compiler can help you write correct code.
20. Section 9.12.8 of the ISO/IEC C++20 standard document. https://wg21.link/n4849
.
You may also use this attribute on your own function definitions. Figure 5.19 shows a cube
function declared with [[nodiscard]]
, which you place before the return type— typically on a line by itself (line 4).
1 // fig05_19.cpp 2 // C++17 [[nodiscard]] attribute. 3 4[[nodiscard]]
5 int cube(int x) { 6 return x * x * x; 7}
8 9 int main() { 10 cube(10); // generates a compiler warning 11}
Line 10 calls cube
, but does not use the returned value. When you compile this program our preferred compilers, they issue the following warnings:
• Microsoft Visual C++: "discarding return value of function with 'nodis-card' attribute"
• Clang in Xcode: "Ignoring return value of function declared with 'nodis-card' attribute
• GNU C++: "ignoring return value of 'int cube(int)', declared with attribute nodiscard"
However, these are just warnings, so the program still compiles and runs.
[[nodiscard("with reason")]]
AttributeOne problem with C++17’s [[nodiscard]]
attribute is that it did not provide any insight into why you should not ignore a given function’s return value. So, in C++20, you can now include a message21 that will be displayed as part of the compiler warning, as in:
[[nodiscard("Insight into why return value should not be ignored")]]
21. At the time of this writing, this feature is not yet implemented.
No doubt, you’ve noticed that the last Objectives bullet for this chapter, the last section name in the chapter outline, the last sentence in Section 5.1 and the section title above all look like gibberish. These are not mistakes! In this section, we continue our objects-natural presentation. You’ll conveniently encrypt and decrypt messages with an object you create of a preexisting class that
implements a Vignère secret key cipher.22
22. https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher
.
In prior objects-natural sections, you created objects of built-in C++ standard library class string
and objects of classes from open-source libraries. Sometimes you’ll use classes built by your organization or team members for internal use or for use in a specific project. For this example, we wrote our own class called Cipher
(in the header "cipher.h"
) and provided it to you. In Chapter 10, Introduction to Classes, you’ll start building your own custom classes.
Security Cryptography has been in use for thousands of years23,24 and is critically important in today’s connected world. Every day, cryptography is used behind the scenes to ensure that your Internet-based communications are private and secure. For example, most websites now use the HTTPS protocol to encrypt and decrypt your web interactions.
23. https://en.wikipedia.org/wiki/Cryptography#History_of_cryptography_and_cryptanalysis
.
24. https://www.binance.vision/security/history-of-cryptography
.
Julius Caesar used a simple substitution cipher to encrypt military communications.25 His technique—which became known as the Caesar cipher—replaces every letter in a communication with the letter three ahead in the alphabet. So, A is replaced with D, B with E, C with F, … X with A, Y with B and Z with C. Thus, the plain text
Caesar Cipher
25. https://en.wikipedia.org/wiki/Caesar_cipher
.
would be encrypted as
Fdhvdu Flskhu
The encrypted text is known as the ciphertext.
For a fun way to play with the Caesar cipher and many other cipher techniques, check out the website:
https://cryptii.com/pipes/caesar-cipher
which is an online implementation of the open-source cryptii
project:
https://github.com/cryptii/cryptii
Simple substitution ciphers like the Caesar cipher are relatively easy to decrypt. For example, the letter “e” is the most frequently used letter in English. So, you could study ciphertext and assume with a high likelihood that the character appearing most frequently is probably an “e.”
In this example, you’ll use a Vignère cipher, which is a secret-key substitution cipher. A Vignère cipher is implemented using 26 Caesar ciphers—one for each letter of the alphabet. A Vignère cipher uses letters from the plain text and secret key to look up replacement characters in the various Caesar ciphers. You can read more about the implementation at
https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher
For this cipher, the secret key must be composed of letters. Like passwords, the secret key should not be something that’s easy to guess. In this example, we used the 11 randomly selected characters
XMWUJBVYHXZ
There’s no limit to the number of characters you can use in your secret key. However, the person decrypting the ciphertext must know the secret key that was originally used to create the ciphertext.26 Presumably, you’d provide that in advance—possibly in a face-to-face meeting.
26. There are many websites offering Vignère cipher decoders that attempt to decrypt ciphertext without the original secret key. We tried several, but none restored our original text.
Cipher
ClassFor the example in Fig. 5.20, you’ll use our class Cipher
, which implements the Vigenère cipher. The header "cipher.h"
(line 3) from this chapter’s ch05
examples folder defines the class. You don’t need to read and understand the class’s code to use its encryption and decryption capabilities. As with all our other “Objects Natural” case studies, you can simply create an object of class Cipher
and call its encrypt
and decrypt
member functions to encrypt and decrypt text, respectively. In a later chapter, we’ll present class Cipher
’s implementation.
1 // fig15_20.cpp 2 // Encrypting and decrypting text with a Vigenère cipher. 3 #include "cipher.h" 4 #include <iostream> 5 #include <string> 6 using namespace std; 7 8 int main() { 9string plainText;
10 cout << "Enter the text to encrypt: "; 11getline(cin, plainText);
12 13string secretKey;
14 cout << " Enter the secret key: "; 15getline(cin, secretKey);
16 17Cipher cipher;
18 19 // encrypt plainText using secretKey 20string cipherText{cipher.encrypt(plainText, secretKey)};
21 cout << " Encrypted: " << cipherText << endl; 22 23 // decrypt cipherText 24 cout << " Decrypted: " 25<< cipher.decrypt(cipherText, secretKey) << endl;
26 27 // decrypt ciphertext entered by the user 28 cout << " Enter the ciphertext to decipher: "; 29getline(cin, cipherText);
30 cout << " Decrypted: " 31<< cipher.decrypt(cipherText, secretKey) << endl;
32}
Enter the text to encrypt:
Welcome to Modern C++ application development with C++20!
Enter the secret key:
XMWUJBVYHXZ
Encrypted:
Tqhwxnz rv Jnaqnh L++ bknsfbxfeiw eztlinmyahc xdro Z++20!
Decrypted:
Welcome to Modern C++ application development with C++20!
Enter the ciphertext to decipher:
Lnfylun Lhqtomh Wjtz Qarcv: Qjwazkrplm xzz Xndmwwqhlz
Decrypted:
Objects Natural Case Study: Encryption and Decryption
Cipher
’s Member FunctionsThe class provides two key member functions:
• Member function encrypt
receives string
s representing the plain text to encrypt and the secret key, uses the Vigenère cipher to encrypt the text, then returns a string
containing the ciphertext.
• Member function decrypt
receives string
s representing the ciphertext to decrypt, reverses the Vigenère cipher to decrypt the text, then returns a string
containing the plain text.
The program first asks you to enter text to encrypt and a secret key. Line 17 creates the Cipher
object. Lines 20–21 encrypt the text you entered and display the encrypted text. Then, lines 24–25 decrypt the text to show you the plain-text string you entered.
Though the last Objectives bullet in this chapter, the last sentence of Section 5.1 and this section’s title look like gibberish, they’re each ciphertext that we created with our Cipher
class and the secret key
XMWUJBVYHXZ
Line 28–29 prompt for and input existing ciphertext, then lines 30–31 decrypt the ciphertext and display the original plain text that we encrypted.
In this chapter, we presented function concepts, including function prototypes, function signatures, function headers and function bodies. We overviewed the math library functions and new math functions and constants added in C++20, C++17 and C++11.
You learned about argument coercion—forcing arguments to the appropriate types specified by the parameter declarations of a function. We presented an overview of the C++ Standard Library’s headers. We demonstrated how to use functions rand
and srand
to generate random numbers that can be used for simulations, then presented C++11’s nondeterministic capabilities for producing more secure random numbers. We introduced C++14’s digit separators for more readable large numeric literals. We defined sets of constants with scoped enum
s and introduced C++20’s using enum
declaration.
You learned about the scope of variables. We discussed two ways to pass arguments to functions—pass-by-value and pass-by-reference. We showed how to implement inline functions and functions that receive default arguments. You learned that overloaded functions have the same name but different signatures. Such functions can be used to perform the same or similar tasks, using different types or different numbers of parameters. We demonstrated using function templates to conveniently generate families of overloaded functions. You then studied recursion, where a function calls itself to solve a problem.
We presented C++17’s [[nodiscard]]
attribute for indicating that a function’s return value should not be ignored and discussed C++20’s [[nodiscard]]
enhancement for specifying a reason why the return value should not be ignored. Finally, our objects-natural case study introduced secret key substitution ciphers for encrypting and decrypting text.
In Chapter 6, you’ll learn how to maintain lists and tables of data in arrays and object-oriented vector
s. You’ll see a more elegant array-based implementation of the dice-rolling application.