Chapter 5. Functions

Images

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 enums 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.

5.1 Introduction

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 enums 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.

“Rough-Cut” E-Book for O’Reilly Online Learning Subscribers

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.

“Sneak Peek” Videos for O’Reilly Online Learning Subscribers

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.

5.2 Program Components in C++

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 strings 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.

5.3 Math Library Functions

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.

Images
C++11 Additional Math Functions

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
20 C++20—New Mathematical Constants and the <numbers> Header

Though 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:

Images

1. We discuss the preprocessor and macros in Appendix E.

2. http://wg21.link/p0631r8.

17 C++17 Mathematical Special Functions

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

3. http://wg21.link/p0226r1.

Each mathematical special function in the following table has versions for float, double and long double arguments, respectively:

Images

5.4 Function Definitions and Function Prototypes

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;
12     cin >> 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

Fig. 5.1 maximum function with a function prototype.

Function 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.

Function Prototype for 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

Parameter names in function prototypes are optional (the compiler ignores them), but it’s recommended that you use these names for documentation purposes.

What the Compiler Does with maximum’s Function Prototype

When 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.

Returning Control from a Function to Its Caller

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.

5.5 Order of Evaluation of a Function’s Arguments

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.

5.6 Function-Prototype and Argument-Coercion Notes

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).

5.6.1 Function Signatures and Function Prototypes

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.

5.6.2 Argument Coercion

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.

5.6.3 Argument-Promotion Rules and Implicit Conversions4

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.”

Images
Conversions Can Result in Incorrect Values

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
Narrowing Conversions with the Guidelines Support Library

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, <>.

5.7 C++ Standard Library Headers

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.

Images
Images
Images
Images

5.8 Case Study: Random-Number Generation

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.

The rand Function

The 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.

5.8.1 Rolling a Six-Sided Die

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
13     cout << endl;
14  }
6 6 5 5 6 5 1 1 5 3

Fig. 5.2 Shifted, scaled integers produced by 1 + rand() % 6.

5.8.2 Rolling a Six-Sided Die 60,000,000 Times

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

Fig. 5.3 Rolling a six-sided die 60,000,000 times.

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.

14 C++14 Digit Separators for Numeric Literals

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.

5.8.3 Randomizing the Random-Number Generator with 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.

Using 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.

Images 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: ";
12     cin >> 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
21     cout << 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

Fig. 5.4 Randomizing the die-rolling program.

5.8.4 Seeding the Random-Number Generator with the Current Time

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.

5.8.5 Scaling and Shifting Random Numbers

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.

5.9 Case Study: Game of Chance; Introducing Scoped enums

One 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
Implementing the Game

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() {

Fig. 5.5 Craps simulation.

C++11: Scoped enums

The 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.

Winning or Losing on the First Roll

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.

Continuing to Roll

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.

Displaying Whether the Player Won or Lost

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
Function 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    }
Additional Notes Regarding Scoped enums

Qualifying 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.

Enumeration Types Before C++11

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 enums contain constants with the same names, this can lead to naming conflicts and compilation errors.

11 C++11—Specifying the Type of an enum’s Constants

An 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};
20 C++20—using enum Declaration

If 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;

11. http://wg21.link/p1099r5.

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.

11 5.10 C++11’s More Secure Nondeterministic Random Numbers

Images 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.

Rolling a Six-Sided Die

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
23     cout << endl;
24  }
2 1 2 3 5 6 1 5 6 4

Fig. 5.6 Using a C++11 random-number generation engine and distribution to roll a six-sided die more securely.

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.

5.11 Scope Rules

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.

Block Scope

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.

Global Namespace Scope

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.

Scope Demonstration

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

Fig. 5.7 Scoping example.

Function 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
Function 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
Function 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
Function 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   }

5.12 Inline Functions

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

Fig. 5.8 inline function that calculates the volume of a cube.

5.13 References and Reference Parameters

Images 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.

Reference Parameters

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).

Images 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.

Images 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.

Passing Arguments by Value and by Reference

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;
21     squareByReference(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

Fig. 5.9 Passing arguments by value and by reference.

References as Aliases within a Function

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 References

To 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

Images PERF Images 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.

Returning a Reference to a Local Variable

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.

5.14 Default Arguments

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 Arguments

Figure 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

Fig. 5.10 Using default arguments.

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.

Notes Regarding Default Arguments

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.

5.15 Unary Scope Resolution Operator

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

Fig. 5.11 Unary scope resolution operator.

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.

5.16 Function Overloading

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.

Overloaded square Functions

Figure 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
20     cout << endl;
21     cout << square(7.5); // calls double version
22     cout << endl;
23  }
square of integer 7 is 49
square of double 7.5 is 56.25

Fig. 5.12 Overloaded square functions.

How the Compiler Differentiates Among Overloaded Functions

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

Fig. 5.13 Name mangling to enable type-safe linkage.

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.

Overloaded Operators

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.)

5.17 Function Templates

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 Template

Figure 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>
 4  T 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) {
 9        maximumValue = value2;
10     }
11
12     // determine whether value3 is greater than maximumValue
13     if (value3 > maximumValue) {
14        maximumValue = value3;
15     }
16
17     return maximumValue;
18  }

Fig. 5.14 Function template maximum header.

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.

Using Function Template 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;
11     cin >> 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;
20     cin >> 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;
29     cin >> 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

Fig. 5.15 Function template maximum test program.

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;
}

5.18 Recursion

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.

Recursion Concepts

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.

Factorial

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.

Iterative Factorial

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;
}
Recursive Factorial

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!)

Evaluating 5!

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.

Images
Using a Recursive factorial Function to Calculate Factorials

Figure 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

Fig. 5.16 Recursive function factorial.

Factorial Values Grow Quickly

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).

5.19 Example Using Recursion: Fibonacci Series

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.

Recursive Fibonacci Definition

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

Fig. 5.17 Recursive function fibonacci.

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.

Evaluating 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?

Images
Order of Evaluation of Operands

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.

Operators for which Order Of Evaluation Is Specified

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.

Exponential Complexity

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.

5.20 Recursion vs. Iteration

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.

Iterative Factorial Implementation

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) {
23        result *= 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

Fig. 5.18 Iterative function factorial.

Negatives of Recursion

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?

When to Choose Recursion vs. Iteration

Images 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.

17 5.21 C++17 and C++20: [[nodiscard]] Attribute

Some 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  }

Fig. 5.19 C++17 [[nodiscard]] attribute.

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.

20 C++20’s [[nodiscard("with reason")]] Attribute

One 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.

5.22 Lnfylun Lhqtomh Wjtz Qarcv: Qjwazkrplm xzz Xndmwwqhlz

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.

Cryptography

Images 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.

Caesar Cipher

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
Vignère Cipher

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.

Using Our Cipher Class

For 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() {
 9     string plainText;
10     cout << "Enter the text to encrypt:
";
11     getline(cin, plainText);
12
13     string secretKey;
14     cout << "
Enter the secret key:
";
15     getline(cin, secretKey);
16
17     Cipher cipher;
18
19     // encrypt plainText using secretKey
20     string 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:
";
29     getline(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

Fig. 5.20 Encrypting and decrypting text with a Vigenère cipher.

Class Cipher’s Member Functions

The class provides two key member functions:

• Member function encrypt receives strings 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 strings 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.

5.23 Wrap-Up

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 enums 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 vectors. You’ll see a more elegant array-based implementation of the dice-rolling application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset