
iii■ Contents at a Glance
iv ■ Contents at a Glance
v■ Contents at a Glance
Index���������������������������������������������������������������������������������������������������������������������������������613
viIntroduction
Hi, there. Thank you for reading my book Exploring C++ 11. My name is Ray, and I’ll be your author today. Andtomorrow. And the day after that. We’ll be together for quite a while, so why don’t you pull up a chair and getcomfortable. My job is to help you learn C++. To do that, I have written a series of lessons called Explorations.Each Exploration is an interactive exercise that helps you learn C++ one step at a time. Your job is to complete theExplorations and, in so doing, learn C++. No doubt you have already leafed through the book a little bit. If not, do so now. Notice that this book is differentfrom most books. Most programming books are little more than written lectures. The author tells you stuff and expectsyou to read the stuff, learn it, and understand it. This book is different. I don’t see much point in lecturing at you. That’s not how people learn best. You learnprogramming by reading, modifying, and writing programs. To that end, I’ve organized this book so that you spend asmuch time as possible reading, modifying, and writing programs.
xxix■ Introduction
ProjectsThis book also contains four projects. The projects are opportunities to apply what you have learned. Each project is arealistic endeavor, based on the amount of C++ covered up to that point. I encourage you to try every project. Designyour project using your favorite software design techniques. Remember to write test cases in addition to the sourcecode. Do your best to make the code clean and readable, in addition to correct. After you are confident that yoursolution is finished, download the files from the book’s web site, and compare your solution with mine.
Work TogetherYou can use this book alone, teaching yourself C++, or a teacher might adopt this book as a textbook for a formalcourse. You can also work with a partner. It’s more fun to work with friends, and you’ll learn more and faster by workingtogether. Each of you needs your own copy of the book. Read the lessons and do the work on your own. If you havequestions, discuss them with your partner, but answer the exercises on your own. Then compare answers with yourpartner. If your answers are different, discuss your reasoning. See if you can agree on a single answer before proceeding.
xxx ■ Introduction
Work on the projects together. Maybe you can divide the work into two (or more) modules. Maybe one personcodes and the other person checks. Maybe you’ll practice some form of pair programming. Do whatever works bestfor you, but make sure you understand every line of code in the project. If you have asymmetric roles, be sure to swaproles for each project. Give everyone a chance to do everything.
Why Explorations?In case you were wondering about the unusual nature of this book, rest assured that, “though this be madness,yet there is method in’t.” The method is an approach to teaching and writing that I developed while I was teaching computer science atOregon State University. I wanted to improve the quality of my teaching, so I investigated research into learning andknowledge, especially scientific knowledge, and in particular, computer programming. To summarize several decades of research: everyone constructs mental models of the world. We acquire knowledgeby adding information to our models. The new information must always be in concert with the model. Sometimes,however, new information contradicts the model. In that case, we must adjust our models to accommodate the newinformation. Our brains are always at work, always taking in new information, always adjusting our mental models to fit. As a result of this research, the emphasis in the classroom has shifted from teachers to students. In the past,teachers considered students to be empty vessels, waiting to be filled from the fount of the teacher’s knowledge andwisdom. Students were passive recipients of information. Now we know better. Students are not passive, but active.Even when their outward appearance suggests otherwise, their brains are always at work, always absorbing newinformation and fitting that information into their mental models. The teacher’s responsibility has changed from beingthe source of all wisdom to being an indirect manager of mental models. The teacher cannot manage those modelsdirectly but can only create classroom situations in which students have the opportunity to adjust their own models. Although the research has focused on teachers, the same applies to authors. In other words, I cannot teach youC++, but I can create Explorations that enable you to learn C++. Explorations are not the only way to apply researchto learning and writing, but they are a technique that I have refined over several years of teaching and have foundsuccessful. Explorations work because • They force you to participate actively in the learning process. It’s too easy to read a book passively. The questions force you to confront new ideas and to fit them into your mental model. If you skip the questions, you might also skip a crucial addition to your model. • They are small, so your model grows in easy steps. If you try to grasp too much new information at once, you are likely to incorporate incorrect information into your model. The longer that misinformation festers, the harder it will be to correct. I want to make sure your model is as accurate as possible at all times. • They build on what you know. I don’t toss out new concepts with the vain hope that you will automatically grasp them. Instead, I tie new concepts to old ones. I do my best to ensure that every concept has a strong anchor in your existing mental model. • They help you learn by doing. Instead of spending the better part of a chapter reading how someone else solves a problem, you spend as much time as possible working hands-on with a program: modifying existing programs and writing new programs.
xxxi■ Introduction
C++ is a complicated language, and learning C++ is not easy. In any group of C++ programmers, even simplequestions can often provoke varied responses. Most C++ programmers’ mental models of the language are not merelyincomplete but are flawed, sometimes in fundamental ways. My hope is that I can provide you with a solid foundationin C++, so that you can write interesting and correct programs, and most important, so that you can continue to learnand enjoy C++ for many years to come.
xxxiiEXPLORATION 1
Before you begin your exploration of the C++ landscape, you must gather some basic supplies: a text editor, a C++compiler, a linker, and a debugger. You can acquire these tools separately or bundled, possibly as a package deal withan integrated development environment (IDE). Options abound, regardless of your platform, operating system,and budget. If you are taking a class, the teacher will provide the tools or dictate which tools to use. If you are working at anorganization that already uses C++, you probably want to use its tools, so you can become familiar with them and theirproper use. If you have to acquire your own tools, check out this book’s web site, versions and quality change too rapidly to provide details in print form, so you can find up-to-date suggestionson the web site. The following section gives some general advice.
Ray’s RecommendationsC++ is one of the most widely used programming languages in the world; it is second only to C (depending on howyou measure “widely used”). Therefore, C++ tools abound for many hardware and software environments andat a wide variety of price points. You can choose command-line tools, which are especially popular in UNIX and UNIX-like environments, or youcan opt for an IDE, which bundles all the tools into a single graphical user interface (GUI). Choose whichever styleyou find most comfortable. Your programs won’t care what tools you use to edit, compile, and link them.
Microsoft WindowsIf you are working with Microsoft Windows, I recommend Microsoft’s Visual Studio (be sure that you have a currentrelease). As I write this, the current release is Visual Studio 2012. If you want a no-cost option, download VisualStudio Express from Microsoft’s web site (find a current link at), or for an open-source solution,download MinGW, which is a port of the popular GNU compiler to Windows. Note that C++/CLI is not the same as C++. It is a language that Microsoft invented to help integrate C++ into the.NET environment. That’s why they chose a name that incorporates C++, just as the name C++ derives from thename C. Just as C++ is a distinct language from C, so is C++/CLI a distinct language from C++. This book covers standardC++ and nothing else. If you decide to use Visual Studio, take care that you work with C++, not C++/CLI or ManagedC++ (the predecessor to C++/CLI). Visual Studio includes a number of doodads, froufrous, and whatnots that are unimportant for your core task oflearning C++. Perhaps you will have to use ATL, MFC, or .NET for your job, but for now, you can ignore all that. All youneed is the C++ compiler and standard library. If you prefer a free (as in speech) solution, the GNU compiler collection is available on Windows. Choose theCygwin distribution, which includes a nearly complete UNIX-like environment, or MinGW, which is much smallerand might be easier to manage. In both cases, you get a good C++ compiler and library. This book’s web site() has links with helpful hints for installing and using these tools.
1EXPLORATION 1 ■ Honing Your Tools
Macintosh OS XApple provides a no-cost IDE, call Xcode (see the link at), which includes the clang C++ compilerfrom the LLVM Compiler Infrastructure project. This is an excellent C++ compiler, and Apple bundles an excellentC++ standard library with it.
Everyone ElseI recommend the GNU compiler collection (GCC). The C++ compiler is called g++. Linux and BSD distributionstypically come with the entire GCC suite, including g++, but you might have to install the necessary developerpackages. Be sure you have a recent release. Some hardware vendors (Oracle, HP, etc.) offer a commercial compiler specifically for their hardware. Thiscompiler might offer better optimization than GCC but might not conform to the C++ standard as well as GCC.At least while you work through the exercises in this book, I recommend GCC. If you already have the vendor’scompiler installed, and you don’t want to bother installing yet another compiler, go ahead and use the vendor’scompiler. However, if it ever trips over an example in this book, be prepared to install GCC. If you are using an Intel hardware platform, Intel’s compiler is excellent and available at no cost fornoncommercial use. Visit the book’s web site for a current link. If you want to use an IDE, choose from Eclipse, NetBeans, KDevelop, Anjuta, and others(go to for an up-to-date list).
2 EXPLORATION 1 ■ Honing Your Tools
Compiler OptionsVisual C++ command line /EHsc /ZaVisual C++ IDE Enable C++ exceptions, disable language extensionsg++ -pedantic -std=c++11clang/llvm -pedantic -std=c++11
3EXPLORATION 1 ■ Honing Your Tools
if (argc < 2) read(std::cin, text); else { std::ifstream in(argv[1]); if (not in) { std::perror(argv[1]); return EXIT_FAILURE; } read(in, text); } // Part 2. Sort the text. std::sort(text.begin(), text.end()); // Part 3. Print the sorted text. std::copy(text.begin(), text.end(), std::ostream_iterator<std::string>(std::cout, "\n"));} No doubt, some of this code is gibberish to you. That’s okay. The point of this exercise is not to understand C++but to make sure you can use your tools properly. The comments describe the program, which is a simple sort utility.I could have started with a trivial, “Hello, world”–type of program, but that touches only a tiny fraction of the languageand library. This program, being slightly more complex, does a better job at revealing possible installation or otherproblems with your tools. Now go back and double-check your source code. Make sure you entered everything correctly. Did you actually double-check the program? ________________ Did you find any typos that needed correcting? ________________ To err is human, and there is no shame in typographical errors. We all make them. Go back and recheckyour program. Now compile your program. If you are using an IDE, find the Compile or Build button or menu item. If you areusing command-line tools, be sure to link the program too. For historical (or hysterical) reasons, UNIX tools such asg++ typically produce an executable program named a.out. You should rename it to something more useful or use the-o option to name an output file. Table 1-2 shows sample command lines to use for Visual C++, g++, and clang.
If you receive any errors from the compiler, it means you made a mistake entering the source code; the compiler,linker, or C++ library has not been installed correctly; or the compiler, linker, or library does not conform to the C++standard and so are unsuitable for use with this book. Triple-check that you entered the text correctly. If you areconfident that the error lies with the tools and not with you, check the date of publication. If the tools predate 2011,
4 EXPLORATION 1 ■ Honing Your Tools
discard them immediately. They predate the standard and, therefore, by definition, they cannot conform to thestandard. Compiler vendors have worked hard at ensuring their tools conform to the latest standard, but this takestime. Only in 2013 have we seen compilers that truly implement enough of the C++ 11 standard to be useful. If all else fails, try a different set of tools. Download the current release of GCC or Visual Studio Express. You mayhave to use these tools for this book, even if you must revert to some crusty, rusty, old tools for your job. Successful compilation is one thing, but successful execution is another. How you invoke the program dependson the operating system. In a GUI environment, you will need a console or terminal window where you can type acommand line. You may have to type the complete path to the executable file or only the program name—again, thisdepends on your operating system. When you run the program, it reads text from the standard input stream, whichmeans whatever you type, the program reads. You then have to notify the program you are done, by pressing themagic keystrokes that signal end-of-file. On most UNIX-like operating systems, press Control+D. On Windows,press Control+Z. Running a console application from an IDE is sometimes tricky. If you aren’t careful, the IDE might close theprogram’s window before you have a chance to see any of its output. You have to ensure that the window remainsvisible. Some IDEs (such as Visual Studio and KDevelop) do this for you automatically, asking you to press afinal Enter key before it closes the window. If the IDE doesn’t keep the window open automatically, and you can’t find any option or setting to keep thewindow open, you can force the issue by setting a break point on the program’s closing curly brace or the neareststatement where the debugger will let you set a break point. Knowing how to run the program is one thing; another is to know what numbers to type so you can test theprogram effectively. How would you test list0101 to ensure it is running correctly? _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ Okay, do it. Does the program run correctly? ________________ There, that was easy, wasn’t it? You should try several different sequences. Run the program with no input at all.Try it with one line of text. Try it with two that are already in order and two lines that are in reverse order. Try a lot oftext, in order. Try a lot of text in random order. Try text in reverse order. Before you finish this Exploration, I have one more exercise. This time, the source file is more complicated.It was written by a professional stunt programmer. Do not attempt to read this program, even with adult supervision.Don’t try to make any sense of the program. Above all, don’t emulate the programming style used in this program.This exercise is not for you, but for your tools. Its purpose is to see whether your compiler can correctly compile thisprogram and that your library implementation has the necessary parts of the standard library. It’s not a severe torturetest for a compiler, but it does touch on a few advanced C++ features. So don’t even bother trying to read the code. Just download the file list0102.cpp from the book’s web site andtry to compile and link it with your tools. (I include the full text of the program only for readers who lack convenientInternet access.) If your compiler cannot compile and run Listing 1-2 correctly, you must replace it (your compiler, notthe program). You may be able to squeak by in the early lessons, but by the end of the book, you will be writing somefairly complicated programs, and you need a compiler that is up to the task. (By the way, Listing 1-2 pretty much doesthe same thing as Listing 1-1.)
5EXPLORATION 1 ■ Honing Your Tools
// Part 1. Read the entire input into text. If the command line names a file, // read that file. Otherwise, read the standard input. std::vector<text<char>> text; ///< Store the lines of text here if (argc < 2) text = read(std::cin); else { std::ifstream in(argv[1]); if (not in) { std::perror(argv[1]); return EXIT_FAILURE; } text = read(in); } // Part 2. Sort the text. The second command line argument, if present, // names a locale, to control the sort order. Without a command line // argument, use the default locale (which is obtained from the OS). std::locale const& loc{ std::locale(argc >= 3 ? argv[2] : "") }; std::collate<char> const& collate( std::use_facet<std::collate<char>>(loc) ); std::sort(text.begin(), text.end(), [&collate](std::string const& a, std::string const& b) { return collate.compare(a.data(), a.data()+a.size(), b.data(), b.data()+b.size()) < 0; } ); // Part 3. Print the sorted text. for (auto const& line : text) std::cout << line << '\n';}catch (std::exception& ex){ std::cerr << "Caught exception: " << ex.what() << '\n'; std::cerr << "Terminating program.\n"; std::exit(EXIT_FAILURE);}catch (...){ std::cerr << "Caught unknown exception type.\nTerminating program.\n"; std::exit(EXIT_FAILURE);} I caught you peeking. In spite of my warning, you tried to read the source code, didn’t you? Just remember thatI deliberately wrote this program in a complicated fashion to test your tools. By the time you finish this book, you willbe able to read and understand this program. Even more important, you will be able to write it more simply and morecleanly. Before you can run, however, you must learn to walk. Once you are comfortable working with your tools,it’s time to start learning C++. The next Exploration begins your journey with a reading lesson.
7EXPLORATION 2
I suspect you already have some knowledge of C++. Maybe you already know C, Java, Perl, or other C-like languages.Maybe you know so many languages that you can readily identify common elements. Let’s test my hypothesis. Take afew minutes to read Listing 2-1, then answer the questions that follow it.
9EXPLORATION 2 ■ Reading C++ Code
Listing 2-1 reads integers from the standard input and keeps track of the largest and smallest values entered.After exhausting the input, it then prints those values. If the input contains no numbers, the program prints nothing. Let’s take a closer look at the various parts of the program.
CommentsLine 1 begins with three consecutive slashes to start a comment. The comment ends at the end of the line. Actually,you need only two slashes to signal the start of a comment (//), but as you will learn later in the book, the extra slashhas a special meaning. Note that you cannot put a space between the slashes. That’s true in general for all the multicharacter symbols inC++. It’s an important rule, and one you must internalize early. A corollary to the “no spaces in a symbol” rule is thatwhen C++ sees adjacent characters, it usually constructs the longest possible symbol, even if you can see that doing sowould produce meaningless results. The other method you can use to write a comment in C++ is to begin the comment with /* and end it with */.The difference between this style and the style demonstrated in Listing 2-1 is with this method, your comment canspan multiple lines. You may notice that some programs in this book use /** to start a comment. Much like the thirdslash in Listing 2-1, this second asterisk (*) is magic, but unimportant at this time. A comment cannot nest within acomment of the same style, but you can nest one style of comment in comments of the other style, as illustrated inListing 2-2.
HeadersLines 3 and 4 of Listing 2-1 import declarations and definitions from parts of the standard library. C++, like C andmany other languages, distinguishes between the core language and the standard library. Both are part of thestandard language, and a tool suite is incomplete without both parts. The difference is that the core language isself-contained. For example, certain types are built-in, and the compiler inherently knows about them. Other typesare defined in terms of the built-in types, so they are declared in the standard library, and you must instruct thecompiler that you want to use them. That’s what lines 3 and 4 are all about. In particular, line 3 informs the compiler about the names of the standard I/O streams (cin for the standard inputand cout for the standard output), the input operator (>>), and the output operator (<<). Line 4 brings in the namenumeric_limits. Note that names from the standard library generally begin with std:: (short for “standard”).
10 EXPLORATION 2 ■ Reading C++ Code
In C++ parlance, the #include keyword is also a verb, as in “line 3 includes the iostream header,” “line 4 includesthe limits header,” and so on. A header is typically a file that contains a series of declarations and definitions.(A declaration is a kind of definition. A definition tells the compiler more about a name than a declaration. Don’tworry about the difference yet, but notice when I use declaration and when I use definition.) The compiler needsthese declarations and definitions, so it knows what to do with names such as std::cin. Somewhere in thedocumentation for your C++ compiler and standard library is the location for its standard headers. If you are curious,visit that folder or directory and see what you can find there, but don’t be disappointed if you can’t read the headers.The C++ standard library makes full use of the entire range of C++ language features. It’s likely you won’t be able todecipher most of the library until after you’ve made it through a large part of this book. Another important C++ rule: the compiler has to know what every name means. A human can often infermeaning or at least a part of speech from context. For example, if I were to say, “I furbled my drink all over my shirt,”you may not know exactly what furbled means, but you can deduce that it is the past tense of a verb and that itprobably implies something undesirable and somewhat messy. C++ compilers are a lot dumber than you. When the compiler reads a symbol or identifier, it must know exactlywhat the symbol or identifier means and what part of “speech” it is. Is the symbol a punctuator (such as thestatement-ending semicolon) or an operator (such as a plus sign for addition)? Is the identifier a type? A function?A variable? The compiler also has to know everything you can do with that symbol or name, so it can correctlycompile the code. The only way it can know is for you to tell it, and the way you tell it is by writing a declaration or byimporting a declaration from a header. And that’s what #include directives are all about. Any line that begins with # ends at the end of the line. C++ has several different # directives, but only #includeconcerns us at this time. Inside the angle brackets must be a header name, which is typically a standard library header,but it might also be a header from a third-party library. Later in the book, you’ll even learn to write your own headers. Modify line 4 to misspell limits as stimil. Try to compile the program. What happens? _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ The compiler cannot find any header named stimil, so it issues a message. Then it may try to compile theprogram, but it doesn’t know what std::numeric_limits is, so it issues one or more messages. Some compilerscascade messages, which means every use of std::numeric_limits produces additional messages. The actual errorbecomes lost in the noise. Focus on the first one or few messages the compiler issues. Fix them, then try again. As yougain experience with C++, you will learn which messages are mere noise and which are important. Unfortunately,most compilers will not tell you, for example, that you can’t use std::numeric_limits until you include the <limits>header. Instead, you need a good C++ language reference, so you can look up the correct header on your own. Thefirst place to check is the documentation that accompanies your compiler and library. Authors have been slowerthan compiler-writers to catch up to the C++ 11 standard, so keep checking the web site and bookstores for updatedreferences. Most programmers don’t use <limits> much; Listing 2-1 included it only to obtain the definition ofstd::numeric_limits. On the other hand, almost every program in this book uses <iostream>, because it declaresthe names and types of the I/O stream objects, std::cin and std::cout. There are other I/O headers, but for basicconsole interactions, you need only <iostream>. You will meet more headers in coming Explorations.
Main ProgramEvery C++ program must have int main(), as shown on line 6. You are permitted a few variations on a theme, but thename main is crucial. A program can have only one main, and the name must be spelled using all lowercase characters.The definition must start with int.
11EXPLORATION 2 ■ Reading C++ Code
■■Note Some books instruct you to use void. Those books are wrong. If you have to convince someone that void iswrong and int is right, refer the skeptic to section 3.6.1 of the C++ Standard.
Variable DefinitionsLines 8 through 11 define some variables. The first word on each line is the variable’s type. The next word is thevariable name. The name is followed optionally by an initial value in curly braces. The type int is short for integer,and bool is short for Boolean.
■■Note Boolean is named after George Boole, the inventor of mathematical logic. As such, some languages use thename logical for this type. It is unclear why languages such as C++ use bool instead of boole for the type namedafter Boole.
The name std::numeric_limits is part of the C++ standard library and lets you query the attributes of the built-in arithmetic types. You can determine the number of bits a type requires, the number of decimal digits, the minimumand maximum values, and more. Put the type that you are curious about in angle brackets. (You’ll see this approach tousing types quite often in C++.) Thus, you could also query std::numeric_limits<bool>::min() and get falseas the result. If you were to query the number of bits in bool, what would you expect as a result? ________________ Try compiling and running Listing 2-3, and find out if you are correct.
12 EXPLORATION 2 ■ Reading C++ Code
int main(){ // Note that "digits" means binary digits, i.e., bits. std::cout << "bits per bool: " << std::numeric_limits<bool>::digits << '\n';} Did you get the value you expected? If not, do you understand why you got 1 as a result?
StatementsLine 12 of Listing 2-1 contains a while statement. Lines 15, 17, and 21 begin if statements. They have similar syntax:both statements begin with a keyword, followed by a Boolean condition in parentheses, followed by a statement. Thestatement can be a simple statement, such as the assignment on line 16, or it can be a list of statements within curlybraces. Notice that a simple statement ends with a semicolon. Assignment (lines 14, 16, and 18) uses a single equal sign. For clarity, when I read a program out loud or tomyself, I like to read the equal sign as “gets.” For example, “x gets min.” A while loop performs its associated statement while the condition is true. The condition is tested prior toexecuting the statement, so if the condition is false the first time around, the statement never executes. On line 12, the condition is an input operation. It reads an integer from the standard input (std::cin) and storesthat integer in the variable x. The condition is true as long as a value is successfully stored in x. If the input is malformed,or if the program reaches the end of the input stream, the logical condition becomes false, and the loop terminates. The if statement can be followed by an else branch; you’ll see examples in future Explorations. Line 21’s condition consists of a single name: any. Because it has type bool, you can use it directly as a condition. Modify line 15 to change the statement to just “if (x)”. This kind of mistake sometimes occurs when you getcareless (and we all get careless from time to time). What do you expect to happen when you compile the program? _____________________________________________________________ _____________________________________________________________ Were you surprised that the compiler did not complain? What do you expect to happen when you run the program? _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
If you supply the following input to the program, what do you expect as output? 0 1 2 3 _____________________________________________________________ _____________________________________________________________ If you supply the following input to the program, what do you expect as output? 3 2 1 0 _____________________________________________________________ _____________________________________________________________
13EXPLORATION 2 ■ Reading C++ Code
OutputThe output operator is <<, which your program gets by including <iostream>. You can print a variable’s value,a character string, a single character, or a computed expression. Enclose a single character in single quotes, such as 'X'. Of course, there may be times when you have to include asingle quote in your output. To print a single quote, you will have to escape the quote character with a backslash (\').Escaping a character instructs the compiler to process it as a standard character, not as a part of the program syntax.Other escape characters can follow a backslash, such as \n for a newline (that is, a magic character sequence to starta new line of text; the actual characters in the output depend on the host operating system). To print a backslashcharacter, escape it: '\\'. Some examples of characters include the following: 'x', '#', '7', '\\', '\n'. If you want to print more than one character at a time, use a character string, which is enclosed in double quotes.To include a double quote in a string, use a backslash escape: std::cout << "not quoted; \"in quotes\", not quoted"; A single output statement can use multiple occurrences of <<, as shown in line 22, or you can use multiple outputstatements. The only difference is readability. Modify Listing 2-3 to experiment with different styles of output. Try using multiple output statements. Remember to use curly braces when the body of an if statement contains more than one statement. See! I told you that you could read a C++ program. Now all you have to do is fill in some of your knowledge gapsabout the details. The next Exploration starts doing that with the basic arithmetic operators.
14EXPLORATION 3
Integer Expressions
In Exploration 2, you examined a program that defined a few variables and performed some simple operations on them.This Exploration introduces the basic arithmetic operators. Read Listing 3-1, then answer the questions that follow it.
15EXPLORATION 3 ■ Integer Expressions
Lines 12 and 13 show examples of addition (+) and assignment (=). Addition follows the normal rules ofcomputer arithmetic (we’ll worry about overflow later). Assignment works the way it does in any procedural language. Thus, you can see that Listing 3-1 reads integers from the standard input, adds them up, and prints the average(mean) value, as computed by the division (/) operator. Or does it? What is wrong with Listing 3-1? _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ Try running the program with no input—that is, press the end-of-file keystroke immediately after starting theprogram. Some operating systems have a “null” file that you can supply as the input stream. When a program readsfrom the null file, the input stream always sees an end-of-file condition. On UNIX-like operating systems, run thefollowing command line: list0301 < /dev/null On Windows, the null file is called NUL, so type list0301 < NUL What happens? ____________________________________________________________________ C++ doesn’t like division by zero, does it? Each platform reacts differently. Most systems indicate an errorcondition one way or another. A few quietly give you garbage results. Either way, you don’t get anything meaningful. Fix the program by introducing an if statement. Don’t worry that the book hasn’t covered if statements yet. I’mconfident you can figure out how to ensure this program avoids dividing by zero. Write the corrected program below: _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
16 EXPLORATION 3 ■ Integer Expressions
_____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ Now try your new program. Was your fix successful? _____________________________________________________________ Compare your solution with Listing 3-2.
17EXPLORATION 3 ■ Integer Expressions
Did you get what you expected? Some languages use different operators for integer division and floating-pointdivision. C++ (like C) uses the same operator symbol and depends on the context to decide what kind of division toperform. If both operands are integers, the result is an integer. What do you expect if the input is 2 5 4 _____________________________________________________________ Try it. What is the actual output? _____________________________________________________________ Integer division truncates the result toward zero, so the C++ expression 5 / 3 equals 4 / 3 equals 1. The other arithmetic operators are - for subtraction, * for multiplication, and % for remainder. C++ does not havean operator for exponentiation. Listing 3-3 asks for integers from the user and tells the user whether the number is even or odd. (Don’t worryabout how input works in detail; Exploration 5 will cover that.) Complete line 10.
18 EXPLORATION 3 ■ Integer Expressions
17 if (count == 0)18 std::cout << "No data.\n";19 else20 std::cout << "average = " << sum / count << '\n';21 } Now modify Listing 3-5 to use a single equal sign on line 17. What message does your compiler issue?
_____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
Most modern compilers recognize this common error and issue a warning. Strictly speaking, the code is correct:the condition assigns zero to count. Recall that a condition of zero means false, so the program always prints No data.,regardless of how much data it actually reads. If your compiler does not issue a warning, read the compiler’s documentation. You might have to enable a switchto turn on extra warnings, such as “possible use of assignment instead of comparison” or “condition is always false.” As you can see, working with integers is easy and unsurprising. Text, however, is a little trickier, as you will see inthe next Exploration.
20EXPLORATION 4
Strings
In earlier Explorations, you used quoted character strings as part of each output operation. In this Exploration, youwill begin to learn how to make your output a little fancier by doing more with strings. Start by reading Listing 4-1.
21EXPLORATION 4 ■ Strings
What does the output look like? A tab character does not automatically align a column but merely positions theoutput at the next tab position. To align columns, you have to take control of the output. One easy way to do this is touse multiple tab characters, as shown in Listing 4-3.
22 EXPLORATION 4 ■ Strings
23EXPLORATION 4 ■ Strings
When you define a string variable with no initial value, C++ guarantees that the string is initially empty. ModifyListing 4-4 so the shape and sides variables are uninitialized. Predict the output of the program. _____________________________________________________________ _____________________________________________________________ What happened? Explain. _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ Your program should look like Listing 4-6.
Shape Sides----- -----Square 4Circle ? 4226851
With another compiler on another platform, the final number is 0. Yet another compiler’s program prints-858993460 as the final number. Some systems might even crash instead of printing the value of shape or sides. Isn’t that curious? If you do not supply an initial value for a variable of type std::string, C++ makes sure thevariable starts out with an initial value—namely, an empty string. On the other hand, if the variable has type int, youcannot tell what the initial value will actually be, and in fact, you cannot even tell whether the program will run.This is known as undefined behavior. The standard permits the C++ compiler and runtime environment to do anything,absolutely anything, when confronted with certain erroneous situations, such as accessing an uninitialized variable. A design goal of C++ is that the compiler and library should not do any extra work if they can avoid it. Only theprogrammer knows what value makes sense as a variable’s initial value, so assigning that initial value must be theprogrammer’s responsibility. After all, when you are putting the finishing touches on your weather simulator (the onethat will finally explain why it always rains when I go to the beach), you don’t want the inner loop burdened by even
24 EXPLORATION 4 ■ Strings
one wasteful instruction. The flip side of that performance guarantee is an added burden on the programmer to avoidsituations that give rise to undefined behavior. Some languages help the programmer avoid problem situations, butthat help invariably comes with a performance cost. So what’s the deal with std::string? The short answer is that complicated types, such as strings, are differentfrom the simple, built-in types. For types such as std::string, it is actually simpler for the C++ library to provide awell-defined initial value. Most of the interesting types in the standard library behave the same way. If you have trouble remembering when it is safe to define a variable without an initial value, play it safe and useempty braces: std::string empty{}; int zero{}; I recommend initializing every variable, even if you know the program will overwrite it soon, such as the inputloops we used earlier. Omitting initialization in the name of “performance” rarely improves performance and alwaysimpairs readability. The next Exploration demonstrates the importance of initializing every variable.
OLD-FASHIONED INITIALIZATION
The brace style of initializing all variables was introduced in C++ 11, so code that predates C++ 11 (or new code that was written by programmers who learned C++ prior to C++ 11 and still haven’t come to grips with the new style of initialization) uses different ways to initialize variables. A common way to initialize integers, for example, uses an equal sign. It looks like an assignment statement, but it isn’t. It defines and initializes a variable. int x = 42; You can also use parentheses: int x(42); The same is true for many standard-library types: std::string str1 = "sample"; std::string str2("sample"); Other types require parentheses and don’t work with an equal sign. Other types used curly braces before C++ 11. Equal signs, parentheses, and curly braces all had different rules, and it was hard for beginners to understand the subtle difference between the equal sign and parentheses for initialization. So the standardization committee made an effort to define a single, uniform initialization style in C++ 11, which is what I presented in this Exploration.
25EXPLORATION 5
Simple Input
So far, the Explorations have focused on output. Now it’s time to turn your attention to input. Given that the outputoperator is <<, what do you expect the input operator to be? ________________ That didn’t take a rocket scientist to deduce, did it? The input operator is >>, the opposite direction of theoutput operator. Think of the operators as arrows pointing in the direction that information flows: from the stream tovariables for input, or from variables to the stream for output. Listing 5-1 shows a simple program that performs input and output.
Suppose you enter 42 and 21 as the two input values. What do you expect for the output?
_____________________________________________________________
Now run the program and check your prediction. I hope you got 63. Suppose you type the following as input?
42*21
27EXPLORATION 5 ■ Simple Input
Do you see what happened? If not, try xyz as the input to the program. Try 42-21. Try 42.21. The program exhibits two distinct behaviors that you have to understand. First, to read an int, the input streammust contain a valid integer. The integer can start with a sign (- or +) but must be all digits after that; no interveningwhitespace is allowed. The input operation stops when it reaches the first character that cannot be part of a validinteger (such as *). If at least one digit is read from the input stream, the read succeeds, and the input text is convertedto an integer. The read fails if the input stream does not start with a valid integer. If the read fails, the input variable isnot modified. The second behavior is what you discovered in the previous Exploration; uninitialized int variables result inundefined behavior. In other words, if a read fails, the variable contains junk, or worse. When you learn about floatingpoint numbers, for example, you will learn that some bit patterns in an uninitialized floating-point variable can causea program to terminate. On some specialized hardware, an uninitialized integer can do the same. The moral of thestory is that using an uninitialized variable results in undefined behavior. That’s bad. So don’t do it. Thus, when the input is xyz, both reads fail, and undefined behavior results. You probably see junk values forboth numbers. When the input is 42-21, the first number is 42 and the second number is -21, so the result is correct.However, when the input is 42.21, the first number is 42, and the second number is junk, because an integer cannotstart with a dot (.). Once an input operation fails, all subsequent input attempts will also fail, unless you take remedial action. That’swhy the program doesn’t wait for you to type a second number if the first one is invalid. C++ can tell you when aninput operation fails, so your program can avoid using junk values. Also, you can reset a stream’s error state, so youcan resume reading after handling an error. I will cover these techniques in future Explorations. For now, make sureyour input is valid and correct. Some compilers warn you when your program leaves variables uninitialized, but it is best to be safe and initializeevery variable, all the time. As you can see, even if the program immediately attempts to store a value in the variable, itmight not succeed, which can give rise to unexpected behavior. Did you think that integers could be so complicated? Surely strings are simpler, because there is no need tointerpret them or convert their values. Let’s see if they truly are simpler than integers. Listing 5-2 is similar to Listing5-1, but it reads text into std::string variables.
28 EXPLORATION 5 ■ Simple Input
Think of the standard input as a stream of characters. Regardless of how the user types those characters,the program sees them arrive one by one. (Okay, they arrive in big chunks, by the buffer-load, but that’s a minorimplementation detail. As far as you are concerned, your program reads one character at a time, and it doesn’t matterthat the character comes from the buffer, not the actual input device.) Thus, the program always maintains the notionof a current position in the stream. The next read operation always starts at that position.
29EXPLORATION 5 ■ Simple Input
Before starting any input operation, if the character at the input position is a whitespace character, the programskips (that is, reads and discards) that character. It keeps reading and discarding characters until it reaches a nonspacecharacter. Then the actual read begins. If the program attempts to read an integer, it grabs the character at the input position and checks whether it isvalid for an integer. If not, the read fails, and the input position does not move. Otherwise, the input operation keepsthe character and all subsequent characters that are valid elements of an integer. The input operation interprets thetext as an integer and stores the value in the variable. Thus, after reading an integer, you know that the input positionpoints to a character that is not a valid integer character. When reading a string, all the characters are grabbed from the stream until a whitespace character is reached.Thus, the string variable does not contain any whitespace characters. The next read operation will skip over thewhitespace, as described earlier. The input stream ends at the end of the file (if reading from a file), when the user closes the console or terminal,or when the user types a special keystroke sequence to tell the operating system to end the input (such as Control+Don UNIX or Control+Z on DOS or Windows). Once the end of the input stream is reached, all subsequent attempts toread from the stream will fail. This is what caused the loop to end in Exploration 2. Listing 5-3 shows my version of the name-first program. Naturally, your program will differ in the details, but thebasic outline should agree with yours.
When an input operation fails due to malformed input, the stream enters an error state; e.g., the input streamcontains the string “Ray” when the program tries to read an integer. All subsequent attempts to read from the streamresult in an error, without actually trying to read. Even if the stream subsequently tries to read a string, which wouldotherwise succeed, the error state is sticky, and the string read fails too. In other words, when the program cannot read the user’s age, it won’t be able to read the name either. That’s whythe program gets both name and age correct, or it gets both wrong.
30 EXPLORATION 5 ■ Simple Input
Handling errors in an input stream requires some more advanced C++, but handling errors in your code issomething you can take care of right now. The next Exploration helps you untangle compiler error messages.
31EXPLORATION 6
Error Messages
By now you’ve seen plenty of error messages from your C++ compiler. No doubt, some are helpful and others arecryptic—a few are both. This Exploration presents a number of common errors and gives you a chance to see whatkinds of messages your compiler issues for these mistakes. The more familiar you are with these messages, the easierit will be for you to interpret them in the future. Read through Listing 6-1 and keep an eye out for mistakes.
33EXPLORATION 6 ■ Error Messages
Create three groups: messages that you correctly predicted, messages that you expected but the compiler didnot issue, and messages that the compiler issued but you did not expect. How many messages are in each group?________________
If you use command-line tools, expect to see a slew of errors running across your screen. If you use an IDE, itwill help corral the error messages and associate each message with the relevant point in the source code that thecompiler thinks is the cause of the error. The compiler isn’t always right, but its hint is often a good starting point. Compilers usually group problems into one of two categories: errors and warnings. An error prevents thecompiler from producing an output file (an object file or program). A warning tells you that something is wrong butdoesn’t stop the compiler from producing its output. Modern compilers are pretty good at detecting problematic,but valid, code and issuing warnings, so get in the habit of heeding the warnings. In fact, I recommend dialing up thesensitivity to warnings. Check your compiler’s documentation and look for options that have the compiler detect asmany warnings as possible. For g++ and clang++, the switch is -Wall. Visual Studio uses /Wall. On the other hand,sometimes the compiler gets it wrong, and certain warnings are not helpful. You can usually disable individualwarnings, such as -Wno-unused-local-typedefs (my favorite for g++) or /wd4514 (for Visual Studio). The program actually contains seven errors, but don’t fret if you missed them. Let’s take them one at a time.
MisspellingLine 1 misspells <iostream> as <iosteam>. Your compiler should give you a simple message, informing you that itcould not find <iosteam>. The compiler probably cannot tell that you meant to type <iostream>, so it does not giveyou a suggestion. You have to know the proper spelling of the header name. Most compilers give up completely at this point. If that happens to you, fix this one error then run the compileragain, to see some more messages. If your compiler tries to continue, it does so without the declarations from the misspelled header. In this case,<iostream> declares std::cin and std::cout, so the compiler also issues messages about those names beingunknown, as well as other error messages about the input and output operators.
34 EXPLORATION 6 ■ Error Messages
Bogus CharacterThe most interesting error is the use of a square-bracket character ([) instead of a brace character ({) in line 4. Somecompilers may be able to guess what you meant, which can limit the resulting error messages. Others cannot and givea message that may be rather cryptic. For example, g++ issues many errors, none of which directly points you to theerror. Instead, it issues the following messages:
list0601.cpp:5:58: error: array bound is not an integer constant before ';' token
When you cannot understand the error messages, look at the first message and the line number it identifies.Search for errors at or near the line number. Ignore the rest of the messages. On line 5, you may see another error or two. After you fix them, however, a slew of messages still remains. Thatmeans you still haven’t found the real culprit (which is on line 4). A different compiler (clang++) is more helpful. Itpoints you to line 4. list0601.cpp:5:59: error: expected ']' std::cout < "This program prints a table of squares.\n"; ^list0601.cpp:4:1: note: to match this '['[^ Once you track down the square bracket and change it to a curly brace, you may get entirely different messages. This isbecause the substitution of [ for { so thoroughly confuses the compiler, it cannot make any sense of the rest of the program.Correcting that problem straightens out the program for the compiler, but now it may find a whole new set of errors.
35EXPLORATION 6 ■ Error Messages
Unknown OperatorThe input and output operators (>> and <<) are no different from any other C++ operator, such as addition (+),multiplication (*), or comparison (such as >). Every operator has a limited set of allowed operands. For example, youcannot “add” two I/O streams (e.g., std::cin + std::cout), nor can you use an output operator to “write” a numberto a string (e.g., "text" << 3). On line 5, one error is the use of < instead of <<. The compiler cannot determine that you intended to use << andinstead issues a message that indicates what is wrong with <. The exact message depends on the compiler, but mostlikely the message is not something that helps you solve this particular problem. One compiler complains as follows:
This message notifies you that you are using the wrong operator or the wrong operands. You must determinewhich one it is. Once you fix the operator, notice that the compiler does not issue any message for the other mistake, namely, theextraneous semicolon. Strictly speaking, it is not a C++ error. It is a logical error, but the result is a valid C++ program.Some compilers will issue a warning, advising you that line 6 does nothing, which is a hint that you made a mistake.Other compilers will silently accept the program. The only sure way to detect this kind of mistake is to learn to proofread your code.
Unknown NameAn easy error for a compiler to detect is when you use a name that the compiler does not recognize at all. In this case,accidentally typing the letter l instead of a semicolon produces the name endl instead of end;. The compiler issues aclear message about this unknown name. Fix the semicolon, and now the compiler complains about another operator. This time, you should be able tozoom in on the problem and notice that the operator is facing the wrong way (<< instead of >>). The compiler may notoffer much help, however. One compiler spews out errors of the following form:
list0601.cxxlist'<<''list'<<''
The line number tells you where to look, but it is up to you to find the problem.
36 EXPLORATION 6 ■ Error Messages
Symbol ErrorsBut now you run into a strange problem. The compiler complains that it does not know what a name means(cout on line 17), but you know what it means. After all, the rest of the program uses std::cout without any difficulty.What’s wrong with line 17 that it causes the compiler to forget? Small errors can have profound consequences in C++. As it turns out, a single colon means something completelydifferent from a double colon. The compiler sees std:cout as a statement labeled std, followed by the bare namecout. At least the error message points you to the right place. Then it’s up to you to notice the missing colon.
37EXPLORATION 7
For Loops
Explorations 2 and 3 show some simple while loops. This Exploration introduces the while loop’s big brother, thefor loop.
Bounded LoopsYou’ve already seen while loops that read from the standard input until no more input is available. That is a classiccase of an unbounded loop. Unless you know beforehand exactly what the input stream will contain, you cannotdetermine the loop’s bounds or limits. Sometimes you know in advance how many times the loop must run; that is,you know the bounds of the loop, making it a bounded loop. The for loop is how C++ implements a bounded loop. Let’s start with a simple example. Listing 7-1 shows a program that prints the first ten non-negative integers.
39EXPLORATION 7 ■ For Loops
InitializationThe first part looks similar to a variable definition. It defines an int variable named i, with an initial value of 0.Some C-inspired languages permit only an initialization expression, not a variable definition. In C++, you have achoice: expression or definition. The advantage of defining the loop control variable as part of the initialization is thatyou cannot accidentally refer to that variable outside the loop. The disadvantage of defining the loop control variablein the initialization part is that you cannot deliberately refer to that variable outside the loop. Listing 7-2 demonstratesthe advantage of limiting the loop control variable.
Listing 7-2. You Cannot Use the Loop Control Variable Outside the Loop#include <iostream> int main(){ for (int i{0}; i != 10; i = i + 1) std::cout << i << '\n'; std::cout << "i=" << i << '\n'; // error: i is undefined outside the loop} Another consequence of limiting the loop control variable is that you may define and use the same variable namein multiple loops, as shown in Listing 7-3.
40 EXPLORATION 7 ■ For Loops
ConditionThe middle part follows the same rules as a while loop condition. As you might expect, it controls the loop execution.The loop body executes while the condition is true. If the condition is false, the loop terminates. If the condition isfalse the first time the loop runs, the loop body never executes (but the initialization part always does). Sometimes you will see a for loop with a missing condition. That means the condition is always true, so theloop runs without stopping. A better way to write a condition that is always true is to be explicit and use true as thecondition. That way, anyone who has to read and maintain your code in the future will understand that you deliberatelydesigned the loop to run forever. Think of it as the equivalent of a comment: “This condition deliberately left blank.”
PostiterationThe last part looks like a statement, even though it lacks the trailing semicolon. In fact, it is not a full statement, butonly an expression. The expression is evaluated after the loop body (hence the name postiteration) and before thecondition is tested again. You can put anything you want here, or leave it blank. Typically, this part of the for loopcontrols the iteration, advancing the loop control variable as needed.
41EXPLORATION 7 ■ For Loops
Your TurnNow it’s your turn to write a for loop. Listing 7-4 shows a skeleton of a C++ program. Fill in the missing parts tocompute the sum of integers from 10 to 20, inclusive.
42EXPLORATION 8
Formatted Output
In Exploration 4, you used tab characters to line up output neatly. Tabs are useful but crude. This Explorationintroduces some of the features that C++ offers to format output nicely, such as setting the alignment, padding, andwidth of output fields.
The ProblemThis Exploration begins a little differently. Instead of reading a program and answering questions about it, you mustwrite your own program to solve a problem. The task is to print a table of squares and cubes (the mathematicalvariety, not the geometrical shapes) for integers from 1 to 20. The output of the program should look something likethe following:
N N^2 N^3 1 1 1 2 4 8 3 9 27 4 16 64 5 25 125 6 36 216 7 49 343 8 64 512 9 81 72910 100 100011 121 133112 144 172813 169 219714 196 274415 225 337516 256 409617 289 491318 324 583219 361 685920 400 8000
To help you get started, Listing 8-1 gives you a skeleton program. You need only fill in the loop body.
43EXPLORATION 8 ■ Formatted Output
N N^2 N^31 1 12 4 83 9 274 16 645 25 1256 36 2167 49 3438 64 5129 81 72910 100 1000
Left-alignment is not the way we usually write numbers. Tradition dictates that numbers should align to the right(or on decimal points, when applicable—more on that in the related section, “Alignment,” later in this Exploration).Right-aligned numbers are easier to read. C++ offers some simple but powerful techniques to format output. To format the table of powers, you have todefine a field for each column. A field has a width, a fill character, and an alignment. The following sections explainthese concepts in depth.
Field WidthBefore exploring how you would specify alignment, first you must know how to set the width of an output field. I gaveyou a hint in Listing 8-1. What is the hint?_____________________________________________________________ The first line of the program is #include <iomanip>, which you have not seen before. This header declares someuseful tools, including std::setw(), which sets the minimum width of an output field. For example, to print a numberso that it occupies at least three character positions, call std::setw(3). If the number requires more space thanthat—say the value is 314159—the actual output will take up as much space as needed. In this case, the spacing turnedout to be six character positions. To use setw, call the function as part of an output statement. The statement looks like you are trying to print setw,but in fact, nothing is printed, and all you are doing is manipulating the state of the output stream. That’s why setw iscalled an I/O manipulator. The <iomanip> header declares several manipulators, which you will learn about in due course.
44 EXPLORATION 8 ■ Formatted Output
Listing 8-2 shows the table of powers program, using setw to set the width of each field in the table.
Fill CharacterBy default, values are padded, or filled, with space characters (' '). You can set the fill character to be any that youchoose, such as zero ('0') or an asterisk ('*'). Listing 8-3 shows a fanciful use of both fill characters in a program thatprints a check.
// Print date in USA order. Later in the book, you will learn how to // handle internationalization. cout << "Date: " << setfill('0') << setw(2) << month << '/' << setw(2) << day << '/' << setw(2) << year << '\n'; cout << "Pay to the order of: CASH\n"; cout << "The amount of $" << setfill('*') << setw(8) << dollars << '.' << setfill('0') << setw(2) << cents << '\n';} Notice that unlike setw, setfill is sticky. That is, the output stream remembers the fill character and uses thatcharacter for all output fields until you set a different fill character.
std PrefixAnother new feature in Listing 8-3 is the declaration using namespace std;. All those std:: prefixes can sometimesmake the code hard to read. The important parts of the names become lost in the clutter. By starting your programwith using namespace std;, you are instructing the compiler to treat names that it doesn’t recognize as though theybegan with std::. As the keyword indicates, std is called a namespace. Almost every name in the standard library is part of thestd namespace. You are not allowed to add anything to the std namespace, nor are any third-party library vendors.Thus, if you see std::, you know that what follows is part of the standard library (so you can look it up in any reliablereference). More important, you know that most names you invent in your own program will not conflict with anyname in the standard library, and vice versa. Namespaces keep your names separate from the standard library names.Later in this book, you will learn to create your own namespaces, which help organize libraries and manage largeapplications. On the other hand, using namespace std; is a dangerous declaration, and one I use sparingly. Without thestd:: qualifier before every standard library name, you have opened the door to confusion. Imagine, for example, ifyour program defines a variable named cout or setw. The compiler has strict rules for interpreting names and wouldnot be confused at all, but the human reader certainly would be. It is always best to avoid names that collide withthose in the standard library, with or without using namespace std;.
AlignmentC++ lets you align output fields to the right or the left. If you want to center a number, you are on your own. To forcethe alignment to be left or right, use the left and right manipulators, which you get for free when you include<iostream>. (The only time you need <iomanip> is when you want to use manipulators that require additionalinformation, such as setw and setfill.) The default alignment is to the right, which might strike you as odd. After all, the first attempt at using tabcharacters to align the table columns produced left-aligned values. As far as C++ is concerned, however, it knowsnothing about your table. Alignment is within a field. The setw manipulator specifies the width, and the alignmentdetermines whether the fill characters are added on the right (left-alignment) or on the left (right-alignment). Theoutput stream has no memory of other values it may have printed earlier (such as on the previous line). So, forexample, if you want to align a column of numbers on their decimal points, you must do that by hand (or ensure thatevery value in the column has the same number of digits after the decimal point).
46 EXPLORATION 8 ■ Formatted Output
Exploring FormattingNow that you know the rudiments of formatting output fields, it is time to explore a little and help you develop athorough understanding of how field width, fill character, and alignment interact. Read the program in Listing 8-4and predict its output.
00004242000042-42-
Lots of different programs can achieve the same goal. My program, shown in Listing 8-5, is only one possibilityof many.
The manipulators that take arguments, such as setw and setfill, are declared in <iomanip>. The manipulatorswithout arguments, such as left and right, are declared in <iostream>. If you can’t remember, include both headers.If you include a header that you don’t really need, you might see a slightly slower compilation time, but no other illeffects will befall you.
The left and boolalpha manipulators are not declared in <iostream>. I lied to you. They are actually declared in <ios>. But <iostream> contains #include <ios>, so you get everything in <ios> automatically when you include <iostream>. I’ve been lying to you for some time. The input operator (>>) is actually declared in <istream>, and the output operator (<<) is declared in <ostream>. As with <ios>, the <iostream> header always includes <istream> and <ostream>. Thus, you can include <iostream> and get all the headers you need for typical input and output. Other headers, such as <iomanip>, are less commonly used, so they are not part of <iostream>. So I wasn’t really lying to you, just waiting until you could handle the truth.
Alternative SyntaxI like to use manipulators, because they are concise, clear, and easy to employ. You can also apply functions tothe output stream object, using the dot operator (.). For example, to set the fill character, you can call std::cout.fill('*'). The fill function is called a member function, because it is a member of the output stream’s type. Youcannot apply it to any other kind of object. Only some types have member functions, and each type defines themember functions that it allows. A large part of any C++ library reference is taken up with the various types and theirmember functions. (The member functions of an output stream are declared in <ostream>, along with the outputoperators. An input stream’s member functions are declared in <istream>. Both of these headers are includedautomatically when you include <iostream>.) When setting sticky properties, such as fill character or alignment, you might prefer using member functionsinstead of manipulators. You can also use member functions to query the current fill character, alignment and otherflags, and field width—something you can’t do with manipulators. The member function syntax uses the stream object, a dot (.), and the function call, e.g., cout.fill('0').Setting the alignment is a little more complicated. Listing 8-6 shows the same program as Listing 8-5 but uses memberfunctions instead of manipulators.
48 EXPLORATION 8 ■ Formatted Output
On Your OwnNow it is time for you to write a program from scratch. Feel free to look at other programs, to make sure you haveall the necessary parts. Write this program to produce a multiplication table for the integers from 1 to 10, inclusive,as follows:
*| 1 2 3 4 5 6 7 8 9 10----+---------------------------------------- 1| 1 2 3 4 5 6 7 8 9 10 2| 2 4 6 8 10 12 14 16 18 20 3| 3 6 9 12 15 18 21 24 27 30 4| 4 8 12 16 20 24 28 32 36 40 5| 5 10 15 20 25 30 35 40 45 50 6| 6 12 18 24 30 36 42 48 54 60 7| 7 14 21 28 35 42 49 56 63 70 8| 8 16 24 32 40 48 56 64 72 80 9| 9 18 27 36 45 54 63 72 81 90 10| 10 20 30 40 50 60 70 80 90 100
After you finish your program and have made sure it produces the correct output, compare your program withmine, which is shown in Listing 8-7.
49EXPLORATION 8 ■ Formatted Output
50EXPLORATION 9
Now that you understand the basics, it is time to start moving on to more exciting challenges. Let’s write a realprogram, something nontrivial but still simple enough to master this early in the book. Your job is to write a programthat reads integers from the standard input, sorts them into ascending order, and then prints the sorted numbers,one per line. At this point, the book has not quite covered enough material for you to solve this problem, but it is instructive tothink about the problem and the tools you may need to solve it. Your first task in this Exploration is to write pseudo-codefor the program. Write C++ code where you can and make up whatever you need to tackle the problem. _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
51EXPLORATION 9 ■ Arrays and Vectors
VectorsLine 7 defines the variable data, of type std::vector<int>. C++ has several container types, that is, data structuresthat can contain a bunch of objects. One of those containers is vector, which is an array that can change size. All C++containers require an element type, that is, the type of object that you intend to store in the container. In this case, theelement type is int. Specify the element type in angle brackets: <int>. That tells the compiler that you want data to bea vector and that the vector will store integers. What’s missing from the definition?_____________________________________________________________ The vector has no size. Instead, the vector can grow or shrink while the program runs. (If you know that youneed an array of a specific, fixed size, you can use the type array. You will use vector much more often than array.)Thus, data is initially empty. Like std::string, vector is a library type, and it has a well-defined initial value, namely,empty, so you can omit the {} initializer if you wish.
52 EXPLORATION 9 ■ Arrays and Vectors
You can insert and erase items at any position in the vector, although the performance is best when you additems only at the end or erase them only from the end. That’s how the program stores values in data: by callingpush_back, which adds an element to the end of a vector (line 13). The “back” of a vector is the end, with the highestindex. The “front” is the beginning, so back() returns the last element of the vector, and front() returns the first.(Don’t call these functions if the vector is empty; that yields undefined behavior.) If you want to refer to a specificelement, use at(n), where n is a zero-based index, as shown on line 20. The size() function (line 19) returns thenumber of elements in the vector. Therefore, valid indices range from 0 to size() - 1. When you read C++ programs, you will most likely see square brackets (data[n]) used to access elements of avector. The difference between square brackets and the at function is that the at function provides an additional levelof safety. If the index is out of bounds, the program will terminate cleanly. On the other hand, using square bracketswith an invalid index will result in undefined behavior: you don’t know what will happen. Most dangerous is that yourprogram will not terminate but will continue to run with the bad data. That’s why I recommend using at for now. As you can tell from the std:: prefix, the vector type is part of the standard library and is not built into thecompiler. Therefore, you need #include <vector>, as shown on line 3. No surprises there. All the functions mentioned so far are member functions; that is, you must supply a vector object on the left-handside of the dot operator (.) and the function call on the right-hand side. Another kind of function does not use the dotoperator and is free from any particular object. In most languages, this is the typical kind of function, but sometimesC++ programmers call them free functions, to distinguish them from member functions. Line 16 shows an example ofa free function, std::sort. How would you define a vector of strings?
_____________________________________________________________
Substitute std::string for int to get std::vector<std::string>. You can also define a vector of vectors,which is a kind of two-dimensional array: std::vector<std::vector<int>>. (Prior to C++ 11, you needed to insert aspace between the closing angle brackets, so you will see a lot of code written that way. But in C++ 11, you don’t needto concern yourself with that syntax oddity.)
IteratorsThe std::sort function sorts stuff, as you can tell from the name. In some other object-oriented language, you mightexpect vector to have a sort() member function. Alternatively, the standard library could have a sort function thatcan sort anything the library can throw at it. The C++ library falls into the latter category, but with a twist. The twist is that the std::sort function does not take a vector as an argument to sort the contents of the vector.Instead, you pass two values to the sort function: the start of a range and the one past the end of the range. The sortfunction sorts the values in that range. The elements in the range can be any type, as long as the compiler knows howto compare objects with the less-than (<) operator. The example program specifies the starting position by callingdata.begin() and one past the end by calling data.end(). These “positions” are called iterators. An iterator is an object that can refer to an element of a sequence. As thename implies, an iterator can also iterate over a sequence. The sequence might be elements of a vector or they couldbe data in a file or database or nodes in a tree. The implementation of the sequence is irrelevant, and the std::sortfunction knows nothing about it. Instead, the sort function sees only the iterators. Iterators present a simple interface, even if their implementation is complicated. The * operator returns the valueto which the iterator refers (*iter), and you can advance an iterator to the next element of the sequence (++iter).You can compare two iterators to see if they refer to the same element (iter1 == iter2). Iterators come in differentflavors, and some flavors let you modify the element or move backward in the sequence. A bounded loop requires a starting and ending position. One way to specify these for a vector is to specify thepositions of the first and last elements, but that raises a thorny issue of what to do with an empty vector. Long ago,computer scientists invented the concept of a sentry or guard. Sometimes, the sentry was a real element addedafter the last element of a container, marking the position one past the last element. In that case, a container withonly the sentry element was empty. Iterators work similarly, but whether the container has a true sentry is a hidden
53EXPLORATION 9 ■ Arrays and Vectors
implementation detail. A special iterator value represents the sentry at a position of one past the last element in thecontainer, even if the container has no actual sentry element. The notion of “one past the end” is a common idiom inthe C++ library and programs. Thus, data.begin() returns an iterator that refers to the first element of data, and data.end() returns an iteratorwith the special one-past-the-end value, as shown in Figure 9-1.
data
10 20 30 42 50
data.begin() data.end()
54EXPLORATION 10
The previous Exploration introduced vectors and iterators using std::sort to sort a vector of integers. This Explorationexamines iterators in more depth and introduces generic algorithms, which perform useful operations on iterators.
AlgorithmsThe std::sort function is an example of a generic algorithm, so named because these functions implement commonalgorithms and operate generically. That is, they work for just about anything you can express as a sequence. Most ofthe standard algorithms are declared in the <algorithm> header, although the <numeric> header contains a few thatare numerically oriented. The standard algorithms run the gamut of common programming activities: sorting, searching, copying,comparing, modifying, and more. Searches can be linear or binary. A number of functions, including std::sort,reorder elements within a sequence. No matter what they do, all generic algorithms share some common features. Almost all the generic algorithms operate on iterators. (The sole exceptions are std::max, std::min, andstd::minmax, which return the maximum and minimum of two values.) Earlier, I mentioned that iterators come indifferent flavors, each flavor having different capabilities. Although C++ has five flavors in all, you can broadly groupthem into two categories: read and write. A read iterator refers to a position in a sequence of values that enables reading from the sequence. Thealgorithms use read iterators for input but do not modify the values. Typically, you must specify a range with a pair ofread iterators: start and one past the end. A write iterator refers to a position in a sequence where the algorithm is to write its output. Typically, you specifyonly the starting position of the output sequence. The algorithm does not and cannot check for overflow, so you mustensure the output sequence has sufficient room to accommodate everything the algorithm will write. For example, the std::copy algorithm copies values from an input sequence to an output sequence. Thefunction takes three arguments: two read iterators to specify the input range and one write iterator to specify the startof the output range. You must ensure the output has enough capacity. Call the resize member function to set the sizeof the output vector, as shown in Listing 10-1.
55EXPLORATION 10 ■ Algorithms and Iterators
output.resize(input.size()); std::copy(input.begin(), input.end(), output.begin()); // Now output has a complete copy of input. assert(input == output);} The assert function is a quick way to verify that what you think is true actually is true. You assert a logicalstatement, and if you are wrong, the program terminates with a message that identifies the assertion. The assertfunction is declared in <cassert>; the c means the C++ library inherits this header from the C standard library. Notethat assert is one of the rare exceptions to the rule that standard library members begin with std::. If the program is correct, it runs and exits normally. But if we make a mistake, the assertion triggers, and theprograms fails with a message. Test the program in Listing 10-1. Just to see what happens when an assertion fails, comment out the call tostd::copy and run it again. Write down the message you get. _____________________________________________________________ _____________________________________________________________ Also note the initialization of input. Listing 10-1 demonstrates another application of “universal initialization”(as introduced in Exploration 4). The comma-separated values inside the curly braces are used to initialize theelements of the vector.
Member TypesRemember Listing 9-1? Go back and look at line 19. The definition in the first part of the for loop is particularly scary,even for experienced C++ programmers. In addition to member functions, a C++ library type can have member types.In this case, the parent type, std::vector<int>, has a member type named size_type. Use this type whenever youhave to store a size or index for a vector. You usually use the dot (.) operator to call a member function, but you use :: (called the scope operator) to referto a member type. The size_type is like an int, but not really. In particular, you cannot assign a negative value to size_type. (Afterall, what kind of vector has a negative size?) Or rather, the language rules let you assign a negative value, but you won’tget the result you want or expect. A good compiler warns you that you are making a mistake. Until you learn enoughC++ to appreciate the subtleties of size_type, the best strategy is to use size_type only for loop control, for storingthe size of a vector, and for storing indices. Don’t try to perform arithmetic with size_type values beyond simplyincrementing a loop control variable. Line 19 uses size_type to define the variables i and end, which are the loop control variables. The loopincrements i from 0 up to the vector size (end), at which point it exits. This is a common idiom for looping througha vector when you need the vector indices. Think of end as the index of one past the end, just like iterators. Mostprograms, in fact, do not need to use the vector indices. I wrote Listing 9-1 that way only to demonstrate the atmember function. A better way to write the program in Listing 9-1 is to use iterators instead of indices and the at() memberfunction. To define an iterator, substitute the member type iterator for size_type in the definition of the loopcontrol variable. Initialize the loop control variable to data.begin(), and end the loop when the iterator equals data.end(). Use the ++ operator to advance the iterator and * to obtain the vector element to which the iterator refers. Putthese pieces together and rewrite lines 19 and 20 of Listing 9-1 to use iterators. _____________________________________________________________ _____________________________________________________________
56 EXPLORATION 10 ■ Algorithms and Iterators
Compare your response to Listing 10-2. (For your convenience, I repeat the entire program. That makes it easy foryou to compile and test the program.)
A Simpler LoopC++ 11 introduced an easy way to write a for loop. It uses iterators under the hood but hides them in a simple syntax.
This loop defines a hidden iterator that traverses all the elements of data (calling its begin() and end() memberfunctions). Each time through the loop, the iterator is dereferenced (with the * operator) and assigned to element.Thus, inside the loop body, you simply use element to obtain each value in the range. After the loop body finishes, theiterator is advanced with the ++ operator.
57EXPLORATION 10 ■ Algorithms and Iterators
This style of loop is called a range-based for loop or a for-each loop. Because range-based for loops are new inC++, you will still see a lot of iterators in for loops, but as more people pick up C++, expect to see many more uses ofrange-based for loops. You can see why.
Listing 10-3. Sorting Integers by Using Only Generic Algorithms and Iterator Adapters 1 #include <algorithm> 2 #include <iostream> 3 #include <iterator> 4 #include <vector> 5 6 int main() 7 { 8 std::vector<int> data{}; 910 // Read integers one at a time.11 std::copy(std::istream_iterator<int>(std::cin),12 std::istream_iterator<int>(),13 std::back_inserter(data));1415 // Sort the vector.16 std::sort(data.begin(), data.end());1728 // Print the vector, one number per line.19 std::copy(data.begin(), data.end(),20 std::ostream_iterator<int>(std::cout, "\n"));21 } A std::istream_iterator (line 11) creates a read iterator that reads from an input stream. Every time you reada value from the iterator, the istream_iterator object uses the >> operator to read a value from the stream. You mustsupply the type in angle brackets, so that the compiler knows what type of information you want to read from thestream, which you pass as a function argument. With no argument, std::istream_iterator<int>() (line 12) returnsa special one-past-the-end iterator. When the input stream iterator equals this special one-past-the-end iterator, theprogram has reached the end of the stream, and no more input is available. The std::back_inserter function (line 13) takes a vector (or any object that has a push_back function) andwraps it in a write iterator. Any time you assign a value to the iterator, the back insert iterator calls the push_backfunction to add the value to the object. Using back_inserter, you can guarantee that the program will not overrun theoutput buffer. Finally, an ostream_iterator (line 20) is the counterpart to an istream_iterator. It takes an output stream andwraps it in a write iterator. Any value that you assign to the iterator is written to the stream using the << operator. Youcan pass an optional string argument, and the ostream_iterator writes that string after each value. In this case, thestring contains just a newline character, so each number is written on its own line. All these special iterators are declared in <iterator>. You don’t need this header to use an ordinary iterator,such as that returned from a vector’s begin() function, but you do need it if you use a special iterator, such asistream_iterator.
58 EXPLORATION 10 ■ Algorithms and Iterators
Until you are accustomed to using the generic algorithms, iterators, and special iterators, this style ofprogramming can seem unusual. Once you are familiar with these unique C++ library members, you will find suchcode easier to read and write than more traditional programming styles. It’s now time for you to practice using iterators and algorithms. Write a program that reads integers from the standard input into a vector. Feel free to use any style of loop(indices, iterators, or range-based). If you are unsure which style to use, I recommend getting used to range-basedloops. Print each value, followed by twice the value and then the value squared. Thus, the output contains one line perinput value, and each line has three numbers. Align the columns, as you learned in the previous Exploration. Test your program using the following input: 3108 What output do you expect?
_____________________________________________________________ _____________________________________________________________ _____________________________________________________________
Listing 10-4. Doubling and Squaring Input Values in a Vector by Using Iterators#include <iomanip>#include <iostream>#include <vector> int main(){ std::vector<int> data{}; int x{}; while (std::cin >> x) data.push_back(x);
59EXPLORATION 10 ■ Algorithms and Iterators
Listing 10-5. Doubling and Squaring Input Values in a Vector by Using a Range-Based Loop#include <iomanip>#include <iostream>#include <vector> int main(){ std::vector<int> data{}; int x{}; while (std::cin >> x) data.push_back(x); std::cout.fill(' '); for (int element : data) std::cout << std::setw(2) << element << std::setw(3) << element * 2 << std::setw(4) << element * element << '\n';} The ++ operator, which advances an iterator, also advances an integer, as shown in Listing 10-4. The nextExploration takes a closer look at this handy operator.
60EXPLORATION 11
The previous Exploration introduced the increment (++) operator to advance an iterator. This operator works onnumeric types as well. Not surprisingly, it has a decrement counterpart: --. This Exploration takes a closer look atthese operators, which appear so often, they are part of the language name.
■■Note I know that you C, Java, etc. programmers have been waiting for this Exploration ever since I wrote i = i + 1in Exploration 7. As you saw in the previous Exploration, the ++ operator means more in C++ than what you’re used to.That’s why I waited until now to introduce it.
IncrementThe ++ operator is familiar to C, Java, Perl, and many other programmers. C was the first widespread languageto introduce this operator to mean “increment” or “add 1.” C++ expanded the usage it inherited from C; thestandard library uses the ++ operator in several new ways, such as advancing an iterator (as you saw in the previousExploration). The increment operator comes in two flavors: prefix and postfix. The best way to understand the differencebetween these two flavors is with a demonstration, as shown in Listing 11-1.
Listing 11-1. Demonstrating the Difference Between Prefix and Postfix Increment#include <iostream> int main(){ int x{42}; std::cout << "x = " << x << "\n"; std::cout << "++x = " << ++x << "\n"; std::cout << "x = " << x << "\n"; std::cout << "x++ = " << x++ << "\n"; std::cout << "x = " << x << "\n";}
61EXPLORATION 11 ■ Increment and Decrement
DecrementThe increment operator has a decrement counterpart: --. The decrement operator subtracts one instead of addingone. Decrement also has a prefix and postfix flavor. The prefix operator pre-decrements, and the postfix operatorpost-decrements. You can increment and decrement any variable with a numeric type; however, only some iterators permitdecrement. For example, write iterators move forward only. You can use the increment operator (prefix or postfix), but notdecrement. Test this for yourself. Write a program that uses std::ostream_iterator and try to use the decrementoperator on the iterator. (If you need a hint, look at Listing 10-4. Save the ostream_iterator object in a variable. Thenuse the decrement operator. It doesn’t matter that the program makes no sense; it won’t get past the compiler anyway.) What error message do you get? _____________________________________________________________ _____________________________________________________________ Different compilers issue different messages, but the essence of the message should be that the -- operator is notdefined. If you need help with the program, see Listing 11-2.
62 EXPLORATION 11 ■ Increment and Decrement
63EXPLORATION 11 ■ Increment and Decrement
The start iterator points to the beginning of the data vector, and end initially points to one past the end. If thevector is empty, the for loop terminates without executing the loop body. Then the loop body decrements end so thatit points to an actual element of the vector. If the vector contains an even number of elements, the if condition is true,so std::iter_swap is called, and the program advances start one position. As the name implies, std::iter_swap swaps the values stored at two iterators, in this case, the iterators startand end. You can think of it as performing the following: int tmp = *start;*start = *end;*end = tmp; Notice that the program is careful to compare start != end after each increment or decrement operation. Ifthe program had only one comparison, it would be possible for start and end to pass each other. The loop conditionwould never be true, and the program would exhibit undefined behavior, so the sky would fall, the earth wouldswallow me, or worse. Also note how the for loop has an empty postiteration part. The iteration logic appears in different places in theloop body, which is not the preferred way to write a loop but is necessary in this case. You can rewrite the loop, so the postiteration logic appears only in the loop header. Some programmers arguethat distributing the increment and decrement in the loop body makes the loop harder to understand and, inparticular, harder to prove the loop terminates correctly. On the other hand, cramming everything in the loop headermakes the loop condition especially tricky to understand, as you can see in Listing 11-4.
64 EXPLORATION 11 ■ Increment and Decrement
■■Note I prefer Listing 11-3 over Listing 11-4. I really don’t like to bury increment and decrement operators in themiddle of a complicated condition.
So what else would experienced C++ programmers do? Because they have broader knowledge of the C++ standardlibrary, they would make better use of it. In particular, they would use the std::reverse algorithm, which reversesthe elements in a range. std::reverse(data.begin(), data.end()); Another idea is to use istream_iterator, which you learned about in Exploration 9. This time, you will use it alittle differently. Instead of using back_inserter (introduced in the previous Exploration) and the copy algorithm,Listing 11-5 calls the insert member function, which copies values from any iterator range into the vector. The valuesare inserted before the position given by the first argument (data.end(), in this case).
65EXPLORATION 12
You first met the bool type in Exploration 2. This type has two possible values: true and false, which are reservedkeywords (unlike in C). Although most Explorations have not needed to use the bool type, many have used logicalexpressions in loop and if-statement conditions. This Exploration examines the many aspects of the bool type andlogical operators.
67EXPLORATION 12 ■ Conditions and Logic
By default, when an input stream has to read a bool value, it actually reads an integer, and if the integer’s valueis 1, the stream interprets that as true. The value 0 is false, and any other value results in an error. With the std::boolalpha manipulator, the input stream requires the exact text true or false. Integers are notallowed, nor are any case differences. The input stream accepts only those exact words. Use the std::noboolalpha manipulator to revert to the default numeric Boolean values. Thus, you can mixalphabetic and numeric representations of bool in a single stream, as follows: bool a{true}, b{false};std::cin >> std::boolalpha >> a >> std::noboolalpha >> b;std::cout << std::boolalpha << a << ' ' << std::noboolalpha << b; Reading and writing bool values does not actually occur all that often in most programs.
Boolean TypeC++ automatically converts many different types to bool, so you can use integers, I/O stream objects, and other valueswhenever you need a bool, such as in a loop or if-statement condition. You can see this for yourself in Listing 12-2.
68 EXPLORATION 12 ■ Conditions and Logic
_____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ Check your answer. Were you right? ________________
You may have been fooled by lines 15 and 16. C++ does not interpret the contents of string literals to decidewhether to convert the string to true or false. All character strings are true, even empty ones. (The C++ languagedesigners did not do this to be perverse. There’s a good reason that strings are true, but you will have to learn quite abit more C++ in order to understand why.) On the other hand, character literals (lines 13 and 14) are completely different from string literals. The compilerconverts the escape character '\0', which has numeric value zero, to false. All other characters are true. Recall from many previous examples (especially in Exploration 3) that loop conditions often depend on aninput operation. If the input succeeds, the loop condition is true. What is actually happening is that C++ knows howto convert a stream object (such as std::cin) to bool. Every I/O stream keeps track of its internal state, and if anyoperation fails, the stream remembers that fact. When you convert a stream to bool, if the stream is in a failure state,the result is false. Not all complex types can be converted to bool, however. What do you expect to happen when you compile and run Listing 12-3? _____________________________________________________________
■■Note Although an istream knows how to convert an input string to bool, the std::string type lacks the informa-tion it needs to interpret the string. Without knowledge of the string’s context, it is unrealistic to ask a string to interprettext, such as “true,” “vrai,” or “richtig.”
69EXPLORATION 12 ■ Conditions and Logic
What about std::vector? Do you think C++ defines a conversion from std::vector to bool? ________________.Write a program to test your hypothesis. What is your conclusion? _____________________________________________________________ This is another case in which no general solution is feasible. Should an empty vector be false, whereas all othersare true? Maybe a std::vector<bool> that contains only false elements should be false. Only the applicationprogrammer can make these decisions, so the C++ library designers wisely chose not to make them for you; therefore,you cannot convert std::vector to bool. However, there are ways of obtaining the desired result by calling memberfunctions.
Logic OperatorsReal-world conditions are often more complicated than merely converting a single value to bool. To accommodatethis, C++ offers the usual logical operators: and, or, and not (which are reserved keywords). They have their usualmeaning from mathematical logic, namely that and is false, unless both operands are true, or is true, unless bothoperands are false, and not inverts the value of its operand. More important, however, is that the built-in and and or operators do not evaluate their right-hand operand,unless they have to. The and operator must evaluate its right-hand operand, only if the left-hand operand is true.(If the left-hand operand is false, the entire expression is false, and there is no need to evaluate the right-handoperand.) Similarly, the or operator evaluates its right-hand operand only if the left-hand operand is true. Stoppingthe evaluation early like this is known as short-circuiting. For example, suppose you are writing a simple loop to examine all the elements of a vector to determine whetherthey are all equal to zero. The loop ends when you reach the end of the vector or when you find an element not equalto zero. Write a program that reads numbers into a vector, searches the vector for a nonzero element, and prints amessage about whether the vector is all zero. You can solve this problem without using a logical operator, but try to use one, just for practice. Take a look atListing 12-4, to see one way to solve this problem.
What happens when the iterator reaches the end of the vector? _____________________________________________________________ The condition iter != data.end() becomes false at the end of the vector. Because of short-circuiting, C++never evaluates the *iter == 0 part of the expression, which is good. Why is this good? What would happen if short-circuiting did not take place? _____________________________________________________________ _____________________________________________________________ Imagine that iter != end is false; in other words, the value of iter is end. That means *iter is just like *end,which is bad—really bad. You are not allowed to dereference the one-past-the-end iterator. If you are lucky, it wouldcrash your program. If you are unlucky, your program would continue to run, but with completely unpredictable anderroneous data, and, therefore, unpredictable and erroneous results. Short-circuiting guarantees that C++ will not evaluate *iter when iter equals end, which means iter will alwaysbe valid when the program dereferences it, which is good. Some languages (such as Ada) use different operators forshort-circuiting and non–short-circuiting operations. C++ does not. The built-in logical operators always performshort-circuiting, so you never accidentally use non–short-circuiting when you intended to use the short-circuitingoperator.
Old-Fashioned SyntaxThe logical operators have symbolic versions: && for and, || for or, and ! for not. The keywords are clearer, easier toread, easier to understand, and less error-prone. That’s right, less error-prone. You see, && means and, but & is also anoperator. Similarly, | is a valid operator. Thus, if you accidentally write & instead of &&, your program will compile andeven run. It might seem to run correctly for a while, but it will eventually fail, because & and && mean different things.(You’ll learn about & and | later in this book.) New C++ programmers aren’t the only ones to make this mistake. I’veseen highly experienced C++ programmers write & when they mean &&, or | instead of ||. Avoid this error by usingonly the keyword logical operators. I was hesitant about even mentioning the symbolic operators, but I can’t ignore them. Many C++ programsuse the symbolic operators instead of the keyword equivalents. These C++ programmers, having grown up with thesymbols, prefer to continue to use the symbols rather than the keywords. This is your chance to become a trendsetter.Eschew the old-fashioned, harder-to-read, harder-to-understand, error-prone symbols and embrace the keywords.
Comparison OperatorsThe built-in comparison operators always yield bool results, regardless of their operands. You have already seen ==and != for equality and inequality. You also saw < for less than, and you can guess that > means greater than. Similarly,you probably already know that <= means less than or equal and >= means greater than or equal. These operators produce the expected results when you use them with numeric operands. You can even usethem with vectors of numeric types.
71EXPLORATION 12 ■ Conditions and Logic
Write a program that demonstrates how < works with a vector of int. (If you’re having trouble writing theprogram, take a look at Listing 12-5.) What are the rules that govern < for a vector? _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
72 EXPLORATION 12 ■ Conditions and Logic
73EXPLORATION 13
Compound Statements
You have already used compound statements (that is, lists of statements enclosed in curly braces) in many programs.Now it’s time to learn some of the special rules and uses for compound statements, which are also known as blocks.
StatementsC++ has some hairy, scary syntax rules. By comparison though, the syntax for statements is downright simplistic. TheC++ grammar defines most statements in terms of other statements. For example, the rule for while statements is while ( condition ) statement In this example, bold elements are required, such as the while keyword. Italic elements stand for other syntaxrules. As you can likely deduce from the example, a while statement can have any statement as the loop body,including another while statement. The reason most statements appear to end with a semicolon is because the most fundamental statement in C++is just an expression followed by a semicolon. expression ; This kind of statement is called an expression statement. I haven’t discussed the precise rules for expressions yet, but they work the way they do in most other languages,with a few differences. Most significant is that assignment is an expression in C++ (as it is in C, Java, C#, etc., but not inPascal, Basic, Fortran, etc.). Consider the following: while (std::cin >> x) sum = sum + x; This example demonstrates a single while statement. Part of the while statement is another statement: inthis case, an expression statement. The expression in the expression statement is sum = sum + x. Expressionsin expression statements are often assignments or function calls, but the language permits any expression. Thefollowing, therefore, is a valid statement: 42; What do you think happens if you use this statement in a program? _____________________________________________________________
75EXPLORATION 13 ■ Compound Statements
Modern compilers are usually able to detect statements that serve no useful purpose and eliminate them fromthe program. Typically, the compiler tells you what it’s doing, but you may have to supply an extra option to tell thecompiler to be extra picky. For example, try the -Wall option for g++ or /Wall for Microsoft Visual C++. (That’s Wall, asin all warnings, not the thing holding up your roof.) The syntax rule for a compound statement is { statement* } where * means zero or more occurrences of the preceding rule (statement). Notice that the closing curly brace has nosemicolon after it. How does C++ parse the following? while (std::cin >> x){ sum = sum + x; ++count;}_______________________________________________________________________________________________________________________________________________________________________________________
Once again, you have a while statement, so the loop body must be a single statement. In this example, the loopbody is a compound statement. The compound statement is a statement that consists of two expression statements.Figure 13-1 shows a tree view of the same information.
statement
while statement
compound statement
{ statement statement }
expression ; expression ;
76 EXPLORATION 13 ■ Compound Statements
Consider the body of main(), such as the one in Listing 13-1. What do you see? That’s right, it’s a compoundstatement. It’s an ordinary block, and it follows the same rules as any other block. In case you were wondering, thebody of main() must be a compound statement. This is one of the few circumstances in which C++ requires a specifickind of statement, instead of allowing any statement whatsoever. Find and fix the errors in Listing 13-1. Visually locate as many errors as you can by reading the code. When youthink you found and fixed them all, try compiling and running the program.
Did you find them all without the compiler’s help? ________________ The errors are: • Extra semicolon on line 9 • Extra semicolon on line 12 • Missing semicolon from the end of lines 11 and 15 • Extra semicolon on line 17 For extra credit, which errors are not syntax violations (the compiler will not alert you to them) and do notaffect the program’s behavior? ________________________________________________________________ ________________________________________________________________
77EXPLORATION 13 ■ Compound Statements
If you answered “the extra semicolon on line 17,” give yourself a star. Strictly speaking, the extra semicolonrepresents an empty, do-nothing statement, called a null statement. Such a statement sometimes has a use in a loop,especially a for loop that does all its work in the loop header, leaving nothing for the loop body to do. (See an examplein Listing 12-4.) Thus, the way the compiler interprets line 9 is that the semicolon is the statement body for the if statement. Thenext statement is a compound statement, which is followed by an else, which has no corresponding if, hence theerror. Every else must be a counterpart to an earlier if in the same statement. In other words, every if conditionmust be followed by exactly one statement, then by an optional else keyword and another statement. You cannot useelse in any other way. As written, the if statement on line 9 is followed by three statements: a null statement, a compound statement,and another null statement. The solution is to delete the null statements by deleting the semicolons on lines 9 and 12. The statements that make up a compound statement can be any statements, including other compoundstatements. The next section explains why you might want to nest a compound statement within another compoundstatement. Line 6 shows that you can declare more than one variable at a time, using a comma separator. I prefer to defineone variable at a time but wanted to show you this style too. Each variable receives its own initializer.
78 EXPLORATION 13 ■ Compound Statements
int main(){ std::vector<int> data{}; data.insert(data.begin(), std::istream_iterator<int>(std::cin), std::istream_iterator<int>()); // Silly way to sort a vector. Assume that the initial portion // of the vector has already been sorted, up to the iterator iter. // Find where *iter belongs in the already sorted portion of the vector. // Erase *iter from the vector and re-insert it at its sorted position. // Use binary search to speed up the search for the proper position. // Invariant: elements in range [begin(), iter are already sorted. for (std::vector<int>::iterator iter{data.begin()}, end{data.end()}; iter != end; ) { // Find where *iter belongs by calling the standard algorithm // lower_bound, which performs a binary search and returns an iterator // that points into data at a position where the value should be inserted. int value{*iter}; std::vector<int>::iterator here{std::lower_bound(data.begin(), iter, value)}; if (iter == here) ++iter; // already in sorted position else { // erase the out-of-position item, advancing iter at the same time. iter = data.erase(iter); // re-insert the value at the correct position. data.insert(here, value); } } // Debugging code: check that the vector is actually sorted. Do this by comparing // each element with the preceding element in the vector. for (std::vector<int>::iterator iter{data.begin()}, prev{data.end()}, end{data.end()}; iter != end; ++iter) { if (prev != data.end()) assert(not (*iter < *prev)); prev = iter; } // Print the sorted vector all on one line. Start the line with "{" and // end it with "}". Separate elements with commas. // An empty vector prints as "{ }". std::cout << '{'; std::string separator{" "};
79EXPLORATION 13 ■ Compound Statements
80 EXPLORATION 13 ■ Compound Statements
int main(){ std::vector<int> data{}; data.insert(data.begin(), std::istream_iterator<int>(std::cin), std::istream_iterator<int>()); std::sort(data.begin(), data.end()); { // Print the sorted vector all on one line. Start the line with "{" and // end it with "}". Separate elements with commas. An empty vector prints // as "{ }". std::cout << '{'; std::string separator{" "}; for (int element : data) { std::cout << separator << element; separator = ", "; } std::cout << " }\n"; } // Cannot use separator out here.} Most C++ programmers nest blocks infrequently. As you learn more C++, you will discover a variety of techniquesthat improve on nested blocks and keep your main program from looking so cluttered.
_____________________________________________________________ _____________________________________________________________ _____________________________________________________________
Personally, I find for loops can become cluttered very easily. Nonetheless, keeping loop control variables localto the loop is critical for clarity and code comprehension. When faced with a large, unknown program, one of thedifficulties you face in understanding that program is knowing when and how variables can take on new values. If avariable is local to a loop, you know the variable cannot be modified outside the loop. That is valuable information. Ifyou still need convincing, try reading and understanding Listing 13-4.
81EXPLORATION 13 ■ Compound Statements
82 EXPLORATION 13 ■ Compound Statements
Well, that wasn’t too hard, was it? After all, you recently finished reading Listing 13-2, so you can see thatListing 13-4 is intended to do the same thing but is reorganized slightly. The difficulty is in keeping track of the valuesof p and i, and ensuring that they have the correct value at each step of the program. Try compiling and running theprogram. Record your observations. _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
I made a mistake and wrote v = *p instead of v = *i. Congratulations if you spotted this error before you ranthe program. If the variables had been properly defined local to their respective scopes, this error could never haveoccurred. The next Exploration introduces file I/O, so your exercises can read and write files, instead of using console I/O.I’m sure your fingers will appreciate it.
83EXPLORATION 14
Reading from standard input and writing to standard output works fine for many trivial programs, and it is a standardidiom for UNIX and related operating systems. Nonetheless, real programs must be able to open named files forreading, writing, or both. This Exploration introduces the basics of file I/O. Later Explorations will tackle moresophisticated I/O issues.
Reading FilesThe most common file-related task in these early Explorations will involve reading from a file instead of from thestandard input stream. One of the greatest benefits of this is it saves a lot of tedious typing. Some IDEs make it difficultto redirect input and output, so it’s easier to read from a file and sometimes write to a file. Listing 14-1 shows arudimentary program that reads integers from a file named list1401.txt and writes them, one per line, to the standardoutput stream. If the program cannot open the file, it prints an error message.
85EXPLORATION 14 ■ Introduction to File I/O
Once the file is open, read from it the same way you read from std::cin. All the input operators that are declaredin <istream> work equally well for an ifstream, as they do for std::cin. The std::perror (declared in <cstdio>) function prints an error message. If the file cannot be opened, the exactreason is saved in a global variable, and perror uses that variable to decide which message to print. It also prints itsargument. Run the program when you know the input file does not exist. What message does the program display? _____________________________________________________________ If you can, create the input file, then change the protection on the file, so you can no longer read it. Run theprogram. What message do you get this time? _____________________________________________________________
Writing FilesAs you have probably guessed, to write to a file, you define an ofstream object. To open the file, simply name the filein the variable’s initializer. If the file does not exist, it will be created. If the file does exist, its old contents are discardedin preparation for writing new contents. If the file cannot be opened, the ofstream object is left in an error state, soremember to test it before you try to use it. Use an ofstream object the same way you use std::cout. Modify Listing 14-1 to write the numbers to a named file. This time, name the input file list1402.in and namethe output file list1402.out. Compare your solution with mine in Listing 14-2.
86 EXPLORATION 14 ■ Introduction to File I/O
The program opens the input file first. If that succeeds, it opens the output file. If the order were reversed, theprogram might create the output file then fail to open the input file, and the result would be a wasted, empty file.Always open input files first. Also notice that the program does not close the input file, if it cannot open the output file. Don’t worry: it closesthe input file just fine. When in is destroyed at the end of main, the file is automatically closed. I know what you’re thinking: If in is automatically closed, why call close at all? Why not let in close automaticallyin all cases? For an input file, that’s actually okay. Feel free to delete the in.close(); statement from the program. Foran output file, however, doing so is unwise. Some output errors do not arise until the file is closed, and the operating system flushes all its internal buffersand does all the other cleanup it needs to do when closing a file. Thus, an output stream object might not receive anerror from the operating system until you call close(). Detecting and handling these errors is an advanced skill. Thefirst step toward developing that skill is to adopt the habit of calling close() explicitly for output files. When it comestime to add the error-checking, you will have a place where you can add it. Try running the program in Listing 14-2 in various error scenarios. Create the output file, list1402.out, and thenuse the operating system to mark the file as read-only. What happens? _____________________________________________________________ If you noticed that the program does not check whether the output operations succeed, congratulations forhaving sharp eyes! C++ offers a few different ways to check for output errors, but they all have drawbacks. The easiestis to test whether the output stream is in an error state. You can check the stream after every output operation,but that approach is cumbersome, and few people write code that way. Another way lets the stream check for anerror condition after every operation and alerts your program with an exception. You’ll learn about this techniquein Exploration 45. A frighteningly common technique is to ignore output errors altogether. As a compromise, Irecommend testing for errors after calling close(). Listing 14-3 shows the final version of the program.
Basic I/O is not difficult, but it can quickly become a morass of gooey, complicated code when you start to throwin sophisticated error-handling, international issues, binary I/O, and so on. Later Explorations will introduce mostof these topics, but only when the time is ripe. For now, however, go back to earlier programs and practice modifyingthem to read and write named files instead of the standard input and output streams. For the sake of brevity (if for noother reason), the examples in the book will continue to use the standard I/O streams. If your IDE interferes withredirecting the standard I/O streams, or if you just prefer named files, you now know how to change the examples tomeet your needs.
88EXPLORATION 15
Now that you understand the basics, it’s time to move on to more exciting challenges. Let’s write a realprogram—something nontrivial but still simple enough to master this early in the book. Your task is to write aprogram that reads words and counts the frequency of each unique word. For the sake of simplicity, a word is astring of non-space characters separated by white space. Be aware, however, that by this definition, words end upincluding punctuation characters, but we’ll worry about fixing that problem later. This is a complicated program, touching on everything you’ve learned about C++ so far. If you want to exerciseyour new understanding of file I/O, read from a named file. If you prefer the simplicity, read from the standard input.Before jumping in and trying to write a program, take a moment to think about the problem and the tools you need tosolve it. Write pseudo-code for the program. Try to write C++ code where you can, and make up whatever else youneed to tackle the problem. Keep it simple—and don’t dwell on trying to get syntax details correct. _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
89EXPLORATION 15 ■ The Map Data Structure
Using MapsThe title of this Exploration tells you what C++ feature will help provide an easy solution to this problem. What C++calls a map, some languages and libraries call a dictionary or association. A map is simply a data structure that storespairs of keys and values, indexed by the key. In other words, it maps a key to a value. Within a map, keys are unique.The map stores keys in ascending order. Thus, the heart of the program is a map that stores strings as keys andnumber of occurrences as the associated value for each key. Naturally, your program needs the <map> header. The map datatype is called std::map. To define a map, you needto specify the key and value type within angle brackets (separated by a comma), as shown in the following example: std::map<std::string, int> counts; You can use almost any type as the key and value types, even another map. As with vector, if you do not initializea map, it starts out empty. The simplest way to use a map is to look up values using square brackets. For example, counts["the"] returnsthe value associated with the key, "the". If the key is not present in the map, it is added with an initial value of zero. Ifthe value type were std::string, the initial value would be an empty string. Armed with this knowledge, you can write the first part of the program—collecting the word counts—as shown inListing 15-1. (Feel free to modify the program to read from a named file, as you learned in Exploration 14.)
PairsThe best way to print the map is to use a range-based for loop to iterate over the map. Each map element is a singleobject that contains the key and the value. The key is called first, and the value is called second.
90 EXPLORATION 15 ■ The Map Data Structure
■■Note The two parts of the map element’s value are not named key and value, because the std::pair type is ageneric part of the C++ library. The library uses this type in several different places. Thus, the names of the parts of apair are also generic and not tied specifically to map.
Use a dot (.) operator to access a member of the pair. To keep things simple, print the output as the key, followedby a tab character, followed by the count, all on one line. Putting all these pieces together, you end up with the finishedprogram, as presented in Listing 15-2.
91EXPLORATION 15 ■ The Map Data Structure
■■Tip Remember size_type from Exploration 10? If not, go back and refresh your memory. Exploration 10 has someimportant admonitions about size_type.
Searching in MapsA map stores its data in sorted order by key. Searching in a map, therefore, is pretty fast (logarithmic time). Because amap keeps its keys in order, you can use any of the standard binary search algorithms (such as lower_bound, to whichyou were introduced in Exploration 13), but even better is to use map’s member functions. These member functionshave the same names as the standard algorithms but can take advantage of their knowledge of a map’s internalstructure. The member functions also run in logarithmic time, but with less overhead than the standard algorithms.
92 EXPLORATION 15 ■ The Map Data Structure
For example, suppose you want to know how many times the word the appears in an input stream. You can readthe input and collect the counts in the usual way, then call find("the") to see if "the" is in the map, and if so, get aniterator that points to its key/value pair. If the key is not in the map, find() returns the end() iterator. If the key ispresent, you can extract the count. You have all the knowledge and skills you need to solve this problem, so go aheadand write the program to print the number of occurrences of the word the. Once again, you can use explore15.txtas sample input. If you don’t want to use redirection, modify the program to read from the explore15.txt file. What count does your program print when you provide this file as the input? __________ The programpresented in Listing 15-4 detects ten occurrences.
93EXPLORATION 15 ■ The Map Data Structure
Ouch. Each compiler is different, but mine spews out a screenful of cryptic errors. In this one case, universalinitialization fails us. Instead, you must use an equal sign or parentheses to initialize the auto iterator. Either of thefollowing two variable definitions will work: auto iterator_using_parens( counts.find("the") );auto iterator_using_equal = counts.find("the"); If you don’t like the exception in this case, C++ (like C) offers another way out: type synonyms, which justhappens to be the subject of the next Exploration.
94EXPLORATION 16
Type Synonyms
typedef DeclarationsC++ inherits the basic syntax and semantics of typedef from C, so you might already be familiar with this keyword.If so, please bear with me while I bring other readers up to speed. The idea of a typedef is to create a synonym, or alias, for another type. There are two compelling reasons forcreating type synonyms. • They create a short synonym for a long type name. For example, you may want to use count_iter as a type synonym for std::map<std::string,int>::iterator. • They create a mnemonic synonym. For example, a program might declare height as a synonym for int, to emphasize that variables of type height store a height value. This information helps the human reader understand the program. The basic syntax for a typedef declaration is like defining a variable, except you start with the typedef keyword,and the name of the type synonym takes the place of the variable name. typedef std::map<std::string,int>::iterator count_iter;typedef int height; Revisit Listing 15-4 and simplify the program by using a typedef declaration. Compare your result withListing 16-1.
Listing 16-1. Counting Words, with a Clean Program That Uses typedef#include <iomanip>#include <iostream>#include <map>#include <string>
95EXPLORATION 16 ■ Type Synonyms
int main(){ std::map<std::string, int> counts{}; typedef std::map<std::string,int> count_map; typedef count_map::iterator count_iterator; // Read words from the standard input and count the number of times // each word occurs. std::string word{}; while (std::cin >> word) ++counts[word]; count_iterator the{counts.find("the")}; if (the == counts.end()) std::cout << "\"the\": not found\n"; else if (the->second == 1) std::cout << "\"the\": occurs " << the->second << " time\n"; else std::cout << "\"the\": occurs " << the->second << " times\n";} I like the new version of this program. It’s a small difference in this little program, but it lets us continue to useuniversal initialization, while preserving clarity and readability.
Common typedefsThe standard library makes heavy use of typedefs, as you have already seen. For example, std::vector<int>::size_typeis a typedef for an integer type. You don’t know which integer type (C++ has several, which you will learn about inExploration 25), nor does it matter. All you have to know is that size_type is the type to use if you want to store a size orindex in a variable. Most likely, size_type is a typedef for std::size_t, which is itself a typedef. The std::size_t typedef is asynonym for an integer type that is suitable for representing a size. In particular, C++ has an operator, sizeof,which returns the size in bytes of a type or object. The result of sizeof is an integer of type std::size_t, however acompiler-writer chooses to implement sizeof and std::size_t.
■■Note A “byte” is defined as the size of type char. So, by definition, sizeof(char) == 1. The size of other typesdepends on the implementation. On most popular desktop workstations, sizeof(int) == 4, but 2 and 8 are also likelycandidates.
Type AliasesAnother way to declare a typedef is with the using keyword. This style of type declaration is called a type alias.For example: using height = int;using count_map = std::map<std::string, int>; 96 EXPLORATION 16 ■ Type Synonyms
The meaning of a type alias is identical to a typedef. They are merely two different syntaxes for the same concept.Some people find type aliases to be easier to read, because the name you are declaring comes first, rather than beinghidden in a typedef declaration. That was short and sweet, wasn’t it? Now you can return to the problem of counting words. This program has anumber of usability flaws. What can you think of to improve the word-counting program? _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
97EXPLORATION 17
Characters
In Exploration 2, I introduced you to character literals in single quotes, such as '\n', to end a line of output, but I havenot yet taken the time to explain these fundamental building blocks. Now is the time to explore characters in greaterdepth.
Character TypeThe char type represents a single character. Internally, all computers represent characters as integers. The characterset defines the mapping between characters and numeric values. Common character sets are ISO 8859-1 (also calledLatin-1) and ISO 10646 (same as Unicode), but many, many other character sets are in wide use. The C++ standard does not mandate any particular character set. The literal '4' represents the digit 4, but theactual value that the computer uses internally is up to the implementation. You should not assume any particularcharacter set. For example, in ISO 8859-1 (Latin-1), '4' has the value 52, but in EBCDIC, it has the value 244. Similarly, given a numeric value, you cannot assume anything about the character that value represents.If you know a char variable stores the value 169, the character may be 'z' (EBCDIC), '©' (Unicode), or 'Љ'(ISO 8859-5). C++ does not try to hide the fact that a character is actually a number. You can compare char values with intvalues, assign a char to an int variable, or do arithmetic with chars. Performing arithmetic is fraught with danger,unless you know the actual character set, but C++ guarantees that any character set your compiler and library supportrepresents digit characters with contiguous values, starting at '0'. Thus, for example, the following is true for all C++implementations: '0' + 7 == '7' Read Listing 17-1. What does the program do? (Hint: the get member function reads a single character from thestream. It does not skip over white space or treat any character specially. Extra hint: what happens if you subtract '0'from a character that you know to be a digit?) _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
99EXPLORATION 17 ■ Characters
100 EXPLORATION 17 ■ Characters
All the digit characters have contiguous values, so the inner loop tests to determine if a character is a digitcharacter by comparing it to the values for '0' and '9'. If it is a digit, subtracting the value of '0' from it leaves youwith an integer in the range 0 to 9. The final loop reads characters and does nothing with them. The loop terminates when it reads a new linecharacter. In other words, the final loop reads and ignores the rest of the input line. Programs that need to handle white space on their own (such as Listing 17-1) can use get, or you can tell theinput stream not to skip over white space prior to reading a number or anything else. The next section discussescharacter I/O in more detail.
Character I/OYou just learned that the get function reads a single character without treating white space specially. You can do thesame thing with a normal input operator, but you must use the std::noskipws manipulator. To restore the defaultbehavior, use the std::skipws manipulator (declared in <ios>). // Skip white space, then read two adjacent characters.char left, right;std::cin >> left >> std::noskipws >> right >> std::skipws; After turning off the skipws flag, the input stream does not skip over leading whitespace characters. For instance,if you were to try to read an integer, and the stream is positioned at white space, the read would fail. If you were totry to read a string, the string would be empty, and the stream position would not advance. So you have to considercarefully whether to skip white space. Typically, you would do that only when reading individual characters. Remember that an input stream uses the >> operator (Exploration 5), even for manipulators. Using >> formanipulators seems to break the mnemonic of transferring data to the right, but it follows the convention of alwaysusing >> with an input stream. If you forget, the compiler will remind you. Write a program that reads the input stream one character at a time and echoes the input to the standardoutput stream verbatim. This is not a demonstration of how to copy streams but an example of working withcharacters. Compare your program with Listing 17-2.
101EXPLORATION 17 ■ Characters
Listing 17-3. Finding the Points with the Largest x and y Values#include <algorithm>#include <iostream>#include <limits>#include <vector> int main(){ typedef std::vector<int> intvec; typedef intvec::iterator iterator; intvec xs, ys; // store the xs and ys { // local block to keep I/O variables local int x{}, y{}; // variables for I/O char sep{}; // Loop while the input stream has an integer (x), a character (sep), // and another integer (y); then test that the separator is a comma. while (std::cin >> x >> sep and sep == ',' and std::cin >> y) { xs.push_back(x); ys.push_back(y); } } for (iterator x{xs.begin()}, y{ys.begin()}; x != xs.end(); ++x, ++y) std::cout << *x << ',' << *y << '\n';} The while loop is the key. The loop condition reads an integer and a character and tests to determine if thecharacter is a comma, before reading a second integer. The loop terminates if the input is invalid or ill-formed or if theloop reaches the end of file. A more sophisticated program would distinguish between these two cases, but that’s aside issue for the moment. Also notice how the final for loop iterates over two vectors at the same time. A range-based for loop doesn’thelp in this case, so the loop must use explicit iterators. Notice how a typedef keeps the for loop header tidy andreadable. (Recall from Exploration 15 that the auto keyword doesn’t work as expected when combined with universalinitialization.)
102 EXPLORATION 17 ■ Characters
Character EscapesIn addition to '\n', C++ offers several other escape sequences, such as '\t', for horizontal tab. Table 17-1 lists all thecharacter escapes. Remember that you can use these escapes in character literals and string literals.
Escape Meaning\a Alert: ring a bell or otherwise signal the user\b Backspace\f Formfeed\n Newline\r Carriage return\t Horizontal tab\v Vertical tab\\ Literal \\? Literal ?\' Literal '\" Literal "\xXX . . . Hexadecimal (base 16) character value
The last two items are the most interesting. An escape sequence of one to three octal digits (0 to 7) specifies thevalue of the character. Which character the value represents is up to the implementation. Understanding all the caveats from the first section of this Exploration, there are times when you must specify anactual character value. The most common is '\0', which is the character with value zero, also called a null character,which you may utilize to initialize char variables. It has some other uses as well, especially when interfacing with Cfunctions and the C standard library. The final escape sequence (\x) lets you specify a character value in hexadecimal. Typically, you would use twohexadecimal digits, because this is all that fits in the typical, 8-bit char. (The purpose of longer \x escapes is for widecharacters, the subject of Exploration 55.) The next Exploration continues your understanding of characters by examining how C++ classifies charactersaccording to letter, digit, punctuation, etc.
103EXPLORATION 18
Character Categories
Exploration 17 introduced and discussed characters. This Exploration continues that discussion with characterclassification (e.g., upper or lowercase, digit or letter), which, as you will see, turns out to be more complicated thanyou might have expected.
Character SetsAs you learned in Exploration 17, the numeric value of a character, such as 'A', depends on the character set.The compiler must decide which character set to use at compile time and at runtime. This is typically based onpreferences that the end user selects in the host operating system. Character-set issues rarely arise for the basic subset of characters—such as letters, digits, and punctuationsymbols—that are used to write C++ source code. Although it is conceivable that you could compile a program usingISO 8859-1 and run that program using EBCDIC, you would have to work pretty hard to arrange such a feat. Mostlikely, you will find yourself using one or more character sets that share some common characteristics. For example,all ISO 8859 character sets use the same numeric values for the letters of the Roman alphabet, digits, and basicpunctuation. Even most Asian character sets preserve the values of these basic characters. Thus, most programmers blithely ignore the character-set issue. We use character literals, such as '%' andassume the program will function the way we expect it to, on any system, anywhere in the world—and we are usuallyright. But not always. Assuming the basic characters are always available in a portable manner, we can modify the word-countingprogram to treat only letters as characters that make up a word. The program would no longer count right andright? as two distinct words. The string type offers several member functions that can help us search in strings,extract substrings, and so on. For example, you can build a string that contains only the letters and any other characters that you want toconsider to be part of a word (such as '-'). After reading each word from the input stream, make a copy of the wordbut keep only the characters that are in the string of acceptable characters. Use the find member function to try tofind each character; find returns the zero-based index of the character, if found, or std::string::npos, if not found. Using the find function, rewrite Listing 15-3 to clean up the word string prior to inserting it in the map. Testthe program with a variety of input samples. How well does it work? Compare your program with Listing 18-1.
Listing 18-1. Counting Words: Restricting Words to Letters and Letter-Like Characters#include <iomanip>#include <iostream>#include <map>#include <string>
105EXPLORATION 18 ■ Character Categories
int main(){ typedef std::map<std::string, int> count_map; typedef std::string::size_type str_size; count_map counts{}; // Read words from the standard input and count the number of times // each word occurs. std::string okay{"ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789-_"}; std::string word{}; while (std::cin >> word) { // Make a copy of word, keeping only the characters that appear in okay. std::string copy{}; for (char ch : word) if (okay.find(ch) != std::string::npos) copy.push_back(ch); // The "word" might be all punctuation, so the copy would be empty. // Don't count empty strings. if (not copy.empty()) ++counts[copy]; } // Determine the longest word. str_size longest{0}; for (auto pair : counts) if (pair.first.size() > longest) longest = pair.first.size(); // For each word/count pair... const int count_size{10}; // Number of places for printing the count for (auto pair : counts) // Print the word, count, newline. Keep the columns neatly aligned. std::cout << std::setw(longest) << std::left << pair.first << std::setw(count_size) << std::right << pair.second << '\n';} Some of you may have written a program very similar to mine. Others among you—particularly those livingoutside the United States—may have written a slightly different program. Perhaps you included other characters inyour string of acceptable characters. For example, if you are French and using Microsoft Windows (and the Windows 1252 character set), you mayhave defined the okay object as follows: std::string okay{"ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÄÇÈÉÊËÎÏÔÙÛÜŒŸ" "abcdefghijklmnopqrstuvwxyzàáäçèéêëîïöùûüœÿ" "0123456789-_"}; But what if you then try to compile and run this program in a different environment, particularly one that uses theISO 8859-1 character set (popular on UNIX systems)? ISO 8859-1 and Windows 1252 share many character codes but
106 EXPLORATION 18 ■ Character Categories
differ in a few significant ways. In particular, the characters 'Œ', 'œ', and 'Ÿ' are missing from ISO 8859-1. As a result,the program may not compile successfully in an environment that uses ISO 8859-1 for the compile-time character set. What if you want to share the program with a German user? Surely that user would want to include characterssuch as 'Ö', 'ö', and 'ß' as letters. What about Greek, Russian, and Japanese users? We need a better solution. Wouldn’t it be nice if C++ provided a simple function that would notify us if a characteris a letter, without forcing us to hard-code exactly which characters are letters? Fortunately, it does.
Character CategoriesAn easier way to write the program in Listing 18-1 is to call the isalnum function (declared in <locale>). This functionindicates whether a character is alphanumeric in the runtime character set. The advantage of using isalnum is thatyou don’t have to enumerate all the possible alphanumeric characters; you don’t have to worry about differingcharacter sets; and you don’t have to worry about accidentally omitting a character from the approved string. Rewrite Listing 18-1 to call isalnum instead of find. The first argument to std::isalnum is the character to test,and the second is std::locale{""}. (Don’t worry yet about what that means. Have patience: I’ll get to that soon.) Try running the program with a variety of alphabetic input, including accented characters. Compare the resultswith the results from your original program. The files that accompany this book include some samples that use avariety of character sets. Choose the sample that matches your everyday character set and run the program again,redirecting the input to that file. If you need help with the program, see my version of the program in Listing 18-2. For the sake of brevity, Ieliminated the neat-output part of the code, reverting to simple strings and tabs. Feel free to restore the pretty output,if you desire.
// For each word/count pair, print the word & count on one line. for (auto pair : counts) std::cout << pair.first << '\t' << pair.second << '\n';} Now turn your attention to the std::locale{""} argument. The locale directs std::isalnum to the characterset it should use to test the character. As you saw in Exploration 17, the character set determines the identity of acharacter, based on its numeric value. A user can change character sets while a program is running, so the programmust keep track of the user’s actual character set and not depend on the character set that was active when youcompiled the program. Download the files that accompany this book and find the text files whose names begin with sample. Find theone that best matches the character set you use every day, and select that file as the redirected input to theprogram. Look for the appearance of the special characters in the output. Change locale{""} to locale{} in the boldface line of Listing 18-2. Now compile and run the program with thesame input. Do you see a difference? ________________ If so, what is the difference? _____________________________________________________________ _____________________________________________________________
Without knowing more about your environment, I can’t tell you what you should expect. If you are using aUnicode character set, you won’t see any difference. The program would not treat any of the special charactersas letters, even when you can plainly see they are letters. This is due to the way Unicode is implemented, andExploration 55 will discuss this topic in depth. Other users will notice that only one or two strings make it to the output. Western Europeans who use ISO 8859-1may notice that ÁÇÐÈ is considered a word. Greek users of ISO 8859-7 will see ABGDE as a word. Power users who know how to change their character sets on the fly can try several different options. You mustchange the character set that programs use at runtime and the character set that your console uses to display text. What is most noticeable is that the characters the program considers to be letters vary from one character set toanother. But after all, that’s the idea of different character sets. The knowledge of which characters are letters in whichcharacter sets is embodied in the locale.
LocalesIn C++, a locale is a collection of information pertaining to a culture, region, and language. The locale includesinformation about • formatting numbers, currency, dates, and time • classifying characters (letter, digit, punctuation, etc.) • converting characters from uppercase to lowercase and vice versa • sorting text (e.g., is 'A' less than, equal to, or greater than 'Å'?) • message catalogs (for translations of strings that your program uses) Every C++ program begins with a minimal, standard locale, which is known as the classic or "C" locale. Thestd::locale::classic() function returns the classic locale. The unnamed locale, (std::locale{""}), is the user’spreferences that C++ obtains from the host operating system. The locale with the empty-string argument is oftenknown as the native locale. The advantage of the classic locale is that its behavior is known and fixed. If your program must read data in afixed format, you don’t want the user’s preferences getting in the way. By contrast, the advantage of the native formatis that the user chose those preferences for a reason and wants to see program output follow that format. A user who
108 EXPLORATION 18 ■ Character Categories
always specifies a date as day/month/year doesn’t want a program printing month/day/year simply because that’s theconvention in the programmer’s home country. Thus, the classic format is often used for reading and writing data files, and the native format is best used tointerpret input from the user and to present output directly to the user. Every I/O stream has its own locale object. To affect the stream’s locale, call its imbue function, passing thelocale object as the sole argument.
■■Note You read that correctly: imbue, not setlocale or setloc—given that the getloc function returns a stream’scurrent locale—or anything else that might be easy to remember. On the other hand, imbue is such an unusual name fora member function, you may remember it for that reason alone.
In other words, when C++ starts up, it initializes each stream with the classic locale, as follows: std::cin.imbue(std::locale::classic());std::cout.imbue(std::locale::classic()); Suppose you want to change the output stream to adopt the user’s native locale. Do this using the followingstatement at the start of your program: std::cout.imbue(std::locale{""}); For example, suppose you have to write a program that reads a list of numbers from the standard input andcomputes the sum. The numbers are raw data from a scientific instrument, so they are written as digit strings.Therefore, you should continue to use the classic locale to read the input stream. The output is for the user’s benefit,so the output should use the native locale. Write the program and try it with very large numbers, so the output will be greater than 1000. What does theprogram print as its output? ________________ See Listing 18-3 for my approach to solving this problem.
109EXPLORATION 18 ■ Character Categories
Notice the commas that separate thousands. In some European countries, you might see the following instead: sum = 1.234.567 You should obtain a result that conforms to native customs, or at least follows the preferences that you set in yourhost operating system. When you use the native locale, I recommend defining a variable of type std::locale in which to store it. Youcan pass this variable to isalnum, imbue, or other functions. By creating this variable and distributing copies of it, yourprogram has to query the operating system for your preferences only once, not every time you need the locale. Thus,the main loop ends up looking something like Listing 18-4.
110Exploration 19
Case-Folding
Picking up where we left off in Exploration 18, the next step to improving the word-counting program is to updateit, so that it ignores case differences when counting. For example, the program should count The just as it does the.This is a classic problem in computer programming. C++ offers some rudimentary help but lacks some importantfundamental pieces. This Exploration takes a closer look at this deceptively tricky issue.
Simple CasesWestern European languages have long made use of capital (or majuscule) letters and minuscule letters. The morefamiliar terms—uppercase and lowercase—arise from the early days of typesetting, when the type slugs for majusculeletters were kept in the upper cases of large racks containing all the characters used to make a printing plate. Beneaththem were the cases, or boxes, that stored the minuscule letter slugs. In the <locale> header, C++ declares the isupper and islower functions. They take a character as the firstargument and a locale as the second argument. The return value is a bool: true if the character is uppercase(or lowercase, respectively) and false if the character is lowercase (or uppercase) or not a letter. std::isupper('A', std::locale{""}) == truestd::islower('A', std::locale{""}) == falsestd::isupper('Æ', std::locale{""}) == truestd::islower('Æ', std::locale{""}) == falsestd::islower('½', std::locale{""}) == falsestd::isupper('½', std::locale{""}) == false The <locale> header also declares two functions to convert case: toupper converts lowercase to uppercase.If its character argument is not a lowercase letter, toupper returns the character as is. Similarly, tolower convertsto lowercase, if the character in question is an uppercase letter. Just like the category testing functions, the secondargument is a locale object. Now you can modify the word-counting program to fold uppercase to lowercase and count all words inlowercase. Modify your program from Exploration 18, or start with Listing 18-4. If you have difficulty, take a look atListing 19-1.
111Exploration 19 ■ Case-Folding
int main(){ typedef std::map<std::string, int> count_map; std::locale native{""}; // get the native locale std::cin.imbue(native); // interpret the input and output according to std::cout.imbue(native); // the native locale count_map counts{}; // Read words from the standard input and count the number of times // each word occurs. std::string word{}; while (std::cin >> word) { // Make a copy of word, keeping only alphabetic characters. std::string copy{}; for (char ch : word) if (std::isalnum(ch, native)) copy.push_back(tolower(ch, native)); // The "word" might be all punctuation, so the copy would be empty. // Don't count empty strings. if (not copy.empty()) ++counts[copy]; } // For each word/count pair, print the word & count on one line. for (auto pair : counts) std::cout << pair.first << '\t' << pair.second << '\n';} That was easy. So what’s the problem?
Harder CasesSome of you—especially German readers—already know the problem. Several languages have letter combinationsthat do not map easily between upper and lowercase, or one character maps to two characters. The German Eszett,ß, is a lowercase letter; when you convert it to uppercase, you get two characters: SS. Thus, if your input file contains“ESSEN” and “eßen”, you want them to map to the same word, so they’re counted together, but that just isn’t feasiblewith C++. The way the program currently works, it maps “ESSEN” to “essen”, which it counts as a different word from“eßen”. A naïve solution would be to map “essen” to “eßen”, but not all uses of ss are equivalent to ß. Greek readers are familiar with another kind of problem. Greek has two forms of lowercase sigma: use V at theend of a word and s elsewhere. Our simple program maps S (uppercase sigma) to s, so some words in all uppercasewill not convert to a form that matches its lowercase version. Sometimes, accents are lost during conversion. Mapping é to uppercase usually yields É but may also yield E.Mapping uppercase to lowercase has fewer problems, in that É maps to é, but what if that E (which maps to e) reallymeans É, and you want it to map to é? The program has no way of knowing the writer’s intentions, so all it can do ismap the letters it receives. Some character sets are more problematic than others. For example, ISO 8859-1 has a lowercase ÿ but not anuppercase equivalent (ÿ). Windows 1252, on the other hand, extends ISO 8859-1, and one of the new code points is ÿ.
112 Exploration 19 ■ Case-Folding
■■Tip Code point is a fancy way of saying “numeric value that represents a character.” Although most programmersdon’t use code point in everyday speech, those programmers who work closely with character set issues use it all thetime, so you may as well get used to it. Mainstream programmers should become more accustomed to using this phrase.
In other words, converting case is impossible to do correctly using only the standard C++ library. If you know your alphabet is one that C++ handles correctly, then go ahead and use toupper and tolower. Forexample, if you are writing a command line interpreter, within which you have full control over the commands,and you decide that the user should be able to enter commands in any case, simply make sure the commandsmap correctly from one case to another. This is easy to do, as all character sets can map the 26 letters of the Romanalphabet without any problems. On the other hand, if your program accepts input from the user and you want to map that input to uppercaseor lowercase, you cannot and must not use standard C++. For example, if you are writing a word processor, and youdecide you need to implement some case-folding functions, you must write or acquire a non-standard function toimplement the case-folding logic correctly. Most likely, you would need a library of character and string functions toimplement your word processor. Case-folding would simply be one small part of this hypothetical library. (See thisbook’s web site for some links to non-hypothetical libraries that can help you.) What about our simple program? It isn’t always practical to handle the full, complete, correct handling ofcases and characters when you just want to count a few words. The case-handling code would dwarf theword-counting code. In this case (pun intended), you must accept the fact that your program will sometimes produce incorrect results.Our poor little program will never recognize that “ESSEN” and “eßen” are the same word but in different cases. You canwork around some of the multiple mappings (such as with Greek sigma) by mapping to uppercase then to lowercase.On the other hand, this can introduce problems with some accented characters. And I still have not touched uponthe issue of whether “naïve” is the same word as “naive.” In some locales, the diacritics are significant, which wouldcause “naïve” and “naive” to be interpreted as two different words. In other locales, they are the same word andshould be counted together. In some character sets, accented characters can be composed from separate non-accented characters followedby the desired accent. For example, you can write “nai¨ve”, which is the same as “naïve.” I hope by now you are completely scared away from manipulating cases and characters. Far too many naïveprogrammers become entangled in this web, or worse, simply write bad code. I was tempted to wait until much laterin the book before throwing all this at you, but I know that many readers will want to improve the word-countingprogram by ignoring case, so I decided to tackle the problem early. Well, now you know better. That doesn’t mean you can’t keep working on the word-counting program. The next Exploration returns to therealm of the realistic and feasible, as I finally show you how to write your own functions.
113EXPLORATION 20
Writing Functions
At last, it’s time to embark on the journey toward writing your own functions. In this Exploration, you’ll begin byimproving the word-counting program you’ve been crafting over the past five Explorations, writing functions toimplement separate aspects of the program’s functionality.
FunctionsYou’ve been using functions since the very first program you wrote. In fact, you’ve been writing functions too. You see,main() is a function, and you should view it the same as you would any other function (well, sort of, main() actuallyhas some key differences from ordinary functions, but they needn’t concern you yet). A function has a return type, a name, and parameters in parentheses. Following that is a compound statement,which is the function body. If the function has no parameters, the parentheses are empty. Each parameter is like avariable declaration: type and name. Parameters are separated by commas, so you cannot declare two parametersafter a single type name. Instead, you must specify the type explicitly for each parameter. A function usually has at least one return statement, which causes the function to discontinue execution andreturn control to its caller. A return statement’s structure begins with the return keyword, followed by an expression,and ends with a semicolon, as demonstrated in the following example: return 42; You can use a return statement anywhere you need a statement, and you can use as many return statementsas you need or want. The only requirement is that every execution path through the function must have a returnstatement. Many compilers will warn you if you forget. Some languages distinguish between functions, which return a value, and procedures or subroutines, which donot. C++ calls them all functions. If a function has no return value, declare the return type as void. Omit the value inthe return statements in a void function: return; You can also omit the return statement entirely, if the function returns void. In this circumstance, control returnsto the caller when execution reaches the end of the function body. Listing 20-1 presents some function examples.
115EXPLORATION 20 ■ Writing Functions
{ char c{}; while (std::cin.get(c) and c != '\n') /*empty*/;} /** Prompt the user, then read a number, and ignore the rest of the line. * @param prompt the prompt string * @return the input number or 0 for end-of-file */int prompted_read(std::string prompt){ std::cout << prompt; int x{0}; std::cin >> x; ignore_line(); return x;} /** Print the statistics. * @param count the number of values * @param sum the sum of the values */void print_result(int count, int sum){ if (count == 0) { std::cout << "no data\n"; return; } std::cout << "\ncount = " << count; std::cout << "\nsum = " << sum; std::cout << "\nmean = " << sum/count << '\n';} /** Main program. * Read integers from the standard input and print statistics about them. */int main(){ int sum{0}; int count{0}; while (std::cin) { int x{ prompted_read("Value: ") }; if (std::cin)
116 EXPLORATION 20 ■ Writing Functions
{ sum = sum + x; ++count; } } print_result(count, sum);} What does Listing 20-1 do? _____________________________________________________________ _____________________________________________________________
The ignore_line function reads and discards characters from std::cin until it reaches the end of the line or theend of the file. It takes no arguments and returns no values to the caller. The prompted_read function prints a prompt to std::cout, then reads a number from std::cin. It then discardsthe rest of the input line. Because x is initialized to 0, if the read fails, the function returns 0. The caller cannotdistinguish between a failure and a real 0 in the input stream, so the main() function tests std::cin to know whento terminate the loop. (The value 0 is unimportant; feel free to initialize x to any value.) The sole argument to thefunction is the prompt string. The return type is int, and the return value is the number read from std::cin. The print_result function takes two arguments, both of type int. It returns nothing; it simply prints the results.Notice how it returns early if the input contains no data. Finally, the main() function puts it all together, repeatedly calling prompted_read and accumulating the data.Once the input ends, main() prints the results, which, in this example, are the sum, count, and average of the integersit read from the standard input.
Function CallIn a function call, all arguments are evaluated before the function is called. Each argument is copied to thecorresponding parameter in the function, then the function body begins to run. When the function executes a returnstatement, the value in the statement is copied back to the caller, which can then use the value in an expression,assign it to a variable, and so on. In this book, I try to be careful about terminology: arguments are the expressions in a function call, andparameters are the variables in a function’s header. I’ve also seen the phrase actual argument used for arguments andformal argument used for parameters. I find these confusing, so I recommend you stick with the terms arguments andparameters.
117EXPLORATION 20 ■ Writing Functions
118 EXPLORATION 20 ■ Writing Functions
Counting Words—AgainYour turn. Rewrite the word-counting program (last seen in Exploration 19), this time making use of functions.For example, you can restore the pretty-printing utility by encapsulating it in a single function. Here’s a hint: youmay want to use the typedef names in multiple functions. If so, declare them before the first function, following the#include directives. Test the program to ensure that your changes have not altered its behavior. Compare your program with Listing 20-3.
119EXPLORATION 20 ■ Writing Functions
120 EXPLORATION 20 ■ Writing Functions
121EXPLORATION 20 ■ Writing Functions
One way that main() differs from ordinary functions is immediately obvious. All the main() functions in this booklack a return statement. An ordinary function that returns an int must have at least one return statement, but main()is special. If you don’t supply your own return statement, the compiler inserts a return 0; statement at the end ofmain(). If control reaches the end of the function body, the effect is the same as return 0;, which returns a successstatus to the operating system. If you want to signal an error to the operating system, you can return a non-zero valuefrom main(). How the operating system interprets the value depends on the implementation. The only portablevalues to return are 0, EXIT_SUCCESS, and EXIT_FAILURE. EXIT_SUCCESS means the same thing as 0—namely,success, but its actual value can be different from 0. The names are declared in <cstdlib>. The next Exploration continues to examine functions by taking a closer look at the arguments in function calls.
122EXPLORATION 21
Function Arguments
This Exploration continues the examination of functions introduced in Exploration 20, by focusing on argument-passing.Take a closer look. Remember that arguments are the expressions that you pass to a function in a function call. Parametersare the variables that you declare in the function declaration. This Exploration introduces the topic of function arguments,an area of C++ 11 that is surprisingly complex and subtle.
Argument PassingRead through Listing 21-1 then answer the questions that follow it.
int main(){ int a{42}; modify(a); std::cout << "a=" << a << '\n'; int b{triple(14)}; std::cout << "b=" << b << '\n'; std::vector<int> data{ 10, 20, 30, 40 }; print_vector(data); add(data, 42); print_vector(data);} Predict what the program will print. _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
Now compile and run the program. What does it actually print? _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
Were you correct? ________________ Explain why the program behaves as it does. _____________________________________________________________ _____________________________________________________________
124 EXPLORATION 21 ■ Function Arguments
Rewrite the add function so it returns the modified vector to the caller. _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
Consider what would happen when you call add with a very large vector. The function makes an entirely newcopy of its argument, consuming twice as much memory as it really ought to.
Pass-by-ReferenceInstead of passing large objects (such as vectors) by value, C++ lets you pass them by reference. Add an ampersand(&) after the type name in the function parameter declaration. Change Listing 21-1 to pass vector parameters byreference. Also change the modify function, but leave the other int parameters alone. What do you predict will bethe output? _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
125EXPLORATION 21 ■ Function Arguments
126 EXPLORATION 21 ■ Function Arguments
int b{triple(14)}; std::cout << "b=" << b << '\n'; std::vector<int> data{ 10, 20, 30, 40 }; print_vector(data); add(data, 42); print_vector(data);} When I run the program, I get the following results: a=10b=42{ 10 20 30 40 }{ 52 62 72 82 } This time, the program modified the x parameter in modify and updated the vector’s contents in add. Change the rest of the parameters to use pass-by-reference. What do you expect to happen? _____________________________________________________________ _____________________________________________________________
The compiler does not allow you to call triple(14) when triple’s parameter is a reference. Consider whatwould happen if triple attempted to modify its parameter. You can’t assign to a number, only to a variable. Variablesand literals fall into different categories of expressions. In general terms, a variable is an lvalue, as are references.A literal is called an rvalue, and expressions that are built up from operators and function calls usually result inrvalues. When a parameter is a reference, the argument in the function call must be an lvalue. If the parameter iscall-by-value, you can pass an rvalue. Can you pass an lvalue to a call-by-value parameter? ________________ You’ve seen many examples that you can pass an lvalue. C++ automatically converts any lvalue to an rvalue whenit needs to. Can you convert an rvalue to an lvalue? ________________ If you aren’t sure, try to think of the problem in more concrete terms: can you convert an integer literal to avariable? That means you cannot convert an rvalue to an lvalue. Except, sometimes you can, as the next section willexplain.
const ReferencesIn the modified program, the print_vector function takes its parameter by reference, but it doesn’t modify theparameter. This opens a window for programming errors: you can accidentally write code to modify the vector. Toprevent such errors, you can revert to call-by-value, but you would still have a memory problem if the argument islarge. Ideally, you would be able to pass an argument by reference, but still prevent the function from modifyingits parameter. Well, as it turns out, such a method does exist. Remember const? C++ lets you declare a functionparameter const too.
127EXPLORATION 21 ■ Function Arguments
CONST WARS
Many C++ programmers put the const keyword in front of the type, as demonstrated in the following: void print_vector(const std::vector<int>& v) For simple definitions, the const placement is not critical. For example, to define a named constant, you might use const int max_width{80}; // maximum line width before wrapping The difference between that and int const max_width{80}; // maximum line width before wrapping is small. But with a more complicated declaration, such as the parameter to print_vector, the different style is more significant. I find my technique much easier to read and understand. My rule of thumb is to keep the const keyword as close as possible to whatever it is modifying. More and more C++ programmers are coming around to adopt the const-near-the-name style instead of const out in front. Again, this is an opportunity for you to be in the vanguard of the most up-to-date C++ programming trends. But you have to get used to reading code with const out in front, because you’re going to see a lot of it.
So, v is a reference to a const vector. Because the vector is const, the compiler prevents the print_vectorfunction from modifying it (adding elements, erasing elements, changing elements, and so on). Go ahead and try it.See what happens if you throw in any one of the following lines:
128 EXPLORATION 21 ■ Function Arguments
If a parameter is a reference to const, you can pass an rvalue as an argument. This is the exception that lets youconvert an rvalue to an lvalue. To see how this works, change triple’s parameter to be a reference to const. int triple(int const& x) Convince yourself that you can pass an rvalue (such as 14) to triple. Thus, the more precise rule is that you canconvert an rvalue to a const lvalue, but not to a non-const lvalue.
const_iteratorOne additional trick you have to know when using const parameters: if you need an iterator, use const_iteratorinstead of iterator. A const variable of type iterator is not very useful, because you cannot modify its value, sothe iterator cannot advance. You can still modify the element by assigning to the dereferenced iterator (e.g., *iter).Instead, a const_iterator can be modified and advanced, but when you dereference the iterator, the resulting objectis const. Thus, you can read values but not modify them. This means you can safely use a const_iterator to iterateover a const container. void print_vector(std::vector<int> const& v){ std::cout << "{ "; std::string separator{}; for (std::vector<int>::const_iterator i{v.begin()}, end{v.end()}; i != end; ++i) { std::cout << separator << *i; separator = ", "; } std::cout << "}\n";} You can do the same with a range-based for loop, but I wanted to show the use of const_iterator.
129Exploration 22
Using Algorithms
So far, your use of the standard algorithms has been limited to a few calls to copy, the occasional use of sort, and soon. The main limitation has been that many of the more interesting algorithms require you to supply a function inorder to do anything useful. This Exploration takes a look at these more advanced algorithms. In addition, we’ll revisitsome of the algorithms you already know, to show you how they, too, can be used in a more advanced manner.
Transforming DataSeveral programs that you’ve read and written have a common theme: copying a sequence of data, such as a vectoror string, and applying some kind of transformation to each element (converting to lowercase, doubling the values inan array, and so on). The standard algorithm, transform, is ideal for applying an arbitrarily complex transformation tothe elements of a sequence. For example, recall Listing 10-5, which doubled all the values in an array. Listing 22-1 presents a new way to writethis same program, but using transform.
131Exploration 22 ■ Using Algorithms
std::copy(data.begin(), data.end(), std::ostream_iterator<int>(std::cout, "\n"));} The transform function takes four arguments: the first two specify the input range (as start and one-past-the-end iterators), the third argument is a write iterator, and the final argument is the name of a function. Like otheralgorithms, transform is declared in the <algorithm> header. Regarding the third argument, as usual, it is your responsibility to ensure the output sequence has enough roomto accommodate the transformed data. In this case, the transformed data overwrite the original data, so the start ofthe output range is the same as the start of the input range. The fourth argument is just the name of a function thatyou must have declared or defined earlier in the source file. In this example, the function takes one int parameter andreturns an int. The general rule for a transform function is that its parameter type must match the input type, whichis the type of the element to which the read iterators refer. The return value must match the output type, which is thetype to which the result iterator refers. The transform algorithm calls this function once for each element in the inputrange. It copies to the output range the value returned by the function. Rewriting the word-counting program is a little harder. Recall from Listing 20-3 that the sanitize functiontransforms a string by removing non-letters and converting all uppercase letters to lowercase. The purpose of theC++ standard library is not to provide a zillion functions that cover all possible programming scenarios but, rather,to provide the tools you need to build your own functions with which you can solve your problems. Thus, you wouldsearch the standard library in vain for a single algorithm that copies, transforms, and filters. Instead, you can combinetwo standard functions: one that transforms and one that filters. A further complication, however, is that you know that the filtering and transforming functions will relyon a locale. Solve the problem for now by setting your chosen locale as the global locale. Do this by callingstd::local::global, and passing a locale object as the sole argument. An std::locale object created with thedefault constructor uses the global locale, so after your program sets your chosen locale as the global locale, you caneasily imbue a stream or otherwise access the chosen locale by means of std::locale{}. Any function can use theglobal locale without having to pass locale objects around. Listing 22-2 demonstrates how to rewrite Listing 20-3 toset the global locale to the native locale and then how to use the global locale in the rest of the program.
Listing 22-2. New main Function That Sets the Global Locale#include <iomanip>#include <iostream>#include <locale>#include <map>#include <string> typedef std::map<std::string, int> count_map; ///< Map words to countstypedef count_map::value_type count_pair; ///< pair of a word and a counttypedef std::string::size_type str_size; ///< String size type /** Initialize the I/O streams by imbuing them with * the global locale. Use this function to imbue the streams * with the native locale. C++ initially imbues streams with * the classic locale. */void initialize_streams(){ std::cin.imbue(std::locale{}); std::cout.imbue(std::locale{});}
132 Exploration 22 ■ Using Algorithms
133Exploration 22 ■ Using Algorithms
result.push_back(std::tolower(c, std::locale{})); return result;} /** Main program to count unique words in the standard input. */int main(){ // Set the global locale to the native locale. std::locale::global(std::locale{""}); initialize_streams(); count_map counts{}; // Read words from the standard input and count the number of times // each word occurs. std::string word{}; while (std::cin >> word) { std::string copy{sanitize(word)}; // The "word" might be all punctuation, so the copy would be empty. // Don't count empty strings. if (not copy.empty()) ++counts[copy]; } print_counts(counts);} Now it’s time to rewrite the sanitize function to take advantage of algorithms. Use transform to convertcharacters to lowercase. Use remove_if to get rid of nonalphabetic characters from the string. The remove_ifalgorithm calls a function for each element of a sequence. If the function returns true, remove_if eliminates thatelement from the sequence—well, it kind of does. One curious side effect of how iterators work in C++ is that the remove_if function does not actually eraseanything from the sequence. Instead, it rearranges the elements and returns an iterator that points to the position onepast the end of the elements to be preserved. You can then call the erase member function to delete the elements thatwere removed, or you can make sure your function keeps track of the new logical end of the sequence. Figure 22-1 illustrates how remove_if works with a before and after view of a string. Notice how the remove_iffunction does not alter the size of the string, but the characters after the new end are not meaningful. They are junk.
134 Exploration 22 ■ Using Algorithms
135Exploration 22 ■ Using Algorithms
{ // Remove all non-letters from the string, and then erase them. str.erase(std::remove_if(str.begin(), str.end(), non_letter), str.end()); // Convert the remnants of the string to lowercase. std::transform(str.begin(), str.end(), str.begin(), lowercase); return str;} The erase member function takes two iterators as arguments and erases all the elements within that range. Theremove_if function returns an iterator that points to one-past-the-end of the new string, which means it also pointsto the first position of the elements to be erased. Passing str.end() as the end of the range instructs erase to get rid ofall the removed elements. The remove/erase idiom is common in C++, so you should get used to seeing it. The standard library has severalremove-like functions, all of which work the same way. It takes a little while to get used to this approach, but once youdo, you will find it quite easy to use.
PredicatesThe non_letter function is an example of a predicate. A predicate is a function that returns a bool result. Thesefunctions have many uses in the standard library. For example, the sort function sorts values in ascending order. What if you wanted to sort data in descendingorder? The sort function lets you provide a predicate to compare items. The ordering predicate (call it pred) mustmeet the following qualifications: • pred(a, a) must be false (a common error is to implement <= instead of <, which violates this requirement). • If pred(a, b) is true, and pred(b, c) is true, then pred(a, c) must also be true. • The parameter types must match the element type to be sorted. • The return type must be bool or something that C++ can convert automatically to bool. If you don’t provide a predicate, sort uses the < operator as the default. Write a predicate to compare two integers for sorting in descending order. _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
136 Exploration 22 ■ Using Algorithms
_____________________________________________________________ _____________________________________________________________
Run the new program with a variety of inputs. What actually happens? _____________________________________________________________ _____________________________________________________________
137Exploration 22 ■ Using Algorithms
Other AlgorithmsThe standard library contains too many useful algorithms to cover in this book, but I’ll take a few moments in thissection to introduce you to at least some of them. Refer to a comprehensive language reference to learn about theother algorithms. Let’s explore algorithms by looking for palindromes. A palindrome is a word or phrase that reads the sameforward and backward, ignoring punctuation, such as, for example:
The program reads one line of text at a time by calling the getline function. This function reads from an inputstream into a string, stopping when it reads a delimiter character. The default delimiter is '\n', so it reads one line oftext. It does not skip over initial or trailing white space. The first step is to remove non-letter characters, but you already know how to do that. The next step is to test whether the resulting string is a palindrome. The reverse function transposes the order ofelements in a range, such as characters in a string. The equal function compares two sequences to determine whether they are the same. It takes two iteratorsfor the first range and a starting iterator for the second range. It assumes the two ranges are the same size, and itcompares one element at a time and works for any kind of sequence. In this case, the comparison must be case-insensitive, so provide a predicate that converts all text to a canonical case and then compares them. Go ahead. Write the program. A simple web search should deliver up some juicy palindromes with which to testyour program. If you don’t have access to the Web, try the following: Eve Deed Hannah Leon saw I was Noel If you need some hints, here are my recommendations: • Write a function called is_palindrome that takes a string as a parameter and returns a bool. • This function uses the remove_if function to clean up the string. • There’s no need to erase the removed elements, however. Instead, save the end iterator returned by remove_if and use that instead of str.end(). • Define a new string, rev, which is a copy of the sanitized string. Copy only the desired part of the test string by taking only up to the iterator that was returned from remove_if. • Next, call reverse. • Write a predicate, is_same_char, to compare two characters after converting them both to uppercase and then to lowercase. • Call the equal function with same_char as its predicate, to compare the original string with the reversed copy. • The main program sets the global locale to the native locale and imbues the input and output streams with the new global locale. • The main program calls getline(std::cin, line) until the function returns false (meaning error or end-of-file), then calls is_palindrome for each line.
138 Exploration 22 ■ Using Algorithms
139Exploration 22 ■ Using Algorithms
int main(){ std::locale::global(std::locale{""}); std::cin.imbue(std::locale{}); std::cout.imbue(std::locale{}); std::string line{}; while (std::getline(std::cin, line)) if (is_palindrome(line)) std::cout << line << '\n';} In a large program, the predicate or transformation function might be declared far from where it is used.Often, a predicate is used only once. By defining a function just for that predicate, it makes your program harder tounderstand. The human reader must read all the code to ensure that the predicate truly is called in only one place.It would be nice if C++ offered a way to write the predicate in the place where it is used, and thereby avoid theseproblems. Read the next Exploration to learn how to achieve this in C++ 11.
140Exploration 23
Unnamed Functions
One problem with calling algorithms is that sometimes the predicate or transformation function must be declaredfar from the place where it is called. With a properly descriptive name, this problem can be reduced, but often thefunction is trivial, and your program would be much easier to read if you could put its functionality directly in the callto the standard algorithm. A new feature in C++ 11 permits exactly that.
LambdasC++ 11 lets you define a function as an expression. You can pass this function to an algorithm, save it in a variable, orcall it immediately. Such a function is called a lambda, for reasons that only a computer scientist would understandor even care about. If you aren’t a computer scientist, don’t worry, and just realize that when the nerds talk aboutlambdas, they are just talking about unnamed functions. As a quick introduction, Listing 23-1 rewrites Listing 22-1 touse a lambda.
141Exploration 23 ■ Unnamed Functions
That’s right, the function’s return type. When a lambda’s function body contains only a single return statement,the compiler deduces the function’s type from the type of the return expression. In this case, the return type is int. With a lambda, the program is slightly shorter and much easier to read. You don’t have to hunt for the definitionof times_two() to learn what it does. (Not all functions are named so clearly.) But lambdas are even more powerfuland can do things that ordinary functions can’t. Take a look at Listing 23-2 to see what I mean.
The first number is the multiplier, and the remaining numbers are multiplied by it, yielding 48121620 See the trick? The lambda is able to read the local variable, multiplier. A separate function, such as times_two()in Listing 22-1, can’t do that. Of course, you can pass two arguments to times_two(), but this use of the transformalgorithm calls the function with only one argument. There are ways to work around this limitation, but I won’t bothershowing them to you, because lambdas solve the problem simply and elegantly.
142 Exploration 23 ■ Unnamed Functions
Now run the program. Was your prediction correct? ________________ Why or why not? _____________________________________________________________ _____________________________________________________________
The value of multiplier was captured by value when the lambda was defined. Thus, changing the value ofmultiplier later does not change the lambda, and it still multiplies by three. The transform() algorithm is calledtwice, so the effect is to multiply by 9, not 60. If you need your lambda to keep track of the local variable and always use its most recent value, you can capturea variable by reference by prefacing its name with an ampersand (similar to a reference function parameter), as in thefollowing example: [&multiplier](int i) { return i * multiplier; }; Modify Listing 23-3 to capture multiplier by reference. Run the program to observe its new behavior. You can choose to omit the capture name to capture all local variables. Use an equal sign to capture everything byvalue or just an ampersand to capture everything by reference. int x{0}, y{1}, z{2};auto capture_all_by_value = [=]() { return x + y + z; };auto capture_all_by_reference = [&]() { x = y = z = 0; }; I advise against capturing everything by default, because it leads to sloppy code. Be explicit about the variablesthat the lambda captures. The list of captures should be short, or else you are probably doing something wrong.Nonetheless, you will likely see other programmers capture everything, if only out of laziness, so I had to show you thesyntax. If you follow best practices and list individual capture names, the default is capture-by-value, so you must supplyan ampersand for each name that you want to capture by reference. Feel free to mix capture-by-value andcapture-by-reference. auto lambda = [by_value, &by_reference, another_by_value, &another_by_reference]() { by_reference = by_value; another_by_reference = another_by_value; };
144 Exploration 23 ■ Unnamed Functions
const CaptureCapture-by-value has one trick up its sleeve that can take you by surprise. Consider the simple program inListing 23-4.
_____________________________________________________________
You already know that function parameters are call-by-value, so the y = 1 assignment has no effect outside ofthe lambda, and local remains 0. A by-value capture is similar in that you cannot change the local variable that iscaptured (x in Listing 23-4). But the compiler is even pickier than that. It doesn’t let you write the assignment x = 1.It’s as though every by-value capture were declared const. Lambdas are different from ordinary functions in that default for by-value captures is const, and to get a non-const capture, you must explicitly tell the compiler. The keyword to use is mutable, which you put after the functionparameters, as shown in Listing 23-5.
145Exploration 23 ■ Unnamed Functions
Now the compiler lets you assign to the capture, x. The capture is still by-value, so x in main() doesn’t change.The output of the program is 3, 0, 0 So far, I have never found an instance when I wanted to use mutable. It’s there if you need it, but you will probablynever need it.
Return TypeThe return type of a lambda is the type of the return expression, if the lambda body contains only a return statement.But what if the lambda is more complicated? Although some argue that the compiler should be able to deduce thetype from a more complicated lambda (with the restriction that every return statement have the same type), that is notwhat the standard says. Future updates to the standard will loosen this restriction, but as I write this, the restrictionremains in place. But the syntax for a lambda does not lend itself to declaring a function return type in the usual way. Instead, thereturn type follows the function parameter list, with an arrow (->) between the closing parenthesis and the returntype: [](int i) -> int { return i * 2; } In general, the lambda is easier to read without the explicit return type. The return type is usually obvious, but if itis not, go ahead and be explicit. Clarity trumps brevity. Rewrite Listing 22-5 to take advantage of lambdas. Write functions where you think functions are appropriate,and write lambdas where you think lambdas are appropriate. Compare your solution with mine in Listing 23-6.
146 Exploration 23 ■ Unnamed Functions
147Exploration 24
In C++, multiple functions can have the same name, provided the functions have a different number of arguments ordifferent argument types. Using the same name for multiple functions is called overloading and is common in C++.
OverloadingAll programming languages use overloading at one level or another. For example, most languages use + for integeraddition as well as for floating-point addition. Some languages, such as Pascal, use different operators for integerdivision (div) and floating-point division (/), but others, such as C and Java, use the same operator (/). C++ takes overloading one step further, letting you overload your own function names. Judicious use ofoverloading can greatly reduce complexity in a program and make your programs easier to read and understand. For example, C++ inherits several functions from the standard C library that compute an absolute value: abstakes an int argument; fabs takes a floating-point argument; and labs takes a long integer argument.
■■Note Don’t be concerned that I have not yet covered these other types. All that matters for the purpose of thisdiscussion is that they are distinct from int. The next Exploration will begin to examine them more closely, so please bepatient.
C++ also has its own complex type for complex numbers, which has its own absolute value function. In C++,however, they all have the same name, std::abs. Using different names for different types merely clutters the mentallandscape and contributes nothing to the clarity of the code. The sort function, just to name one example, has two overloaded forms: std::sort(start, end);std::sort(start, end, compare); The first form sorts in ascending order, comparing elements with the < operator, and the second form compareselements by calling compare. Overloading appears in many other places in the standard library. For example, whenyou create a locale object, you can copy the global locale by passing no arguments std::isalpha('X', std::locale{}); or create a native locale object by passing an empty string argument std::isalpha('X', std::locale{""}); 149Exploration 24 ■ Overloading Function Names
Overloading functions is easy, so why not jump in? Write a set of functions, all named print. They all have avoid return type and take various parameters.
• One takes an int as a parameter. It prints the parameter to the standard output. • Another takes two int parameters. It prints the first parameter to the standard output and uses the second parameter as the field width. • Another takes a vector<int> as the first parameter, followed by three string parameters. Print the first string parameter, then each element of the vector (by calling print), with the second string parameter between elements, and the third string parameter after the vector. If the vector is empty, print the first and third string parameters only. • Another has the same parameters as the vector form, but also takes an int as the field width for each vector element.
Write a program to print vectors using the print functions. Compare your functions and program with mine inListing 24-1.
150 Exploration 24 ■ Overloading Function Names
151Exploration 24 ■ Overloading Function Names
152 Exploration 24 ■ Overloading Function Names
153Exploration 24 ■ Overloading Function Names
If you look closely at to_upper and to_lower, you’ll notice a couple of techniques that are different from other,similar functions. Can you spot them? If so, what are they? _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
The range-based for loops work with references! Like a pass-by-reference function parameter, the declaration char& ch is a reference to a character in the range. Thus, assigning to ch changes the character in the string. Note thatauto can also declare a reference. If each element of the range is large, you should use references to avoid makingunnecessary copies. Use a const reference if you do not have to modify the element, as follows: for (auto const& big_item : container_full_of_big_things) The other anomaly is that to_lower and to_upper string functions do not take const references but take plainstrings as parameters. This means the argument is passed by value, which in turn means the compiler arranges tocopy the string when passing the argument to the function. The function requires the copy, so this technique helps thecompiler generate optimal code for copying the argument, and it saves you a step in writing the function. It’s a smalltrick, but a useful one. This technique will be especially useful later in the book—so don’t forget it. The is_alpha string function does not modify its parameter, so it can take a const reference. A common use of overloading is to overload a function for different types, including different integer types, suchas a long integer. The next Exploration takes a look at these other types.
154Exploration 25
Another common use for overloading is to write functions that work just as well with large and small integers as withplain integers. C++ has five different integer types, ranging in size from 8 bits to 64 bits or larger, with several choicesfor sizes in between. This Exploration takes a look at the details.
155Exploration 25 ■ Big and Little Numbers
else std::cout << std::numeric_limits<int>::digits << '\n'; std::cout << "bits per bool = "; if (std::numeric_limits<bool>::is_signed) std::cout << std::numeric_limits<bool>::digits + 1 << '\n'; else std::cout << std::numeric_limits<bool>::digits << '\n';}
Long IntegersSometimes, you need more bits than int can handle. In this case, add long to the definition to get a long integer. long int lots_o_bits{2147483647}; You can even drop the int, as shown in the following: long lots_o_bits{2147483647}; The standard guarantees that a long int can handle numbers in the range –2,147,483,647 to 2,147,483,647, butan implementation can choose a large size. C++ does not guarantee that a long int is actually longer than a plainint. On some platforms, int might be 32 bits and long might be 64. When I first used a PC at home, an int was 16 bitsand long was 32 bits. At times, I’ve used systems for which int and long were both 32 bits. I’m writing this book on amachine that uses 32 bits for int and 64 bits for long. The type long long int can be even bigger, with a range of at least –9,223,372,036,854,775,807 to9,223,372,036,854,775,807. You can drop the int, if you wish, and programmers often do. Use long long if you want to store numbers as large as possible and are willing to pay a small performancepenalty (on some systems, or a large penalty on others). Use long if you have to ensure portability and must representnumbers outside the range ±32,767 (which is all C++ guarantees for type int).
Short IntegersSometimes, you don’t have the full range of an int, and reducing memory consumption is more important. In thiscase, use a short int, or just short, which has a guaranteed range of at least –32,767 to 32,767, inclusive. This is thesame guaranteed range as int, but implementations often choose to make an int larger than the minimum and keepshort in this range of 16 bits. But you must not assume that int is always larger than short. Both may be the exactsame size. As is done with long, you define a type as short int or short. short int answer{42};short zero{0}; Modify Listing 25-1 to print the number of bits in a long and a short too. How many bits are in a long on yoursystem? ______________ How many in a short? ________________ long long? ___________ When I run the program in Listing 25-2, I get 16 bits in a short, 32 in an int, and 64 in a long and long long. Onanother computer in my network, I get 16 bits in a short, 32 in an int and long, and 64 in a long long.
156 Exploration 25 ■ Big and Little Numbers
Listing 25-2. Revealing the Number of Bits in Short and Long Integers#include <iostream>#include <limits> int main(){ std::cout << "bits per int = "; if (std::numeric_limits<int>::is_signed) std::cout << std::numeric_limits<int>::digits + 1 << '\n'; else std::cout << std::numeric_limits<int>::digits << '\n'; std::cout << "bits per bool = "; if (std::numeric_limits<bool>::is_signed) std::cout << std::numeric_limits<bool>::digits + 1 << '\n'; else std::cout << std::numeric_limits<bool>::digits << '\n'; std::cout << "bits per short int = "; if (std::numeric_limits<short>::is_signed) std::cout << std::numeric_limits<short>::digits + 1 << '\n'; else std::cout << std::numeric_limits<short>::digits << '\n'; std::cout << "bits per long int = "; if (std::numeric_limits<long>::is_signed) std::cout << std::numeric_limits<long>::digits + 1 << '\n'; else std::cout << std::numeric_limits<long>::digits << '\n'; std::cout << "bits per long long int = "; if (std::numeric_limits<long long>::is_signed) std::cout << std::numeric_limits<long long>::digits + 1 << '\n'; else std::cout << std::numeric_limits<long long>::digits << '\n';}
Integer LiteralsWhen you write an integer literal (that is, an integer constant), the type depends on its value. If the value fits in an int,the type is int; otherwise, the type is long or long long. You can force a literal to have type long by adding l or L (theletter L in lowercase or uppercase) after the digits. (Curiously, C++ has no way for you to type a short literal.) I alwaysuse uppercase L because a lowercase l looks too much like the digit 1. The compiler can always tell the difference, butevery year it gets a little harder for me to see the difference between 1 and l. Use two consecutive L’s for a long long. Devise a way for a program to print int=, followed by the value, for an int literal; print long=, followed by thevalue, for a long literal; and print long long=, followed by the value, for a long long literal. (Hint: What was thetopic of the previous Exploration?) Write a program to demonstrate your idea, and test it with some literals. If you can,run the program on platforms that use different sizes for int and long. Compare your program to that of Listing 25-3.
157Exploration 25 ■ Big and Little Numbers
Byte-Sized IntegersThe smallest integer type that C++ offers is signed char. The type name looks similar to the character type, char, butthe type acts differently. It usually acts like an integer. By definition, the size of signed char is 1 byte, which is thesmallest size that your C++ compiler supports for any type. The guaranteed range of signed char is –127 to 127. In spite of the name, you should try not to think of signed char as a mutated character type; instead, think of it asa misspelled integer type. Many programs have a typedef similar to typedef signed char byte;
158 Exploration 25 ■ Big and Little Numbers
to make it easier for you to think of this type as a byte-sized integer type. There is no easy way to write a signed char literal, just as there is no way to write a simple short literal.Character literals have type char, not signed char. Besides, some characters may be out of range for signed char. Although the compiler does its best to help you remember that signed char is not a char, the standard library isless helpful. The I/O stream types treat signed char values as characters. Somehow, you have to inform the streamthat you want to print an integer, not a character. You also need a solution to create signed char (and short) literals.Fortunately, the same solution lets you use signed char constants and print signed char numbers: type casting.
Type CastingAlthough you cannot write a short or arbitrary signed char literal directly, you can write a constant expression thathas type short or signed char and take any suitable value. The trick is to use a plain int and tell the compiler exactlywhat type you want. static_cast<signed char>(-1)static_cast<short int>(42) The expression does not have to be a literal, as demonstrated in the following: int x{42};static_cast<short>(x); The static_cast expression is known as a type cast. The operator, static_cast, is a reserved keyword. Itconverts an expression from one type to another. The “static” in its name means the type is static, or fixed, at compiletime. You can convert any integer type to any other integer type. If the value is out of range for the target type, you getjunk as a result. For example, the high-order bits may be discarded. Thus, you should always be careful when usingstatic_cast. Be absolutely sure that you are not discarding important information. If you cast a number to bool, the result is false if the number is zero or true if the number is not zero (just likethe conversion that takes place when you use an integer as a condition). Rewrite Listing 25-3 to overload print for short and signed char values too. Use type casting to force variousvalues to different types and ensure that the results match your expectations. Take a look at Listing 25-4 to see onepossible solution.
159Exploration 25 ■ Big and Little Numbers
160 Exploration 25 ■ Big and Little Numbers
Integer ArithmeticWhen you use signed char and short values or objects in an expression, the compiler always turns them into typeint. It then performs the arithmetic or whatever operation you want to do. This is known as type promotion.The compiler promotes a short to an int. The result of arithmetic operations is also an int. You can mix int and long in the same expressions. C++ converts the smaller type to match the larger type, andthe larger type is the type of the result. This is known as type conversion, which is different from type promotion.(The distinction may seem arbitrary or trivial, but it’s important. The next section will explain one of the reasons.)Remember: Promote signed char and short to int; convert int to long. long big{2147483640};short small{7};std::cout << big + small; // promote small to type int; then convert it to long; // the sum has type long When you compare two integers, the same promotion and conversion occurs: the smaller argument is promotedor converted to the size of the larger argument. The result is always bool. The compiler can convert any numeric value to bool; it considers this a conversion on the same level as any otherinteger conversion.
161Exploration 25 ■ Big and Little Numbers
Overload ResolutionThe two-step type conversion process may puzzle you. It matters when you have a set of overloaded functions, andthe compiler has to decide which function to call. The first thing the compiler tries is to find an exact match. If itcan’t find one, it searches for a match after type promotion. Only if that fails does it search for a match allowing typeconversion. Thus, it considers a match based only on type promotion to be better than type conversion. Listing 25-6demonstrates the difference.
162 Exploration 25 ■ Big and Little Numbers
163Exploration 26
Even the longest long long cannot represent truly large numbers, such as Avogadro’s number (6.02↕1023) or extremelysmall numbers, such as the mass of an electron (9.1↕10–31 kg). Scientists and engineers use scientific notation, whichconsists of a mantissa (such as 6.02 or 9.1) and an exponent (such as 23 or –31), relative to a base (10). Computers represent very large and very small numbers using a similar representation, known as floating-point.I know many of you have been waiting eagerly for this Exploration, as you’ve probably grown tired of using onlyintegers, so let’s jump in.
Floating-Point NumbersComputers use floating-point numbers for very large and very small values. By sacrificing precision, you can gain agreatly extended range. However, never forget that the range and precision are limited. Floating-point numbers arenot the same as mathematical real numbers, although they can often serve as useful approximations of real numbers. Like its scientific notation counterpart, a floating-point number has a mantissa, also called a significand, asign, and an exponent. The mantissa and exponent use a common base or radix. Although integers in C++ arealways binary in their representation, floating-point numbers can use any base. Binary is a popular base, but somecomputers use 16 or even 10 as the base. The precise details are, as always, dependent upon the implementation. Inother words, each C++ implementation uses its native floating-point format for maximum performance. Floating-point values often come in multiple flavors. C++ offers single, double, and extended precision, calledfloat, double, and long double, respectively. The difference is that float usually has less precision and a smallerrange than double, and double usually has less precision and smaller range than long double. In exchange, longdouble usually requires more memory and computation time than double, which usually consumes more memoryand computation time than float. On the other hand, an implementation is free to use the same representation for allthree types. Use double, unless there is some reason not to. Use float when memory is at a premium and you can affordto lose precision or long double when you absolutely need the extra precision or range and can afford to give upmemory and performance. A common binary representation of floating-point numbers is the IEC 60559 standard, which is better knownas IEEE 754. Most likely, your desktop system has hardware that implements the IEC 60559 standard. For the sakeof convenience, the following discussion describes only IEC 60559; however, never forget that C++ permits manyfloating-point representations. Mainframes and DSPs, for example, often use other representations. An IEC 60559 float occupies 32 bits, of which 23 bits make up the mantissa and 8 bits form the exponent,leaving one bit for the mantissa’s sign. The radix is 2, so the range of an IEC 60559 float is roughly 2–127 to 2127, or 10–38to 1038. (I lied. Smaller numbers are possible, but the details are not germane to C++. If you are curious, look updenormalization in your favorite computer science reference.)
165Exploration 26 ■ Very Big and Very Little Numbers
The IEC 60559 standard reserves some bit patterns for special values. In particular, if the exponent is all one bits,and the mantissa is all zero bits, the value is considered “infinity.” It’s not quite a mathematical infinity, but it does itsbest to pretend. Adding any finite value to infinity, for example, yields an answer of infinity. Positive infinity is alwaysgreater than any finite value, and negative infinity is always smaller than finite values. If the exponent is all one bits, and the mantissa is not all zero bits, the value is considered as not-a-number,or NaN. NaN comes in two varieties: quiet and signaling. Arithmetic with quiet NaN always yields an NaN result.Using a signaling NaN results in a machine interrupt. How that interrupt manifests itself in your program is up tothe implementation. In general, you should expect your program to terminate abruptly. Consult your compiler’sdocumentation to learn the details. Certain arithmetic operations that have no meaningful result can also yield NaN,such as adding positive infinity to negative infinity. Test whether a value is NaN by calling std::isnan (declared in <cmath>). Similar functions exist to test for infinityand other properties of floating-point numbers. A double is similar in structure to a float, except it takes up 64 bits: 52 bits for the mantissa, 11 bits for theexponent, and 1 sign bit. A double can also have infinity and NaN values, with the same structural representation(that is, exponent all ones). A long double is even longer than double. The IEC 60559 standard permits an extended double-precision formatthat requires at least 79 bits. Many desktop and workstation systems implement extended-precision, floating-pointnumbers using 80 bits (63 for the mantissa, 16 for the exponent, and 1 sign bit).
Floating-Point LiteralsAny numeric literal with a decimal point or a decimal exponent represents a floating-point number. The decimalpoint is always '.', regardless of locale. The exponent starts with the letter e or E and can be signed. No spaces arepermitted in a numeric literal. For example: 3.141592653589731415926535897e-130.000314159265e4 By default, a floating-point literal has type double. To write a float literal, add the letter f or F after the number.For a long double, use the letter l or L, as in the following examples: 3.141592f31415926535897E-13l0.000314159265E+420L As with long int literals, I prefer uppercase L, to avoid confusion with the digit 1. Feel free to use f or F, but Irecommend you pick one and stick with it. For uniformity with L, I prefer to use F. If a floating-point literal exceeds the range of the type, the compiler will tell you. If you ask for a value at greaterprecision than the type supports, the compiler will silently give you as much precision as it can. Another possibility isthat you request a value that the type cannot represent exactly. In that case, the compiler gives you the next higher orlower value. For example, your program may have the literal 0.2F, which seems like a perfectly fine real number, but as abinary floating-point value, it has no exact representation. Instead, it is approximately 0.00110011002. The differencebetween the decimal value and the internal value can give rise to unexpected results, the most common of which iswhen you expect two numbers to be equal and they are not. Read Listing 26-1 and predict the outcome.
166 Exploration 26 ■ Very Big and Very Little Numbers
_____________________________________________________________
Floating-Point TraitsYou can query numeric_limits to reveal the size and limits of a floating-point type. You can also determine whetherthe type allows infinity or NaN. Listing 26-2 shows some code that displays information about a floating-point type.
167Exploration 26 ■ Very Big and Very Little Numbers
typedef float T; std::cout << "min=" << std::numeric_limits<T>::min() << '\n' << "max=" << std::numeric_limits<T>::max() << '\n' << "IEC 60559? " << std::numeric_limits<T>::is_iec559 << '\n' << "max exponent=" << std::numeric_limits<T>::max_exponent << '\n' << "min exponent=" << std::numeric_limits<T>::min_exponent << '\n' << "mantissa places=" << std::numeric_limits<T>::digits << '\n' << "radix=" << std::numeric_limits<T>::radix << '\n' << "has infinity? " << std::numeric_limits<T>::has_infinity << '\n' << "has quiet NaN? " << std::numeric_limits<T>::has_quiet_NaN << '\n' << "has signaling NaN? " << std::numeric_limits<T>::has_signaling_NaN << '\n'; if (std::numeric_limits<T>::has_infinity) { T zero{0}; T one{1}; T inf{std::numeric_limits<T>::infinity()}; if (one/zero == inf) std::cout << "1.0/0.0 = infinity\n"; if (inf + inf == inf) std::cout << "infinity + infinity = infinity\n"; } if (std::numeric_limits<T>::has_quiet_NaN) { // There's no guarantee that your environment produces quiet NaNs for // these illegal arithmetic operations. It's possible that your compiler's // default is to produce signaling NaNs, or to terminate the program // in some other way. T zero{}; T inf{std::numeric_limits<T>::infinity()}; std::cout << "zero/zero = " << zero/zero << '\n'; std::cout << "inf/inf = " << inf/inf << '\n'; }} Modify the program so it prints information about double. Run it. Modify it again for long double, and run it.Do the results match your expectations? ________________
Floating-Point I/OReading and writing floating-point values depend on the locale. In the classic locale, the input format is the same asfor an integer or floating-point literal. In a native locale, you must write the input according to the rules of the locale.In particular, the decimal separator must be that of the locale. Thousands-separators are optional, but if you use them,you must use the locale-specific character and correct placement. Output is more complicated. In addition to the field width and fill character, floating-point output also depends on the precision—the numberof places after the decimal point—and the format, which can be fixed-point (without an exponent), scientific(with an exponent), or general (uses an exponent only when necessary). The default is general. Depending on thelocale, the number may include separators for groups of thousands.
168 Exploration 26 ■ Very Big and Very Little Numbers
In the scientific and fixed formats (which you specify with a manipulator of the same name), the precision is thenumber of digits after the decimal point. In the general format, it is the maximum number of significant digits. Set thestream’s precision with the precision member function or setprecision manipulator. The default precision is six.As usual, the manipulators that do not take arguments are declared in <ios>, so you get them for free with <iostream>,but setprecision requires that you include <iomanip>. double const pi{3.141592653589792};std::cout.precision(12);std::cout << pi << '\n';std::cout << std::setprecision(4) << pi << '\n'; In scientific format, the exponent is printed with a lowercase 'e' (or 'E', if you use the uppercase manipulator),followed by the base 10 exponent. The exponent always has a sign (+ or -), and at least two digits, even if the exponentis zero. The mantissa is written with one digit before the decimal point. The precision determines the number of placesafter the decimal point. In fixed format, no exponent is printed. The number is printed with as many digits before the decimal point asneeded. The precision determines the number of places after the decimal point. The decimal point is always printed. The default format is the general format, which means printing numbers nicely without sacrificing information.If the exponent is less than or equal to –4, or if it is greater than the precision, the number is printed in scientificformat. Otherwise, it is printed without an exponent. However, unlike conventional fixed-point output, trailing zerosare removed after the decimal point. If after removal of the trailing zeros the decimal point becomes the last character,it is also removed. When necessary, values are rounded off to fit within the allotted precision. A new format in C++ 11 is hexfloat. The value is printed in hexadecimal, which lets you discover the exact valueon systems with binary or base 16 representations. Because the letter 'e' is a valid hexadecimal value, the exponent ismarked with the letters 'p' or 'P'. The easiest way to specify a particular output format is with a manipulator: scientific, fixed, or hexfloat. Likethe precision, the format persists in the stream’s state until you change it. (Only width resets after an output operation.)Unfortunately, once you set the format, there is no easy way to revert to the default general format. To do that, you mustuse a member function, and a clumsy one at that, as shown in the following: std::cout << std::scientific << large_number << '\n';std::cout << std::fixed << small_number << '\n';std::cout.unsetf(std::ios_base::floatfield);std::cout << number_in_general_format << '\n'; Complete Table 26-1, showing exactly how each value would be printed in each format, in the classic locale.I filled in the first row for your convenience.
169Exploration 26 ■ Very Big and Very Little Numbers
After you have filled in the table with your predictions, write a program that will test your predictions, then runit and see how well you did. Compare your program with Listing 26-3.
170 Exploration 26 ■ Very Big and Very Little Numbers
Some applications are never required to use floating-point numbers; others need them a lot. Scientists andengineers, for example, depend on floating-point arithmetic and math functions and must understand the subtletiesof working with these numbers. C++ has everything you need for computation-intensive programming. Althoughthe details are beyond the scope of this book, interested readers should consult a reference for the <cmath> headerand the transcendental and other functions that it provides. The <cfenv> header contains functions and relateddeclarations to let you adjust the rounding mode and other aspects of the floating-point environment. If you cannotfind information about <cfenv> in a C++ reference, consult a C 99 reference for the <fenv.h> header. The next Exploration takes a side trip to a completely different topic, explaining the strange comments—the extraslashes (///) and stars (/**)—that I’ve used in so many programs.
171Exploration 27
Documentation
This Exploration is a little different from the others. Instead of covering part of the C++ standard, it examinesa third-party tool called Doxygen. Feel free to skip this Exploration, but understand that this is where I explain thestrange comments you sometimes see in the code listings.
DoxygenDoxygen is a free (in cost and license) tool that reads your source code, looks for comments that follow a certainstructure, and extracts information from the comments and from the code to produce documentation. It producesoutput in a number of formats: HTML, RTF (rich text format), LaTeX, UNIX man pages, and XML. Java programmers may be familiar with a similar tool called javadoc. The javadoc tool is standard in the JavaSoftware Development Kit, whereas Doxygen has no relationship with the C++ standard or with any C++ vendor.C++ lacks a standard for structured documentation, so you are free to do anything you want. For example, Microsoftdefines its own conventions for XML tags in comments, which is fine, if you plan to work entirely within the Microsoft.NET environment. For other programmers, I recommend using tools that have more widespread and portable use.The most popular solution is Doxygen, and I think every C++ programmer should know about it, even if you decidenot to use it. That’s why I include this Exploration in the book.
Structured CommentsDoxygen heeds comments that follow a specific format. • One-line comments start with an extra slash or exclamation mark: /// or //! • Multiline comments start with an extra asterisk or exclamation mark: /** or /*! Also, Doxygen recognizes some widespread comment conventions and decorations. For example, it ignores a lineof slashes. //////////////////////////////////////////////////////////////////////////////////// A multiline comment can begin with a row full of asterisks. /*********************************************************************************** And a line in a multiline comment can begin with an asterisk. /********************************************************************************** * This is a structured comment for Doxygen. * *********************************************************************************/ 173Exploration 27 ■ Documentation
Within a structured comment is where you document the various entities in your program: functions, types,variables, and so on. The convention is that the comment immediately before a declaration or definition applies to the entity beingdeclared or defined. Sometimes, you want to put the comment after the declaration, such as a one-line description ofa variable. To do that, use a “less-than” (<) sign at the start of the comment. double const c = 299792458.0; ///< speed of light in m/sec
@b wordMark up word in boldface. You can also use HTML markup, <b>phrase</b>, which is helpful when phrase containsspaces, or use Markdown, by enclosing the text in asterisks: *phrase*.
@brief one-sentence-descriptionDescribe an entity briefly. Entities have brief and detailed documentation. Depending on how you configure Doxygen,the brief documentation can be the first sentence of the entity’s full documentation, or you can require an explicit@brief tag. In either case, the rest of the comment is the detailed documentation for the entity.
@c wordTreat word as a code fragment and set it in a fixed-pitch typeface. You can also use HTML markup, <tt>phrase</tt>,or use backticks for Markdown, `phrase`.
@em wordEmphasize word in italics. You can also use HTML tags, <em>phrase</em>, or underscores for Markdown: _phrase_.
174 Exploration 27 ■ Documentation
@mainpage titlePresent an overview of the entire project for the index or cover page. You can put @mainpage in any source file, or evenset aside a separate file just for the comment. In small projects, I place @mainpage in the same source file as the mainfunction, but in large projects, I use a separate file.
@p nameSet name in a fixed-pitch typeface to distinguish it as a function parameter.
@par titleStart a new paragraph. If you supply a one-line title, it becomes the paragraph heading. A blank line also separatesparagraphs.
@post postconditionDocument a postcondition for a function. A postcondition is a Boolean expression that you can assume to be truewhen the function returns (assuming all preconditions were true). C++ lacks any formal mechanism for enforcingpostconditions (other than assert), so documenting postconditions is crucial, especially for library writers.
@pre preconditionDocument a precondition for a function. A precondition is a Boolean expression that must be true before the functionis called, or else the function is not guaranteed to work properly. C++ lacks any formal mechanism for enforcingpreconditions (other than assert), so documenting preconditions is crucial, especially for library writers.
@return descriptionDocument what a function returns.
@see xrefInsert a cross-reference to an entity named xref. Doxygen looks for references to other documented entities withinthe structured comment. When it finds one, it inserts a hyperlink (or text cross-reference, depending on theoutput format). Sometimes, however, you have to insert an explicit reference to an entity that is not named in thedocumentation, so you can use @see. You can suppress automatic hyperlink creation by prefacing a name with %.
175Exploration 27 ■ Documentation
176 Exploration 27 ■ Documentation
177Exploration 27 ■ Documentation
std::cin.imbue(std::locale{}); std::cout.imbue(std::locale{}); std::string line{}; while (std::getline(std::cin, line)) if (is_palindrome(line)) std::cout << line << '\n';} Figure 27-1 shows the main page as it appears in a web browser.
178 Exploration 27 ■ Documentation
Using DoxygenInstead of taking lots of command-line arguments, Doxygen uses a configuration file, typically named Doxyfile,in which you can put all that juicy information. Among the information in the configuration file are the name of theproject, which files to examine for comments, which output format or formats to generate, and a variety of options youcan use to tweak and adjust the output. Because of the plethora of options, Doxygen comes with a wizard, doxywizard, to help generate a suitableconfiguration file, or you can just run the command line doxygen utility with the -g switch, to generate a defaultconfiguration file that has lots of comments to help you understand how to customize it. Once you have configured Doxygen, running the program is trivial. Simply run doxygen, and away it goes.Doxygen does a reasonable job at parsing C++, which is a complicated and difficult language to parse, but itsometimes gets confused. Pay attention to the error messages, to see if it had any difficulties with your source files. The configuration file dictates the location of the output. Typically, each output format resides in its ownsubdirectory. For example, the default configuration file stores HTML output in the html directory. Open thehtml/index.html file in your favorite browser, to check out the results. Download and install Doxygen on your system. Add Doxygen comments to one of your programs. Configure and run Doxygen. Future programs will continue to use Doxygen comments sporadically, when I think the comments help youunderstand what the program does. In general, however, I try to avoid them in the book, because the text usuallyexplains things well enough, and I don’t want to waste any space. The programs that accompany the book, however,have more complete Doxygen comments.
179Exploration 28
It’s project time! Body-mass index (BMI) is a measurement some health-care professionals use to determine whethera person is overweight, and if so, by how much. To compute BMI, you need a person’s weight in kilograms and heightin meters. The BMI is simply weight/height2, converted to a unitless value. Your task is to write a program that reads records, prints the records, and computes some statistics. The programshould start by asking for a threshold BMI. Only records with a BMI greater than or equal to the threshold will beprinted. Each record needs to consist of a person’s name (which may contain spaces), weight in kilograms, height incentimeters (not meters), and the person’s sex ('M' or 'F'). Let the user enter the sex in uppercase or lowercase.Ask the user to enter the height in centimeters, so you can compute the BMI using integers. You will have to adjust theformula to allow for centimeters instead of meters. Print each person’s BMI immediately after reading his or her record. After collecting information for everyone,print two tables—one for men, one for women—based on the data. Use an asterisk after the BMI rating to markrecords for which the number meets or exceeds the threshold. After each table, print the mean (average) and medianBMI. (Median is the value at which half the BMI values are less than the median and half are greater than the median.If the user enters an even number of records, take the mean of the two values in the middle.) Compute individual BMIvalues as integers. Compute the mean and median BMI values as floating-point numbers, and print the mean withone place after the decimal point. Listing 28-1 shows a sample user session. User input is in boldface.
181Exploration 28 ■ Project 1: Body-Mass Index
HintsHere are some hints, in case you need them. • Keep track of the data in separate vectors, e.g., heights, weights, sexes, names, bmis. • Use the native locale for all input and output. • Divide the program into functions, e.g., compute_bmi to compute the BMI from weight and height. • You can write this program using nothing more than the techniques that we have covered so far, but if you know other techniques, feel free to use them. The next set of Explorations will present language features that will greatly facilitate writing this kind of program. • The complete source code for my solution is available with the other files that accompany this book, but don’t peek until after you have written the program yourself.
182Exploration 29
Custom Types
One of the key design goals for C++ was that you should be able to define brand-new types that look and act similarto the built-in types. Do you need tri-state logic? Write your own tribool type. Need arbitrary-precision arithmetic?Write your own bigint type. This Exploration introduces some of the language features that let you define customtypes. Subsequent Explorations delve deeper into these topics.
183Exploration 29 ■ Custom Types
The definition starts with the struct keyword. C programmers recognize this as a structure definition—but hangon, there’s much more to follow. The contents of the rational type look like definitions for variables named numerator and denominator, but theywork a little differently. Remember that Listing 29-1 shows a type definition. In other words, the compiler remembersthat rational names a type, but it does not allocate any memory for an object, for numerator, or for denominator.In C++ parlance, numerator and denominator are called data members; some other languages call them instancevariables or fields. Notice the semicolon that follows the closing curly brace. Type definitions are different from compoundstatements. If you forget the semicolon, the compiler will remind you, sometimes quite rudely, while referring to a linenumber several lines after the one where the semicolon belongs. When you define an object with type rational, the object stores the numerator and denominator members. Use thedot (.) operator to access the members (which you have been doing throughout this book), as shown in Listing 29-2.
Member FunctionsLet’s add a member function to rational that reduces the numerator and denominator by their greatest commondivisor. Listing 29-3 shows the sample program, with the reduce() member function.
184 Exploration 29 ■ Custom Types
while (m != 0) { int tmp{n % m}; n = m; m = tmp; } return n;} /// Represent a rational number.struct rational{ /// Reduce the numerator and denominator by their GCD. void reduce() { assert(denominator != 0); int div{gcd(numerator, denominator)}; numerator = numerator / div; denominator = denominator / div; } int numerator; ///< numerator gets the sign of the rational value int denominator; ///< denominator is always positive}; int main(){ rational pi{}; pi.numerator = 1420; pi.denominator = 452; pi.reduce(); std::cout << "pi is approximately " << pi.numerator << "/" << pi.denominator << '\n';} Notice how the reduce() member function looks just like an ordinary function, except its definition appearswithin the rational type definition. Also notice how reduce() can refer to the data members of rational. When youcall the reduce() member function, you must supply an object as the left-hand operand of the dot (.) operator (suchas pi in Listing 29-3). When the reduce() function refers to a data member, the data member is taken from thatleft-hand operand. Thus, numerator = numerator / div has the effect of pi.numerator = pi.numerator/div. The gcd function is a free function. You can call it with any two integers, unrelated to rational numbers. I couldhave defined gcd as a member function, but nothing about the function ties it to rational numbers. It does not accessany members of rational. By making it a free function, you can reuse it throughout your program, anywhere youhave to compute the greatest common divisor of two integers. If you aren’t sure whether a function should be freeor a member, err on the side of making it free.
185Exploration 29 ■ Custom Types
A member function can also call other member functions that are defined in the same type. Try it yourself: addthe assign() member function, which takes a numerator and denominator as two parameters and assigns them totheir respective data members and calls reduce(). This spares the user of rational the additional step (and possibleerror of neglecting the call to reduce()). Let the return type be void. Write your member function below: _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ Listing 29-4 presents the entire program, with my assign member function in boldface.
186 Exploration 29 ■ Custom Types
ConstructorsWouldn’t it be nice to be able to initialize a rational object with a numerator and denominator, and have themproperly reduced automatically? You can do that by writing a special member function that looks and acts a little likeassign, except the name is the same as the name of the type (rational), and the function has no return type or returnvalue. Listing 29-5 shows how to write this special member function.
187Exploration 29 ■ Custom Types
188 Exploration 29 ■ Custom Types
Modify the rational type so it accepts a negative denominator. If the denominator is negative, change it topositive and also change the sign of the numerator. Thus, rational{-710, -227} would have the same value asrational{710, 227}. You can choose to perform the modification in any one of a number of places. Good software design practicedictates that the change should occur in exactly one spot, and all other functions should call that one. Therefore,I suggest modifying reduce(), as shown in Listing 29-6.
Listing 29-6. Modifying the reduce Member Function to Accept a Negative Denominator /// Reduce the numerator and denominator by their GCD. void reduce() { assert(denominator != 0); if (denominator < 0) { denominator = -denominator; numerator = -numerator; } int div{gcd(numerator, denominator)}; numerator = numerator / div; denominator = denominator / div; }
Overloading ConstructorsYou can overload a constructor the same way you overload an ordinary function. All the overloaded constructorshave the same name (that is, the name of the type), and they must differ in the number or type of the parameters. Forexample, you can add a constructor that takes a single integer argument, implicitly using 1 as the denominator. You have a choice of two ways to write an overloaded constructor. You can have one constructor call another one,or you can have the constructor initialize all the members, just like the first constructor you wrote. To call anotherconstructor, use the type name in the initializer list. rational(int num) : rational{num, 1} {} Or initialize each member directly. rational(int num): numerator{num}, denominator{1}{} When you want multiple constructors to share a common behavior, delegating the work to a commonconstructor makes a lot of sense. For example, you can have a single constructor call reduce(), and every otherconstructor can call that one constructor, thereby ensuring that no matter how you construct the rational object, youknow it has been reduced. On the other hand, when the denominator is 1, there is no need to call reduce(), so you might prefer the secondform, initializing the data members directly. The choice is yours. A constructor that calls another constructor is known as a delegating constructor because it delegates its work tothe other constructor. I’m sure you can see many deficiencies in the current state of the rational type. It has several that you probablymissed too. Hang on; the next Exploration starts improving the type. For example, you may want to test yourmodification by comparing two rational objects, to see if they are equal. To do so, however, you have to write acustom == operator, which is the subject of the next Exploration.
189Exploration 30
Overloading Operators
This Exploration continues the study of writing custom types. An important aspect of making a custom type behaveseamlessly with built-in types is ensuring that the custom types support all the expected operators—arithmetic typesmust support arithmetic operators, readable and writable types must support I/O operators, and so on. Fortunately,C++ lets you overload operators in much the same manner as overloading functions.
191Exploration 30 ■ Overloading Operators
{ numerator = num; denominator = den; reduce(); } /// Reduce the numerator and denominator by their GCD. void reduce() { assert(denominator != 0); if (denominator < 0) { denominator = -denominator; numerator = -numerator; } int div{gcd(numerator, denominator)}; numerator = numerator / div; denominator = denominator / div; } int numerator; ///< numerator gets the sign of the rational value int denominator; ///< denominator is always positive}; /// Compare two rational numbers for equality./// @pre @p a and @p b are reduced to normal formbool operator==(rational const& a, rational const& b){ return a.numerator == b.numerator and a.denominator == b.denominator;} /// Compare two rational numbers for inequality./// @pre @p a and @p b are reduced to normal formbool operator!=(rational const& a, rational const& b){ return not (a == b);} One of the benefits of reducing all rational numbers is that it makes comparison easier. Instead of checkingwhether 3/3 is the same as 6/6, the constructor reduces both numbers to 1/1, so it is just a matter of comparing thenumerators and denominators. Another trick is defining != in terms of ==. There’s no point in your making extrawork for yourself, so confine the actual logic of comparing rational objects to one function and call it from anotherfunction. If you worry about the performance overhead of calling an extra layer of functions, use the inline keyword,as shown in Listing 30-2.
Listing 30-4. Implementing the > and >= Operators in Terms of </// Compare two rational numbers for greater-than.inline bool operator>(rational const& a, rational const& b){ return b < a;} /// Compare two rational numbers for greater-than-or-equal.inline bool operator>=(rational const& a, rational const& b){ return not (b > a);} Then write a test program. To help you write your tests, download the test.hpp file and add #include "test.hpp" to your program. Call the TEST() function as many times as you need, passing a Boolean expression as thesole argument. If the argument is true, the test passed. If the argument is false, the test failed, and the TEST functionprints a suitable message. Thus, you may write tests, such as the following:TEST(rational{2, 2} == rational{5,5});TEST(rational{6,3} > rational{10, 6});
193Exploration 30 ■ Overloading Operators
The all-capital name, TEST, tells you that TEST is different from an ordinary function. In particular, if the test fails, thetext of the test is printed as part of the failure message. How the TEST function works is beyond the scope of this book,but it’s useful to have around; you’ll be using it for future test harnesses. Compare your test program with Listing 30-5.
Arithmetic OperatorsComparison is fine, but arithmetic operators are much more interesting. You can overload any or all of the arithmeticoperators. Binary operators take two parameters, and unary operators take one parameter. You can choose any returntype that makes sense. Listing 30-6 shows the binary addition operator and the unary negation operator.
194 Exploration 30 ■ Overloading Operators
195Exploration 30 ■ Overloading Operators
It turns out that rational’s one-argument constructor tells the compiler it can construct a rational from an intanytime it needs to do so. It does so automatically, so the compiler sees the integer 3 and a multiplication of an intand a rational object. It knows about operator* between two rationals, and it knows it cannot use the built-in *operator with a rational operand. Thus, the compiler decides its best response is to convert the int to a rational(by invoking rational{3}), and then it can apply the custom operator* that multiplies two rational objects, yieldinga rational result, namely, rational{1, 1}. It does all this automatically on your behalf. Listing 30-8 illustrates oneway to write the test program.
Listing 30-8. Test Program for Multiplying an Integer and a Rational Number#include <cassert>#include <cstdlib>#include <iostream> ... struct rational omitted for brevity ... int main(){ rational result{3 * rational{1, 3}}; std::cout << result.numerator << '/' << result.denominator << '\n';} Being able to construct a rational object automatically from an int is a great convenience. You can easily writecode that performs operations on integers and rational numbers without concerning yourself with type conversionsall the time. You’ll find this same convenience when mixing integers and floating-point numbers. For example, youcan write 1+2.0 without having to perform a type cast: static_cast<double>(1)+2.0. On the other hand, all this convenience can be too convenient. Try to compile the following code sample andsee what your compiler reports. int a(3.14); // which one is okay,int b{3.14}; // and which is an error? You have seen that parentheses are needed when initializing an auto declaration, and we use curly braceseverywhere else. In truth, you can use parentheses for many of these initializations, but you pay a cost in safety. Thecompiler allows conversions that lose information, such as floating-point to integer, when you use parentheses forinitialization, but it reports an error when you use curly braces. The difference is critical for the rational type. Initializing a rational number using a floating-point number inparentheses truncates the number to an integer and uses the one-argument form of constructor. This isn’t what youwant at all. Instead, initializing rational{3.14} should produce the same result as rational{314, 100}. Writing a high-quality conversion from floating-point to a fraction is beyond the scope of this book. Instead, let’sjust pick a reasonable power of 10 and use that as the denominator. Say we choose 100000, then rational{3.14159}would be treated as rational{static_cast<int>(3.14159 * 10000), 10000}. Write the constructor for afloating-point number. I recommend using a delegating constructor; that is, write the floating-point constructorso it invokes another constructor. Compare your result with mine in Listing 30-9. A better solution uses numeric_limits to determine the numberof decimal digits of precision double can support, and tries to preserve as much precision as possible. An even bettersolution uses the radix of the floating-point implementation, instead of working in base 10.
196 Exploration 30 ■ Overloading Operators
Math FunctionsThe C++ standard library offers a number of mathematical functions, such as std::abs, which computes absolutevalues (as you have already guessed). The C++ standard prohibits you from overloading these standard functions tooperate on custom types, but you can still write functions that perform similar operations. In Exploration 52, you’lllearn about namespaces, which will enable you to use the real function name. Whenever you write a custom numerictype, you should consider which math functions you should provide. In this case, absolute value makes perfect sense.Write an absolute value function that works with rational numbers. Call it absval. Your absval function should take a rational parameter by value and return a rational result. As with thearithmetic operators I wrote, you may opt to use call-by-reference for the parameter. If so, make sure you declare thereference to be const. Listing 30-11 shows my implementation of absval.
197Exploration 30 ■ Overloading Operators
198 Exploration 30 ■ Overloading Operators
199Exploration 31
Wouldn’t it be nice to be able to read and write rational numbers directly, for example, std::cout << rational{355,113}? In fact, C++ has everything you need, although the job is a little trickier than perhaps it should be. ThisExploration introduces some of the pieces you need to accomplish this.
Input OperatorThe I/O operators are just like any other operators in C++, and you can overload them the way you overload any otheroperator. The input operator, also known as an extractor (because it extracts data from a stream), takes std::istream&as its first parameter. It must be a non-const reference, because the function will modify the stream object. Thesecond parameter must also be a non-const reference, because you will store the input value there. By convention,the return type is std::istream&, and the return value is the first parameter. That lets you combine multiple inputoperations in a single expression. (Go back to Listing 17-3 for an example.) The body of the function must do the work of reading the input stream, parsing the input, and deciding how tointerpret that input. Proper error handling is difficult, but the basics are easy. Every stream has a state mask that keepstrack of errors. Table 31-1 lists the available state flags (declared in <ios>).
Flag Descriptionbadbit Unrecoverable erroreofbit End of filefailbit Invalid input or outputgoodbit No errors
If the input is not valid, the input function sets failbit in the stream’s error state. When the caller tests whetherthe stream is okay, it tests the error state. If failbit is set, the check fails. (The test also fails if an unrecoverable erroroccurs, such as a hardware malfunction, but that’s not pertinent to the current topic.) Now you have to decide on a format for rational numbers. The format should be one that is flexible enough for ahuman to read and write easily but simple enough for a function to read and parse quickly. The input format must beable to read the output format and might be able to read other formats too. Let’s define the format as an integer, a slash (/), and another integer. White space can appear before or after anyof these elements, unless the white space flag is disabled in the input stream. If the input contains an integer that isnot followed by a slash, the integer becomes the resulting value (that is, the implicit denominator is 1). The input
201Exploration 31 ■ Custom I/O Operators
operator has to “unread” the character, which may be important to the rest of the program. The unget() memberfunction does exactly that. The input operator for integers will do the same thing: read as many characters as possibleuntil reading a character that is not part of the integer, then unget that last character. Putting all these pieces together requires a little care, but is not all that difficult. Listing 31-1 presents the inputoperator. Add this operator to the rest of the rational type that you wrote in Exploration 30.
Output OperatorWriting the output operator, or inserter (so named because it inserts text into the output stream), has a number ofhurdles, due to the plethora of formatting flags. You want to heed the desired field width and alignment, and you haveto insert fill characters, as needed. Like any other output operator, you want to reset the field width but not change anyother format settings.
202 Exploration 31 ■ Custom I/O Operators
One way to write a complicated output operator is to use a temporary output stream that stores its text in a string.The std::ostringstream type is declared in the <sstream> header. Use ostringstream the way you would use anyother output stream, such as cout. When you are done, the str() member function returns the finished string. To write the output operator for a rational number, create an ostringstream, and then write the numerator,separator, and denominator. Next, write the resulting string to the actual output stream. Let the stream itselfhandle the width, alignment, and fill issues when it writes the string. If you had written the numerator, slash, anddenominator directly to the output stream, the width would apply only to the numerator, and the alignment wouldbe wrong. Similar to an input operator, the first parameter has type std::ostream&, which is also the return type. Thereturn value is the first parameter. The second parameter can use call-by-value, or you can pass a reference to const,as you can see in Listing 31-2. Add this code to Listing 31-1 and the rest of the rational type that you are defining.
Error StateThe next step is to write a test program. Ideally, the test program should be able to continue when it encounters aninvalid-input error. So now is a good time to take a closer look at how an I/O stream keeps track of errors. As you learned earlier in this Exploration, every stream has a mask of error flags (see Table 31-1). You can testthese flags, set them, or clear them. Testing the flags is a little unusual, however, so pay attention. The way most programs in this book test for error conditions on a stream is to use the stream itself or an inputoperation as a condition. As you learned, an input operator function returns the stream, so these two approaches areequivalent. A stream converts to a bool result by returning the inverse of its fail() function, which returns true,if failbit or badbit are set. In the normal course of an input loop, the program progresses until the input stream is exhausted. The streamsets eofbit when it reaches the end of the input stream. The stream’s state is still good, in that fail() returns false,so the loop continues. However, the next time you try to read from the stream, it sees that no more input is available,sets failbit, and returns an error condition. The loop condition is false, and the loop exits. The loop might also exit if the stream contains invalid input, such as non-numeric characters for integer input,or the loop can exit if there is a hardware error on the input stream (such as a disk failure). Until now, the programs inthis book didn’t bother to test why the loop exited. To write a good test program, however, you have to know the cause. First, you can test for a hardware or similar error by calling the bad() member function, which returns true if badbitis set. That means something terrible happened to the file, and the program can’t do anything to fix the problem. Next, test for normal end-of-file by calling the eof() member function, which is true only when eofbit is set. Ifbad() and eof() are both false and fail() is true, this means the stream contains invalid input. How your programshould handle an input failure depends on your particular circumstances. Some programs must exit immediately;others may try to continue. For example, your test program can reset the error state by calling the clear() memberfunction, then continue running. After an input failure, you may not know the stream’s position, so you don’t knowwhat the stream is prepared to read next. This simple test program skips to the next line.
203Exploration 31 ■ Custom I/O Operators
Listing 31-3 demonstrates a test program that loops until end-of-file or an unrecoverable error occurs. If theproblem is merely invalid input, the error state is cleared, and the loop continues.
204Exploration 32
The final step needed to complete this stage of the rational type is to write assignment operators and to improve theconstructors. It turns out C++ does some work for you, but you often want to fine-tune that work. Let’s find out how.
Assignment OperatorUntil now, all the rational operators have been free functions. The assignment operator is different. The C++standard requires that it be a member function. One way to write this function is shown in Listing 32-1.
205Exploration 32 ■ Assignment and Initialization
Because *this is the object itself, another way to refer to members is to use the dot operator (e.g.,(*this).numerator) instead of the basic numerator. Another way to write (*this).numerator is this->numerator.The meaning is the same; the alternative syntax is mostly a convenience. Writing this-> is not necessary for thesesimple functions, but it’s often a good idea. When you read a member function, and you have troublediscerning the members from the nonmembers, that’s a signal that you have to help the reader by usingthis-> before all the member names. Listing 32-2 shows the assignment operator with explicit use of this->.
ConstructorsThe compiler also writes a constructor automatically, specifically one that constructs a rational object by copyingall the data members from another rational object. This is called a copy constructor. Any time you pass a rationalargument by value to a function, the compiler uses the copy constructor to copy the argument value to the parameter.Any time you define a rational variable and initialize it with the value of another rational value, the compilerconstructs the variable by calling the copy constructor.
206 Exploration 32 ■ Assignment and Initialization
As with the assignment operator, the compiler’s default implementation is exactly what we would write ourselves,so there is no need to write the copy constructor. As with an assignment operator, you can state explicitly that youwant the compiler to supply its default copy constructor. rational(rational const&) = default; If you don’t write any constructors for a type, the compiler also creates a constructor that takes no arguments,called a default constructor. The compiler uses the default constructor when you define a variable of a custom typeand do not provide an initializer for it. The compiler’s implementation of the default constructor merely calls thedefault constructor for each data member. If a data member has a built-in type, the member is left uninitialized.In other words, if we did not write any constructors for rational, any rational variable would be uninitialized, soits numerator and denominator would contain garbage values. That’s bad—very bad. All the operators assume therational object has been reduced to normal form. They would fail if you passed an uninitialized rational object tothem. The solution is simple: don’t let the compiler write its default constructor. Instead, you write one. All you have to do is write any constructor at all. This will prevent the compiler from writing its own defaultconstructor. (It will still write its own copy constructor.) Early on, we wrote a constructor for the rational type, but it was not a default constructor. As a result, you could notdefine a rational variable and leave it uninitialized or initialize it with empty braces. (You may have run into that issuewhen writing your own test program.) Uninitialized data is a bad idea, and having default constructors is a good idea. Sowrite a default constructor to make sure a rational variable that has no initializer has a well-defined value nonetheless.What value should you use? I recommend zero, which is in keeping with the spirit of the default constructors for typessuch as string and vector. Write a default constructor for rational to initialize the value to zero. Compare your solution with mine, which is presented in Listing 32-4.
207Exploration 32 ■ Assignment and Initialization
m = tmp; } return n;} /// Represent a rational number (fraction) as a numerator and denominator.struct rational{ rational() : numerator{0}, denominator{1} {/*empty*/} rational(int num) : numerator{num}, denominator{1} {/*empty*/} rational(int num, int den) : numerator{num}, denominator{den} { reduce(); } rational(double r) : rational{static_cast<int>(r * 10000), 10000} {/*empty*/} rational& operator=(rational const& that) { this->numerator = that.numerator; this->denominator = that.denominator; reduce(); return *this; } float as_float() { return static_cast<float>(numerator) / denominator; } double as_double() { return static_cast<double>(numerator) / denominator; } long double as_long_double() { return static_cast<long double>(numerator) / denominator; } /// Assign a numerator and a denominator, then reduce to normal form.
208 Exploration 32 ■ Assignment and Initialization
209Exploration 32 ■ Assignment and Initialization
210 Exploration 32 ■ Assignment and Initialization
{ int n{0}, d{0}; char sep{'\0'}; if (not (in >> n >> sep)) // Error reading the numerator or the separator character. in.setstate(in.failbit); else if (sep != '/') { // Push sep back into the input stream, so the next input operation // will read it. in.unget(); rat.assign(n, 1); } else if (in >> d) // Successfully read numerator, separator, and denominator. rat.assign(n, d); else // Error reading denominator. in.setstate(in.failbit); return in;} /// Write a rational numbers./// Format is @em numerator @c / @em denominator.std::ostream& operator<<(std::ostream& out, rational const& rat){ std::ostringstream tmp{}; tmp << rat.numerator << '/' << rat.denominator; out << tmp.str(); return out;} I encourage you to add tests to the program in Listing 30-5, to exercise all the latest features of the rational class.Make sure everything works the way you expect it. Then put aside rational for the next Exploration, which takes acloser look at the foundations of writing custom types.
211Exploration 33
Writing Classes
The rational type is an example of a class. Now that you’ve seen a concrete example of writing your own class, it’stime to understand the general rules that govern all classes. This Exploration and the next four lay the foundation forthis important aspect of C++ programming.
Anatomy of a ClassA class has a name and members—data members, member functions, and even member typedefs and nested classes.You start a class definition with the struct keyword. (You might wonder why you would not start a class definition withthe class keyword. Please be patient; all will become clear in Exploration 35.) Use curly braces to surround the body ofthe class definition, and the definition ends with a semicolon. Within the curly braces, you list all the members. Declaredata members in a manner similar to a local variable definition. You write member functions in the same manner asyou would a free function. Listing 33-1 shows a simple class definition that contains only data members.
213Exploration 33 ■ Writing Classes
When you define a variable using a class type, the compiler sets aside enough memory so the variable canstore its own copy of every data member of the class. For example, define an object with type point, and the objectcontains the x and y members. Define another object of type point, and that object contains its own, separatex and y members. Use the dot (.) operator to access the members, as you have been doing throughout this book. The object is theleft-hand operand, and the member name is the right-hand operand, as shown in Listing 33-3.
Member FunctionsIn addition to data members, you can have member functions. Member function definitions look very similar toordinary function definitions, except you define them as part of a class definition. Also, a member function can callother member functions of the same class and can access data members of the same class. Listing 33-4 shows somemember functions added to class point.
215Exploration 33 ■ Writing Classes
NO LEADING UNDERSCORE
If you want to use only an underscore to denote members, use it as a suffix, not a prefix. The C++ standard sets aside certain names and prohibits you from using them. The actual rules are somewhat lengthy, because C++ inherits a number of restrictions from the C standard library. For example, you should not use any name that begins with E and is followed by a digit or an uppercase letter. (That rule seems arcane, but the C standard library defines several error code names, such as ERANGE, for a range error in a math function. This rule lets the library add new names in the future and lets those who implement libraries add vendor-specific names.) I like simplicity, so I follow three basic rules. These rules are slightly more restrictive than the official C++ rules, but not in any burdensome way: • Do not use any name that contains two consecutive underscores (like__this). • Do not use any name that starts with an underscore (_like_this). • Do not use any name that is all uppercase (LIKE_THIS). Using a reserved name results in undefined behavior. The compiler may not complain, but the results are unpredictable. Typically, a standard library implementation must invent many additional names for its internal use. By defining certain names that the application programmer cannot use, C++ ensures the library-writer can use these names within the library. If you accidentally use a name that conflicts with an internal library name, the result could be chaos or merely a subtle shift in a function’s implementation.
ConstructorAs you learned in Exploration 29, a constructor is a special member function that initializes an object’s data members.You saw several variations on how to write a constructor, and now it’s time to learn a few more. When you declare a data member, you can also provide an initializer. The initializer is a default value that thecompiler uses in case a constructor does not initialize that member. Use the normal initialization syntax of providing avalue or values in curly braces. struct point { int x{1}; int y{}; point() {} // initializes x to 1 and y to 0}; Use this style of initializing data members only when a particular member needs a single value in all or nearlyall constructors. By separating the initial value from the constructor, it makes the constructor harder to read andunderstand. The human reader must read the constructor and the data member declarations to know how the objectis initialized. On the other hand, using default initializers is a great way to ensure that data members of built-in type,such as int, are always initialized. Recall that constructors can be overloaded, and the compiler chooses which constructor to call, based on thearguments in the initializer. I like to use curly braces to initialize an object. The values in the curly braces are passed tothe constructor in the same manner as function arguments to an ordinary function. In fact, C++ 03 used parenthesesto initialize an object, so an initializer looked very much like a function call. C++ 11 still allows this style of initializer,and you saw that when using an auto declaration, you must use parentheses. But in all other cases, curly braces arebetter. Exploration 30 demonstrated that curly braces provide greater type safety.
216 Exploration 33 ■ Writing Classes
Another key difference with curly braces is that you can initialize a container, such as a vector, with a series ofvalues in curly braces, as follows: std::vector<int> data{ 1, 2, 3 }; This introduces a problem. The vector type has several constructors. For example, a two-argument constructorlets you initialize a vector with many copies of a single value. A vector with ten zeroes, for example, can be initializedas follows: std::vector<int> ten_zeroes(10, 0); Note that I used parentheses. What if I used curly braces? Try it. What happens? ______________________________________________________________ _____________________________________________________________ The vector is initialized with two integers: 10 and 0. The rule is that containers treat curly braces as a series ofvalues with which to initialize the container contents. Curly braces can be used in a few other cases, such as copying acontainer, but in general, you have to use parentheses to call any of the other constructors. Write a constructor almost the same way you would an ordinary member function, but with a few differences. • Omit the return type. • Use plain return; (return statements that do not return values). • Use the class name as the function name. • Add an initializer list after a colon to initialize the data members. An initializer can also invoke another constructor, passing arguments to that constructor. Delegating construction to a common constructor is a great way to ensure rules are properly enforced by all constructors. Listing 33-5 shows several examples of constructors added to class point.
217Exploration 33 ■ Writing Classes
Initialization is one of the key differences between class types and built-in types. If you define an object ofbuilt-in type without an initializer, you get a garbage value, but objects of class type are always initialized by calling aconstructor. You always get a chance to initialize the object’s data members. The difference between built-in types andclass types are also evident in the rules C++ uses to initialize data members in a constructor. A constructor’s initializer list is optional, but I recommend you always provide it, unless every data memberhas an initializer. The initializer list appears after a colon, which follows the closing parenthesis of the constructor’sparameter list; it initializes each data member in the same order in which you declare them in the class definition,ignoring the order in the initializer list. To avoid confusion, always write the initializer list in the same order as thedata members. Member initializers are separated by commas and can spill onto as many lines as you need. Eachmember initializer provides the initial value of a single data member or uses the class name to invoke anotherconstructor. List the member name, followed by its initializer in curly braces. Initializing a data member is the same asinitializing a variable and follows the same rules. If you don’t write any constructors for your class, the compiler writes its own default constructor. The compiler’sdefault constructor is just like a constructor that omits an initializer list. struct point { point() {} // x_ is initialized to 0, and y_ is uninitialized double x_{}; double y_;}; When the compiler writes a constructor for you, the constructor is implicit. If you write any constructor, thecompiler suppresses the implicit default constructor. If you want a default constructor in that case, you must write ityourself. In some applications, you may want to avoid the overhead of initializing the data members of point, becauseyour application will immediately assign a new value to the point object. Most of the time, however, caution is best. A copy constructor is one that takes a single argument of the same type as the class, passed by reference. Thecompiler automatically generates calls to the copy constructor when you pass objects by value to functions, orwhen functions return objects. You can also initialize a point object with the value of another point object, and thecompiler generates code to invoke the copy constructor. point pt1; // default constructorpoint p2{pt1}; // copy constructor If you don’t write your own copy constructor, the compiler writes one for you. The automatic copy constructorcalls the copy constructor for every data member, just like the one in Listing 33-5. Because I wrote one that is exactlylike the one the compiler writes implicitly, there is no reason to write it explicitly. Let the compiler do its job. To help you visualize how the compiler calls constructors, read Listing 33-6. Notice how it prints a message foreach constructor use.
218 Exploration 33 ■ Writing Classes
{ std::cout << "copy constructor(" << x_ << ")\n"; } int x_;}; demo addone(demo d){ ++d.x_; return d;} int main(){ demo d1{}; demo d2{d1}; demo d3{42}; demo d4{addone(d3)};} Predict the output from running the program in Listing 33-6. _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ Check your prediction. Were you correct? ________________
The compiler is allowed to perform some minor optimizations when passing arguments to functions andaccepting return values. For example, instead of copying a demo object to the addone return value, and then copyingthe return value to initialize d4, the C++ standard permits compilers to remove unnecessary calls to the copyconstructor. Not all compilers perform this optimization, and not all do so in the same manner. Most compilersrequire a command line switch or project option to be set before it optimizes. Thus, the exact number of calls tothe copy constructor can vary slightly from one compiler or platform to another, or from one set of command lineswitches to another. When I run the program, I get the following: constructor(0)default constructorcopy constructor(0)constructor(42)copy constructor(42)copy constructor(43)
219Exploration 33 ■ Writing Classes
220Exploration 34
Member functions and constructors are even more fun than what you’ve learned so far. This Exploration continuesto uncover their mysteries.
Revisiting Project 1What did you find most frustrating about Project 1 (Exploration 28)? If you are anything like me (although I hopeyou’re not, for your own sake), you may have been disappointed that you had to define several separate vectors tostore one set of records. However, without knowing about classes, that was the only feasible approach. Now thatyou’ve been introduced to classes, you can fix the program. Write a class definition to store one record. Refer backto Exploration 28 for details. To summarize, each record keeps track of an integer height in centimeters, an integerweight in kilograms, the calculated BMI (which you can round off to an integer), the person’s sex (letter 'M' or 'F'),and the person’s name (a string). Next, write a read member function that reads a single record from an istream. It takes two arguments: anistream and an integer. Prompt the user for each piece of information by writing to std::cout. The integer argumentis the record number, which you can use in the prompts. Write a print member function that prints one record; ittakes an ostream and an integer threshold as arguments. Finally, modify the program to take advantage of the new class you wrote. Compare your solution to mine,shown in Listing 34-1.
221Exploration 34 ■ More About Member Functions
{ in.ignore(std::numeric_limits<int>::max(), '\n');} /// Represent one person's record, storing the person's name, height, weight,/// sex, and body-mass index (BMI), which is computed from the height and weight.struct record{ record() : height_{0}, weight_{0}, bmi_{0}, sex_{'?'}, name_{} {} /// Get this record, overwriting the data members. /// Error-checking omitted for brevity. /// @return true for success or false for eof or input failure bool read(std::istream& in, int num) { std::cout << "Name " << num << ": "; std::string name{}; if (not std::getline(in, name)) return false; std::cout << "Height (cm): "; int height{}; if (not (in >> height)) return false; skip_line(in); std::cout << "Weight (kg): "; int weight{}; if (not (in >> weight)) return false; skip_line(in); std::cout << "Sex (M or F): "; char sex{}; if (not (in >> sex)) return false; skip_line(in); sex = std::toupper(sex, std::locale()); // Store information into data members only after reading // everything successfully. name_ = name; height_ = height; weight_ = weight; sex_ = sex; bmi_ = compute_bmi(height_, weight_); return true; }
222 Exploration 34 ■ More About Member Functions
224 Exploration 34 ■ More About Member Functions
Some records are printed differently than others, depending on the threshold. The >> operator has no convenient wayto specify the threshold. Overloading I/O operators is great for simple types but usually is not appropriate for morecomplicated situations.
225Exploration 34 ■ More About Member Functions
226 Exploration 34 ■ More About Member Functions
{ std::cout << "{ r=" << pt.distance() << ", angle=" << pt.angle() << " }\n";} void print_cartesian(point const& pt){ std::cout << "{ x=" << pt.x << ", y=" << pt.y << " }\n";} int main(){ point p1{}, p2{}; double const pi{3.141592653589792}; p1.x = std::cos(pi / 3); p1.y = std::sin(pi / 3); print_polar(p1); print_cartesian(p1); p2 = p1; p2.scale(4.0); print_polar(p2); print_cartesian(p2); p2.offset(0.0, -2.0); print_polar(p2); print_cartesian(p2);} Another common use for member functions is to restrict access to data members. Imagine what would happen ifa program that used the BMI record type accidentally modified the bmi_ member. A better design would let you calla bmi() function to obtain the BMI but hide the bmi_ data member, to prevent accidental modification. You canprevent such accidents, and the next Exploration shows you how.
227Exploration 35
Access Levels
Everyone has secrets, some of us more than others. Classes have secrets too. For example: Throughout this book, youhave used the std::string class without having any notion of what goes on inside the class. The implementationdetails are secrets—not closely guarded secrets, but secrets nonetheless. You cannot directly examine or modify anyof string’s data members. Instead, it presents quite a few member functions that make up its public interface. You arefree to use any of the publicly available member functions, but only the publicly available member functions.This Exploration explains how you can do the same with your classes.
229Exploration 35 ■ Access Levels
■■Tip Always keep data members private, and provide access only through member functions.
To modify a position, notice that point does not let the user arbitrarily assign a new x or y value. Instead, it offersseveral public member functions to move the point to an absolute position or relative to the current position. The public member functions let you work in Cartesian coordinates—that is, the familiar x and y positions,or in polar coordinates, specifying a position as an angle (relative to the x axis) and a distance from the origin. Bothrepresentations for a point have their uses, and both can uniquely specify any position in two-dimensional space.Some users prefer polar notation, while others prefer Cartesian. Neither user has direct access to the data members,so it doesn’t matter how the point class actually stores the coordinates. In fact, you can change the implementationof point to store the distance and angle as data members by changing only a few member functions. Which memberfunctions would you have to change? _____________________________________________________________ _____________________________________________________________
Changing the data members from x_ and y_ to r_ and angle_ necessitate a change to the x, y, angle, anddistance member functions, just for access to the data members. You also have to change the two move functions:move_polar and move_cartesian. Finally, you have to modify the constructors. No other changes are necessary.Because the scale and offset functions do not access data members directly, but instead call other memberfunctions, they are insulated from changes to the class implementation. Rewrite the point class to store polarcoordinates in its data members. Compare your class with mine, which is shown in Listing 35-2.
230 Exploration 35 ■ Access Levels
It’s something of a hack, but it will have to do for now. Later in the book, you will learn cleaner techniques toaccomplish this task.
Listing 35-3. The point Class Defined with the class Keywordclass point{public: point() : r_{0.0}, angle_{0.0} {} double x() const { return distance() * std::cos(angle()); } double y() const { return distance() * std::sin(angle()); } double angle() const { return angle_; } double distance() const { return r_; } ... other member functions omitted for brevity ... private: double r_; double angle_;};
232 Exploration 35 ■ Access Levels
POD classes are often declared with struct, and the same header can be used in C and C++ programs. Even ifyou don’t intend to share the header with a C program, using struct in this way emphasizes to the reader that theclass is POD. Not everyone uses struct to mean POD, but it’s a convention I follow in my own code. I use class forall other cases, to remind the human reader that the class can take advantage of C++ features and is not required tomaintain compatibility with C.
Public or Private?Usually, you can easily determine which members should be public and which should be private. Sometimes,however, you have to stop and ponder. Consider the rational class (last seen in Exploration 33). Rewrite the rationalclass to take advantage of access levels. Did you decide to make reduce() public or private? I chose private, because there is no need for any outsidecaller to call reduce(). Instead, the only member functions to call reduce() are the ones that change the datamembers themselves. Thus, reduce() is hidden from outside view and serves as an implementation detail. The moredetails you hide, the better, because it makes your class easier to use. When you added access functions, did you let the caller change the numerator only? Did you write a functionto change the denominator only? Or did you ask that the user assign both at the same time? The user of a rationalobject should treat it as a single entity, a number. You can’t assign only a new exponent to a floating-point number,and you shouldn’t be able to assign only a new numerator to a rational number. On the other hand, I see no reasonnot to let the caller examine only the numerator or only the denominator. For example, you may want to write yourown output formatting function, which requires knowing the numerator and denominator separately. A good sign that you have made the right choices is that you can rewrite all the operator functions easily. Thesefunctions should not have to access the data members of rational, but use only the public functions. If you tried to accessany private members, you learned pretty quickly that the compiler wouldn’t let you. That’s what privacy is all about. Compare your solution with my solution, presented in Listing 35-4.
233Exploration 35 ■ Access Levels
234 Exploration 35 ■ Access Levels
235Exploration 35 ■ Access Levels
236 Exploration 35 ■ Access Levels
237Exploration 36
Introduction to Object-OrientedProgramming
This Exploration takes a break from C++ programming to turn to the topic of object-oriented programming (OOP).You may already be familiar with this topic, but I urge you to continue reading. You may learn something new. Toeveryone else, this Exploration introduces some of the foundations of OOP in general terms. Later Explorations willshow how C++ implements OOP principles.
What are the similarities between books and magazines? Write down as many similarities as you can think of. _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ If you can, compare your lists with the lists that other people write. They don’t have to be programmers; everyoneknows what books and magazines are. Ask your friends and neighbors; stop strangers at the bus stop and ask them.Try to find a core set of commonalities and differences. Many items on the lists will be qualified. For instance, “most books have at least one author,” “many magazinesare published monthly,” and so on. That’s fine. When solving real problems, we often map “maybe” and “sometimes”into “never” or “always,” according to the specific needs of the problem at hand. Just remember that this is an OOPexercise, not a bookstore or library exercise.
239Exploration 36 ■ Introduction to Object-Oriented Programming
Now categorize the commonalities and the differences. I’m not telling you how to categorize them. Just try tofind a small set of categories that covers the diverse items on your lists. Some less useful categorizations are: groupby number of words, group by last letter. Try to find useful categories. Write them down. _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ I came up with two broad categories: attributes and actions. Attributes describe the physical characteristicsof books and magazines. • Books and magazines have size (number of pages) and cost. • Most books have an ISBN (International Standard Book Number). • Most magazines have an ISSN (International Standard Serial Number). • Magazines have a volume number and issue number. Books and magazines have a title and publisher. Books have authors. Magazines typically don’t.(Magazine articles have authors, but a magazine as a whole rarely lists an author.) Actions describe how a book or magazine acts or how you interact with them. • You can read a book or magazine. A book or magazine can be open or closed. • You can purchase a book or magazine. • You can subscribe to a magazine. The key distinction between attributes and actions is that attributes are specific to a single object. Actions areshared by all objects of a common class. Sometimes, actions are called behaviors. All dogs exhibit the behavior calledpanting; they all pant in pretty much the same manner and for the same reasons. All dogs have the attribute color, butone dog is golden, another dog is black, and the dog over there next to the tree is white with black spots. In programming terms, a class describes the behaviors or actions and the types of attributes for all the objects ofthat class. Each object has its own values for the attributes that the class enumerates. In C++ terms, member functionsimplement actions and provide access to attributes, and data members store attributes.
ClassificationBooks and magazines don’t do much on their own. Instead, their “actions” depend on how we interact with them.A bookstore interacts with books and magazines by selling, stocking, and advertising them. A library’s actions includelending and accepting returns. Other kinds of objects have actions they initiate on their own. For example, what aresome of the behaviors of a dog? _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
240 Exploration 36 ■ Introduction to Object-Oriented Programming
What about a cat? Do cats and dogs have significantly different behaviors? ____________Attributes? ________________ Summarize the differences.
_____________________________________________________________ _____________________________________________________________
I don’t own dogs or cats, so my observations are limited. From where I sit, dogs and cats have many similarattributes and behaviors. I expect that many readers are much more astute observers than I and can enumerate quitea few differences between the two animals. Nonetheless, I maintain that once you consider the differences closely, you will see that many of them are notattributes or behaviors unique to one type of animal or the other but are merely different values of a single attribute ordifferent details of a single behavior. Cats may be more fastidious, but dogs and cats both exhibit grooming behavior.Dogs and cats come in different colors, but they both have colored furs (with rare exceptions). In other words, when trying to enumerate the attributes and behaviors of various objects, your job can be madesimpler by classifying similar objects together. For critters, biologists have already done the hard work for us, and theyhave devised a rich and detailed taxonomy of animals. Thus, a species (catus or familiaris) belongs to a genus (Felisor Canis), which is part of a family (Felidae or Canidae). These are grouped yet further into an order (Carnivora), a class(Mammalia), and so on, up to the animal (Metazoa) kingdom. (Taxonomists: Please forgive my oversimplification.) So what happens to attributes and behaviors as you ascend the taxonomic tree? Which attributes and behaviorsare the same across all mammals?
_____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
All animals?
As the classification became broader, the attributes and behavior also became more general. Among theattributes of dogs and cats are color of fur, length of tail, weight, and much more. Not all mammals have fur or tails,so you need broader attributes for the entire class. Weight still works, but instead of overall length, you may want to usesize. Instead of color of fur, you need only generic coloring. For all animals, the attributes are quite broad: size, weight,single-cell vs. multicell, etc.
241Exploration 36 ■ Introduction to Object-Oriented Programming
Behaviors are similar. You may list that cats purr, dogs pant, both animals can walk and run, and so on. Allmammals eat and drink. Female mammals nurse their young. For all animals, you are left with a short, general list: eatand reproduce. It’s hard to be more specific than that when you are trying to list the behaviors common to all animals,from amoebae to zebras. A classification tree helps biologists understand the natural world. Class trees (or class hierarchies, as they areoften called, because big words make us feel important) help programmers model the natural world in software (ormodel the unnatural world, as so often happens in many of our projects). Instead of trying to name each level of thetree, programmers prefer a local, recursive view of any class hierarchy. Going up the tree, each class has a base class,also called a superclass or parent class. Thus animal is a base class of mammal, which is a base class of dog. Goingdownward are derived classes, also called subclasses or child classes. Dog is a derived class of mammal. Figure 36-1illustrates a class hierarchy. Arrows point from derived class to base class.
Metazoa
Mammalia
Carnivora Primates
An immediate base class is one with no intervening base classes. For example, the immediate base class of catusis Felis, which has an immediate base class of Felidae, which has an immediate base class of Carnivora. Metazoa,Mammalia, Carnivora, Felidae, and Felis are all base classes of catus, but only Felis is its immediate base class.
InheritanceJust as a mammal has all the attributes and behaviors of an animal, and a dog has the attributes and behaviors of allmammals, in an OOP language, a derived class has all the behaviors and attributes of all of its base classes. The termmost often used is inheritance: the derived class inherits the behaviors and attributes of its base class. This term issomewhat unfortunate, because OOP inheritance is nothing like real-world inheritance. When a derived class inheritsbehaviors, the base class retains its behaviors. In the real world, classes don’t inherit anything; objects do. In the real world, a person object inherits the value of certain attributes (cash, stock, real estate, etc.) from adeceased ancestor object. In the OOP world, a person class inherits behaviors from a base class, such as primate,by sharing the single copy of those behavior functions that are defined in the base class. A person class inherits theattributes of a base class, so objects of the derived class contain values for all the attributes defined in its class and inall of its base classes. In time, the inheritance terminology will become natural to you.
242 Exploration 36 ■ Introduction to Object-Oriented Programming
Because inheritance creates a tree structure, tree terminology also pervades discussion of inheritance. As is socommon in programming, tree diagrams are drawn upside down, with the root at the top, and leaves at the bottom(as you saw in Figure 36-1). Some OOP languages (Java, Smalltalk, Delphi) have a single root, which is the ultimate baseclass for all classes. Others, such as C++, do not. Any class can be the root of its own inheritance tree. So far, the main examples for inheritance involved some form of specialization. Cat is more specialized thanmammal, which is more specialized than animal. The same is true in computer programming. For example, classframeworks for graphical user interfaces (GUIs) often use a hierarchy of specialized classes. Figure 36-2 shows aselection of some of the more important classes that make up wxWidgets, which is an open-source C++ frameworkthat supports many platforms.
wxObject
wxEvtHandler wxGDIObject
wxFrame wxDialog
Even though C++ does not require a single root class, some frameworks do; wxWidgets is one that does requirea single root class. Most wxWidgets classes derive from wxObject. Some objects are straightforward, such as wxPenand wxBrush. Interactive objects derive from wxEvtHandler (short for “event handler”). Thus, each step in the class treeintroduces another degree of specialization. Later in the book, you will see other uses for inheritance, but the most common and most important use is tocreate specialized derived classes from more general base classes.
243Exploration 36 ■ Introduction to Object-Oriented Programming
handler. The click handler does not know or care whether the control is actually a wxListCtrl control, a wxTreeCtrlcontrol, or a wxButton. All that matters is that wxEvtHandler accepts a click event, acquires the position, determineswhich mouse button was clicked, and so on, and then dispatches this event to the event handler. The burden is on the authors of the wxButton, wxListCtrl, and wxTreeCtrl classes to ensure that their clickbehavior meets the requirements of the Substitution Principle. The easiest way to meet the requirements is to let thederived class inherit the behavior of the base class. Sometimes, however, the derived class has additional work to do.Instead of inheriting, it provides new behavior. In that case, the programmer must ensure that the behavior is a validsubstitution for the base class behavior. The next few Explorations will show concrete examples of this abstract principle.
Type PolymorphismBefore returning to C++-land, I want to present one more general principle. Suppose I hand you a box labeled“Mammal.” Inside the box can be any mammal: a dog, a cat, a person, etc. You know the box cannot contain a bird,a fish, a rock, or a tree. It must contain a mammal. Programmers call the box polymorphic, from the Greek meaning“many forms.” The box can hold any one of many forms, that is, any one mammal, regardless of which form ofmammal it is. Although many programmers use the general term polymorphism, this specific kind of polymorphism is typepolymorphism, also known as subtyping polymorphism. That is, the type of a variable (or a box) determines whichkinds of objects it can contain. A polymorphic variable (or box) can contain one of a number of types of objects. In particular, a variable with a base class type can refer to an object of the base class type or to an object of anytype that is derived from that base class. According to the Substitution Principle, you can write code to use the baseclass variable, calling any of the member functions of the base class, and that code will work, regardless of the object’strue, derived type. Now that you have a fundamental understanding of the principles of OOP, it is time to see how they play out in C++.
244Exploration 37
Inheritance
The previous Exploration introduced general OOP principles. Now it’s time to see how to apply those principles to C++.
Deriving a ClassDefining a derived class is just like defining any other class, except that you include a base class access level and nameafter a colon. See Listing 37-1 for an example of some simple classes to support a library. Every item in the library isa work of some kind: a book, a magazine, a movie, and so on. To keep things simple, the class work has only twoderived classes, book and periodical.
Try it. Change work’s default constructor from = default to = delete and try to compile the code for Listing 37-1.(Add a trivial main(), to ensure that you write a complete program, and be certain to #include all necessary headers.)What happens? _____________________________________________________________ That’s right; the compiler complains. The exact error message or messages you receive vary from compiler tocompiler. I get something like the following:
246 Exploration 37 ■ Inheritance
Base classes are always initialized before members, starting with the root of the class tree. You can see this foryourself by writing classes that print messages from their constructors, as demonstrated in Listing 37-2.
247Exploration 37 ■ Inheritance
basemiddlederived
Remember that if you omit the base class from the initializers, or you omit the initializer list entirely, the baseclass’s default constructor is called. Listing 37-2 contains only default constructors, so what happens is the constructorfor derived first invokes the default constructor for middle. The constructor for middle invokes the default constructorfor base first, and the constructor for base has nothing to do except execute its function body. Then it returns, and theconstructor body for middle executes and returns, finally letting derived run its function body.
Member FunctionsA derived class inherits all members of the base class. This means a derived class can call any public member functionand access any public data member. So can any users of the derived class. Thus, you can call the id() and title()functions of a book object, and the work::id() and work::title() functions are called. The access levels affect derived classes, so a derived class cannot access any private members of a base class.(In Exploration 66, you will learn about a third access level that shields members from outside prying eyes whilegranting access to derived classes.) Thus, the periodical class cannot access the id_ or title_ data members, andso a derived class cannot accidentally change a work’s identity or title. In this way, access levels ensure the integrityof a class. Only the class that declares a data member can alter it, so it can validate all changes, prevent changes, orotherwise control who changes the value and how. If a derived class declares a member function with the same name as the base class, the derived class functionis the only one visible in the derived class. The function in the derived class is said to shadow the function in the baseclass. As a rule, you want to avoid this situation, but there are several cases in which you very much want to use thesame name, without shadowing the base class function. In the next Exploration, you will learn about one such case.Later, you will learn others.
DestructorsWhen an object is destroyed—perhaps because the function in which it is defined ends and returns—sometimesyou have to do some cleanup. A class has another special member function that performs cleanup when an objectis destroyed. This special member function is called a destructor. Like constructors, destructors do not have return values. A destructor name is the class name preceded by a tilde(~). Listing 37-3 adds destructors to the example classes from Listing 37-2.
248 Exploration 37 ■ Inheritance
_____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
basemiddlederived~derived~middle~base
249Exploration 37 ■ Inheritance
If you don’t write a destructor, the compiler writes a trivial one for you. Whether you write your own, or thecompiler implicitly writes the destructor, after every destructor body finishes, the compiler arranges to call thedestructor for every data member and then execute the destructor for the base classes, starting with the most-derived.For simple classes in these examples, the compiler’s destructors work just fine. Later, you will find more interestinguses for destructors. For now, the main purpose is just to visualize the life cycle of an object. Read Listing 37-4 carefully.
250 Exploration 37 ■ Inheritance
} void increment_reference(base& b){ ++b;} int main(){ derived d{make_derived()}; base b{increment(d)}; increment_reference(d); increment_reference(b); derived a(d.value() + b.value());} Fill in the left-hand column of Table 37-1 with the output you expect from the program.
Table 37-1. Expected and Actual Results of Running the Program in Listing 37-4
Try it. Fill in the right-hand column of Table 37-1 with the actual output and compare the two columns. Did youget everything correct? ________________. Below is the output generated on my system, along with some commentary. Remember that compilers havesome leeway in optimizing away extra calls to the copy constructor. You may get one or two extra copy calls in the mix.
251Exploration 37 ■ Inheritance
Note how pass-by-reference (increment_reference) does not invoke any constructors, because no objects arebeing constructed. Instead, references are passed to the function, and the referenced object is incremented. By the way, I have not yet shown you how to overload the increment operator, but you probably guessed that’show it works (in class base). Decrement is similar.
Access LevelAt the start of this Exploration, I advised you to use public before the base class name but never explained why. Nowis the time to fill you in on the details. Access levels affect inheritance the same way they affect members. Public inheritance occurs when you use thestruct keyword to define a class or use the public keyword before the base class name. Public inheritance means thederived class inherits every member of the base class at the same access level that the members have in the base class.Except in rare circumstances, this is exactly what you want. Private inheritance occurs when you use the private keyword, and it is the default when you define a class usingthe class keyword. Private inheritance keeps every member of the base class private and inaccessible to users ofthe derived class. The compiler still calls the base class constructor and destructor when necessary, and the derivedclass still inherits all the members of the base class. The derived class can call any of the base class’s public memberfunctions, but no one else can call them through the derived class. It’s as though the derived class re-declares allinherited members as private. Private inheritance lets a derived class make use of the base class without beingrequired to meet the Substitution Principle. This is an advanced technique, and I recommend that you try it only withproper adult supervision. If the compiler complains about inaccessible members, most likely you forgot to include a public keyword in theclass definition. Try compiling Listing 37-5 to see what I mean.
252 Exploration 37 ■ Inheritance
Programming StyleWhen in doubt, make data members and member functions private, unless and until you know you need to make amember public. Once a member is part of the public interface, anyone using your class is free to use that member, andyou have one more code dependency. Changing a public member means finding and fixing all those dependencies.Keep the public interface as small as possible. If you have to add members later, you can, but it’s much harder toremove a member or change it from public to private. Anytime you have to add members to support the publicinterface, make the supporting functions and data members private. Use public, not private, inheritance. Remember that inherited members also become part of the derivedclass’s public interface. If you change which class is the base class, you may have to write additional members inthe derived class, to make up for members that were in the original base class but are missing from the new baseclass. The next Exploration continues the discussion of how derived classes work with base classes to provideimportant functionality.
253Exploration 38
Virtual Functions
Deriving classes is fun, but there’s not a lot you can do with them—at least, not yet. The next step is to see howC++ implements type polymorphism, and this Exploration starts you on that journey.
Type PolymorphismRecall from Exploration 36 that type polymorphism is the ability of a variable of type B to take the “form” of anyclass derived from B. The obvious question is: “How?” The key in C++ is to use a magic keyword to declare amember function in a base class and also implement the function in a derived class with a different magic word.The magic keyword tells the compiler that you want to invoke type polymorphism, and the compiler implements thepolymorphism magic. Define a variable of type reference-to-base class and initialize it with an object of derived classtype. When you call the polymorphic function for the object, the compiled code checks the object’s actual type andcalls the derived class implementation of the function. The magic word to turn a function into a polymorphic functionis virtual. Derived classes are marked with override. For example, suppose you want to be able to print any kind of work in the library (see Listing 37-1) using standard(more or less) bibliographical format. For books, I use the format
Listing 38-1. Adding a Polymorphic print Function to Every Class Derived from workclass work{public: work() = default; work(work const&) = default; work(std::string const& id, std::string const& title) : id_{id}, title_{title} {} virtual ~work() {} std::string const& id() const { return id_; } std::string const& title() const { return title_; } virtual void print(std::ostream&) const {}
255Exploration 38 ■ Virtual Functions
private: std::string id_; std::string title_;}; class book : public work{public: book() : work{}, author_{}, pubyear_{0} {} book(book const&) = default; book(std::string const& id, std::string const& title, std::string const& author, int pubyear) : work{id, title}, author_{author}, pubyear_{pubyear} {} std::string const& author() const { return author_; } int pubyear() const { return pubyear_; } void print(std::ostream& out) const override { out << author() << ", " << title() << ", " << pubyear() << "."; }private: std::string author_; int pubyear_; ///< year of publication}; class periodical : public work{public: periodical() : work{}, volume_{0}, number_{0}, date_{} {} periodical(periodical const&) = default; periodical(std::string const& id, std::string const& title, int volume, int number, std::string const& date) : work{id, title}, volume_{volume}, number_{number}, date_{date} {} int volume() const { return volume_; } int number() const { return number_; } std::string const& date() const { return date_; } void print(std::ostream& out) const override { out << title() << ", " << volume() << '(' << number() << "), " << date() << "."; }private: int volume_; ///< volume number int number_; ///< issue number std::string date_; ///< publication date};
256 Exploration 38 ■ Virtual Functions
■■Tip When writing a stub function, such as print(), in the base class, omit the parameter name or names. Thecompiler requires only the parameter types. Some compilers warn you if a parameter or variable is not used, and even ifthe compiler doesn’t issue a warning, it is a clear message to the human who reads your code that the parameters arenot used.
A program that has a reference to a work object can call the print member function to print that work, andbecause print is polymorphic, or virtual, the C++ environment performs its magic to ensure that the correct print iscalled, depending on whether the work object is actually a book or a periodical. To see this demonstrated, read theprogram in Listing 38-2.
257Exploration 38 ■ Virtual Functions
■■Tip The const keyword, if present, always comes before override. Although the specifiers, such as virtual, canbe mixed freely with the function’s return type (even if the result is strange, such as int virtual long function()),the const qualifier and override specifier must follow a strict order.
Virtual FunctionsA polymorphic function is called a virtual function in C++, owing to the virtual keyword. Once a function is definedas virtual, it remains so in every derived class. The virtual function must have the same name, the same return type,and the same number and type of parameters (but the parameters can have different names) in the derived class. A derived class is not required to implement a virtual function. If it doesn’t, it inherits the base class function thesame way it does for a non-virtual function. When a derived class implements a virtual function, it is said to overridethe function, because the derived class’s behavior overrides the behavior that would have been inherited from thebase class. In the derived class, the override specifier is optional but helps to prevent mistakes. If you accidentally mistypethe function’s name or parameters in the derived class, the compiler might think you are defining a brand-newfunction. By adding override, you tell the compiler that you intend to override a virtual function that was declared inthe base class. If the compiler cannot find a matching function in the base class, it issues an error message. Add a class, movie, to the library classes. The movie class represents a movie or film recording on tape or disc.Like book and periodical, the movie class derives from work. For the sake of simplicity, define a movie as havingan integer running time (in minutes), in addition to the members it inherits from work. Do not override print yet.Compare your class to Listing 38-4.
258 Exploration 38 ■ Virtual Functions
259Exploration 38 ■ Virtual Functions
EVOLUTION OF A LANGUAGE
You may find it odd that the virtual keyword appears at the start of a function header and override appears at the end. You are witnessing the compromises that are often necessary when a language evolves. The override specifier is new in C++ 11. One way to add the override specifier would have been to add it to the list of function specifiers, like virtual. But adding a new keyword to a language is fraught with difficulty. Every existing program that uses override as a variable or other user-defined name would break. Programmers all over the world would have to check and possibly modify their software to avoid this new keyword. So the C++ standard committee devised a way to add override without making it a reserved keyword. The syntax of a function declaration puts the const qualifier in a special place. No other identifiers are allowed there, so it is easy to add override to the syntax for member functions in a manner similar to const, and with no risk of breaking existing code. Other new language features use existing keywords in new ways, such as =default and =delete for constructors. But a few new keywords were added, and they bring with them the risk of breaking existing code. So the committee tried to choose names that would be less likely to conflict with existing user-chosen names. You will see examples of some of these new keywords later in the book.
260 Exploration 38 ■ Virtual Functions
261Exploration 38 ■ Virtual Functions
Virtual DestructorsAlthough most classes you are writing at this time do not require destructors, I want to mention an importantimplementation rule. Any class that has virtual functions must declare its destructor to be virtual too. This rule is aprogramming guideline, not a semantic requirement, so the compiler will not help you by issuing a message when youbreak it (although some compilers may issue a warning). Instead, you must enforce this rule yourself, through discipline. I will repeat the rule when you begin to write classes that require destructors. If you try any experimentson your own, please be mindful of this rule, or else your programs could be subject to subtleproblems—or not-so-subtle crashes. The next Exploration continues the discussion of classes and their relationship in the C++ type system.
262Exploration 39
One of the main design goals for C++ was to give the programmer the ability to define custom types that look and actnearly identically to the built-in types. The combination of classes and overloaded operators gives you that power.This Exploration takes a closer look at the type system and how your classes can best fit into the C++ world.
263Exploration 39 ■ Classes and Types
264 Exploration 39 ■ Classes and Types
265Exploration 39 ■ Classes and Types
The new classes prevent mistakes, such as that in Listing 39-1, but at the expense of more code. For example,you have to write suitable I/O operators. You also have to decide which arithmetic operators to implement. And don’tforget the comparison operators. Most of these functions are trivial to write, but you can’t neglect them. In manyapplications, however, the work will pay off many times over, by removing potential sources of error. I’m not suggesting that you do away with unadorned integers and other built-in types and replace them withclumsy wrapper classes. In fact, I agree with you (don’t ask how I know what you’re thinking) that the BMI exampleis rather artificial. If I were writing a real, honest-to-goodness program for computing and managing BMIs, I woulduse plain int variables and rely on careful coding and proofreading to prevent and detect errors. I use wrapperclasses, such as height and weight, when they add some primary value. A big program in which heights and weightsfigured prominently would offer many opportunities for mistakes. In that case, I would want to use wrapper classes.I could also add some error checking to the classes, impose constraints on the domain of values they can represent,or otherwise help myself to do my job as a programmer. Nonetheless, it’s best to start simple and add complexity slowlyand carefully. The next section explains in greater detail what behavior you must implement to make a useful andmeaningful custom class.
Value TypesThe height and weight types are examples of value types—that is, types that behave as ordinary values. Contrast themwith the I/O stream types, which behave very differently. For example, you cannot copy or assign streams; you mustpass them by reference to functions. Nor can you compare streams or perform arithmetic on them. Value types, bydesign, behave similarly to the built-in types, such as int and float. One of the important characteristics of valuetypes is that you can store them in containers, such as vector and map. This section explains the general requirementsfor value types. The basic guideline is to make sure your type behaves “like an int.” When it comes to copying, comparing, andperforming arithmetic, avoid surprises, by making your custom type look, act, and work as much like the built-in typesas possible.
CopyingCopying an int yields a new int that is indistinguishable from the original. Your custom type should behave thesame way. Consider the example of string. Many implementations of string are possible. Some of these use copy-on-writeto optimize frequent copying and assignment. In a copy-on-write implementation, the actual string contents are keptseparate from the string object. Copies of the string object do not copy the contents until and unless a copy is needed,which happens when the string contents must be modified. Many uses of strings are read-only, so copy-on-writeavoids unnecessary copies of the contents, even when the string objects themselves are copied frequently. Other implementations optimize for small strings by using the string object to store their contents but storinglarge strings separately. Copying small strings is fast, but copying large strings is slower. Most programs use only smallstrings. In spite of these differences in implementation, when you copy a string (such as passing a string by value toa function), the copy and the original are indistinguishable, just like an int. Usually, the automatic copy constructor does what you want, and you don’t have to write any code. Nonetheless,you have to think about copying and assure yourself that the compiler’s automatic (also called implicit) copyconstructor does exactly what you want.
AssigningAssigning objects is similar to copying them. After an assignment, the target and source must contain identical values.The key difference between assignment and copying is that copying starts with a blank slate: a newly constructedobject. Assignment begins with an existing object, and you may have to clean up the old value before you can assignthe new value. Simple types such as height have nothing to clean up, but later in this book, you will learn how toimplement more complicated types, such as string, which require careful cleanup.
266 Exploration 39 ■ Classes and Types
Most simple types work just fine with the compiler’s implicit assignment operator, and you don’t have to writeyour own. Nonetheless, you must consider the possibility and make sure the implicit assignment operator is exactlywhat you want.
MovingSometimes, you don’t want to make an exact copy. I know I wrote that assignment should make an exact copy, but youcan break that rule by having assignment move a value from the source to the target. The result leaves the source in anunknown state (typically empty), and the target gets the original value of the source. Force a move assignment by calling std::move (declared in <utility>): std::string source{"string"}, target;target = std::move(source); After the assignment, source is in an unknown, but valid, state. Typically it will be empty, but you cannot writecode that assumes it is empty. In practical terms, the string contents of source are moved into target without copyingany of the string contents. Moving is fast and independent of the amount of data stored in a container. You can also move an object in an initializer, as follows:std::string source{"string"};std::string target{std::move(source)}; Moving works with strings and most containers. Consider the program in Listing 39-3.
267Exploration 39 ■ Classes and Types
_____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
{ 1 2 3 }{ 1 2 3 }{ 1 2 3 }{ }
The first three lines print { 1 2 3 }, as expected. But the last line is interesting, because source was movedinto move. When a vector’s contents are moved, the source becomes empty. Writing a move constructor is advancedand will have to wait until later in this book, but you can take advantage of move constructors and move assignmentoperators in the standard library by calling std::move().
ComparingI defined copying and assignment in a way that requires meaningful comparison. If you can’t determine whethertwo objects are equal, you can’t verify whether you copied or assigned them correctly. C++ has a few ways to checkwhether two objects are the same.
• The first and most obvious way is to compare objects with the == operator. Value types should overload this operator. Make sure the operator is transitive—that is, if a == b and b == c, then a == c. Make sure the operator is commutative, that is, if a == b, then b == a. Finally, the operator should be reflexive: a == a. • Standard algorithms such as find compare items by one of two methods: with operator== or with a caller-supplied predicate. Sometimes, you may want to compare objects with a custom predicate, for example, a person class might have operator== that compares every data member (name, address, etc.), but you want to search a container of person objects by checking only last names, which you do by writing your own comparison function. The custom predicate must obey the same transitive and reflexive restrictions as the == operator. If you are using the predicate with a specific algorithm, that algorithm calls the predicate in a particular way, so you know the order of the arguments. You don’t have to make your predicate commutative, and in some cases, you wouldn’t want to. • Containers such as map store their elements in sorted order. Some standard algorithms, such as binary_search, require their input range to be in sorted order. The ordered containers and algorithms use the same conventions. By default, they use the < operator, but you can also supply your own comparison predicate. These containers and algorithms never use the == operator to determine whether two objects are the same. Instead, they check for equivalence—that is, a is equivalent to b if a < b is false and b < a is false.
If your value type can be ordered, you should overload the < operator. Ensure that the operator is transitive (if a < b and b < c, then a < c). Also, the ordering must be strict, that is, a < a is always false.
268 Exploration 39 ■ Classes and Types
• Containers and algorithms that check for equivalence also take an optional custom predicate instead of the < operator. The custom predicate must obey the same transitive and strictness restrictions as the < operator. Not all types are comparable with a less-than relationship. If your type cannot be ordered, do not implementthe < operator, but you must also understand that you will not be able to store objects of that type in a map or use anyof the binary search algorithms. Sometimes, you may want to impose an artificial order, just to permit these uses.For example, a color type may represent colors such as red, green, or yellow. Although nothing about red or greeninherently defines one as being “less than” another, you may want to define an arbitrary order, just so you can usethese values as keys in a map. One immediate suggestion is to write a comparison function that compares colors asintegers, using the < operator. On the other hand, if you have a value that should be compared (such as rational), you should implementoperator== and operator<. You can then implement all other comparison operators in terms of these two.(See Exploration 32 for an example of how the rational class does this.) (If you have to store unordered objects in a map, you can use std::unordered_map, which is a new type in C++ 11.It works almost exactly the same as std::map, but it stores values in a hash table instead of a binary tree. Ensuring thata custom type can be stored in std::unordered_map is more advanced and won’t be covered until much later.) Implement a color class that describes a color as three components: red, green, and blue, which are integersin the range 0 to 255. Define a comparison function, order_color, to permit storing colors as map keys. For extracredit, devise a suitable I/O format and overload the I/O operators too. Don’t worry about error-handling yet—forexample, what if the user tries to set red to 1000, blue to 2000, and green to 3000. You’ll get to that soon enough. Compare your solution with mine, which is presented in Listing 39-4.
269Exploration 39 ■ Classes and Types
270 Exploration 39 ■ Classes and Types
271Exploration 39 ■ Classes and Types
When the input function returns, the ioflags object is destroyed, and its destructor restores the originalformatting flags. Without the magic of RAII, the operator>> function would have to restore the flags manually at allfour return points, which is burdensome and prone to error. It makes no sense to copy an ioflags object. If you copy it, which object would be responsible for restoring theflags? Thus, the class deletes the copy constructor. If you accidentally write code that would copy the ioflags object,the compiler will complain. RAII is a common programming idiom in C++. The more you learn about C++, the more you will come toappreciate its beauty and simplicity. As you can see, our examples are becoming more complicated, and it’s becoming harder and harder for me to fitentire examples in a single code listing. Your next task is to understand how to separate your code into multiple files,which makes my job and yours much easier. The first step for this new task is to take a closer look at declarations,definitions, and the distinctions between them.
272EXPLORATION 40
Exploration 20 introduced the distinction between declarations and definitions. This is a good time to remind you ofthe difference and to explore declarations and definitions of classes and their members.
273EXPLORATION 40 ■ Declarations and Definitions
private: void reduce(); int numerator_; int denominator_;}; rational::rational(): rational{0}{} rational::rational(int num): numerator_{num}, denominator_{1}{} rational::rational(int num, int den): numerator_{num}, denominator_{den}{ reduce();} void rational::assign(int num, int den){ numerator_ = num; denominator_ = den; reduce();} void rational::reduce(){ assert(denominator_ != 0); if (denominator_ < 0) { denominator_ = -denominator_; numerator_ = -numerator_; } int div{gcd(numerator_, denominator_)}; numerator_ = numerator_ / div; denominator_ = denominator_ / div;} int rational::numerator()const{ return numerator_;} int rational::denominator()const{ return denominator_;}
274 EXPLORATION 40 ■ Declarations and Definitions
inline FunctionsIn Exploration 30, I introduced the inline keyword, which is a hint to the compiler that it should optimize speedover size by trying to expand a function at its point of call. You can use inline with member functions too. Indeed,for trivial functions, such as those that return a data member and do nothing else, making the function inline canimprove speed and program size. When you define a function inside the class definition, the compiler automatically adds the inline keyword.If you separate the definition from the declaration, you can still make the function inline by adding the inline keywordto the function declaration or definition. Common practice is to place the inline keyword only on the definition, but Irecommend putting the keyword in both places, to help the human reader. Remember that inline is just a hint. The compiler does not have to heed the hint. Modern compilers arebecoming better and better at making these decisions for themselves. My personal guideline is to define one-line functions in the class definition. Longer functions or functions thatare complicated to read belong outside the class definition. Some functions are too long to fit in the class definitionbut are short and simple enough that they should be inline. Organizational coding styles usually include guidelinesfor inline functions. For example, directives for large projects may eschew inline functions because they increasecoupling between software components. Thus, inline may be allowed only on a function-by-function basis, whenperformance measurements demonstrate their need. Rewrite the rational class from Listing 40-1 to use inline functions judiciously. Compare your solution withthat of mine, shown in Listing 40-2.
276 EXPLORATION 40 ■ Declarations and Definitions
You can declare a local reference as a synonym for a local variable. Declare the new name as a reference in thesame manner as a reference parameter, but initialize it with an existing object. If the reference is const, you can useany expression (of a suitable type) as the initializer. For a non-const reference, you must use an lvalue (rememberthose from Exploration 21?), such as another variable. Listing 40-3 illustrates these principles.
277EXPLORATION 40 ■ Declarations and Definitions
What happens if you delete the const from the declaration of range?
The result that equal_range returns is an rvalue, not an lvalue, so you must use const when initializing areference to that result. Because you are free to modify an object via a non-const reference, only lvalue objects areallowed. Usually, the values returned from functions are rvalues, not lvalues, so references must be const.
Static VariablesLocal variables are automatic. This means that when the function begins or a local block (compound statement)is entered, memory is allocated, and the object is constructed. When the function returns or when control exitsthe block, the object is destroyed, and memory is reclaimed. All automatic variables are allocated on the programstack, so memory allocation and release is trivial and typically handled by the host platform’s normal function-callinstructions. Remember that main() is like a function and follows many of the same rules as other functions. Thus, variablesthat you define in main() seem to last for the entire lifetime of the program, but they are automatic variables, allocatedon the stack, and the compiler treats them the same as it treats any other automatic variables. The behavior of automatic variables permits idioms such as RAII (see Exploration 39) and greatly simplifies typicalprogramming tasks. Nonetheless, it is not suited for every programming task. Sometimes you need a variable’s lifetimeto persist across function calls. For example, suppose you need a function that generates unique identification numbersfor a variety of objects. It starts a serial counter at 1 and increments the counter each time it issues an ID. Somehow,the function must keep track of the counter value, even after it returns. Listing 40-5 demonstrates one way to do it.
int main(){ for (int i{0}; i != 10; ++i) std::cout << generate_id() << '\n';} You can also declare a variable outside of any function. Because it is outside of all functions, it is not inside anyblock, thus it cannot be automatic, and so its memory must be static. You don’t have to use the static keyword forsuch a variable. Rewrite Listing 40-6 to declare counter outside of the generate_id function. Do not use the statickeyword. Assure yourself that the program still works correctly. Listing 40-7 shows my solution.
280 EXPLORATION 40 ■ Declarations and Definitions
int main(){ std::cout << "pi = " << rational::pi << '\n';} A static const data member with an integral type is a little odd, however. Only these data members can havean initializer inside the class definition, as part of the data member’s declaration. The value does not change thedeclaration into a definition, and you still must define the data member outside the class definition. However, byproviding a value in the declaration, you can use the static const data member as a constant value elsewhere in theprogram, anywhere a constant integer is needed. class string {… static size_type const npos{-1};…}; Like other collection types, string declares size_type as a suitable integer type for representing sizes andindices. The implementation of the string class has to define this data member, but without an initial value. string::size_type const string::npos; Listing 40-9 shows some examples of static data members in a more sophisticated ID generator. This one usesa prefix as part of the IDs it produces and then uses a serial counter for the remaining portion of each ID. You caninitialize the prefix to a random number to generate IDs that are unique even across multiple runs of the sameprogram. (The code is not meant to show off a high-quality ID generator, only static data members.) Using a differentprefix for every run is fine for production software but greatly complicates testing. Therefore, this version of theprogram uses the fixed quantity 1. A comment shows the intended code.
281EXPLORATION 40 ■ Declarations and Definitions
counter_ = 0; else ++counter_; return static_cast<long>(prefix_) * (max_counter_ + 1) + counter_;} int main(){ generate_id gen; // Create an ID generator for (int i{0}; i != 10; ++i) std::cout << gen.next() << '\n';}
DeclaratorsAs you’ve already seen, you can define multiple variables in a single declaration, as demonstrated in the following: int x{42}, y{}, z{x+y}; The entire declaration contains three declarators. Each declarator declares a single name, whether that name isfor a variable, function, or type. Most C++ programmers don’t use this term in everyday conversation, but C++ expertsoften do. You have to know official C++ terminology, so that if you have to ask for help from the experts, you canunderstand them. The most important reason to know about separating declarations from definitions is so you can put a definitionin one source file and a declaration in another. The next Exploration shows how to work with multiple source files.
282EXPLORATION 41
Real programs rarely fit into a single source file, and I know you’ve been champing at the bit, eager to explore howC++ works with multiple source files that make up a single program. This Exploration shows you the basics. Advancedtechniques, such as shared libraries (DLLs, shared objects, etc.), are beyond the scope of this book and sometimesinvolve compiler-specific features that extend the language beyond the standard. You have to consult your compilerdocumentation for details.
283EXPLORATION 41 ■ Using Multiple Source Files
long generate_id::next(){ if (counter_ == max_counter_) counter_ = 0; else ++counter_; return static_cast<long>(prefix_) * (max_counter_ + 1) + counter_;} You can compile the generate_id.cpp file but not link it. To create a valid C++ program, you must have a mainfunction, which is presented in Listing 41-2. Because main makes use of the generate_id class, it requires the classdefinition but not the definitions of the class’s members.
Listing 41-2. The main Function for the ID-generating Program in the main.cpp File#include <iostream> class generate_id{public: generate_id() : counter_{0} {} long next();private: short counter_; static short prefix_; static short const max_counter_{32767};}; int main(){ generate_id gen{}; // Create an ID generator for (int i{0}; i != 10; ++i) std::cout << gen.next() << '\n';} Now compile the two source files and link them together to produce a working C++ program. An IDE takes care ofthe details for you, provided both source files are part of the same project. If you are using command-line tools, you caninvoke the same compiler, but instead of listing source file names on the command line, list only the object file names.Alternatively, you can compile and link at the same time, by listing all the source file names in one compilation. Verifythat the program’s behavior is identical to that from Listing 40-9. That’s the basic idea, but the details, of course, are a little trickier. For the remainder of this Exploration, we’ll takea closer look at those details.
284 EXPLORATION 41 ■ Using Multiple Source Files
When compiling main.cpp, the compiler sees the declaration of next_id and the call to next_id. As far as itcan tell, everything is just fine. The linker, however, sees a call to next_id in the main object file but no definition ofnext_id in the generate_id object file. Thus, the linker issues an error message and refuses to create the executableprogram file:
A more subtle change is to add a new data member before counter, as shown in Listing 41-4.
285EXPLORATION 41 ■ Using Multiple Source Files
Remember that the program should produce identical results. The exact numeric sequence should be the samebetween this program and earlier incarnations. The problem is that you have stumbled once again into undefinedbehavior. Even though the main function thinks the unused_ data member is indeed unused, the next() function’s ideaof the counter_ data member happens to be at the same address as the unused_ data member. This is bad—reallybad. But if you weren’t watching closely, you might have missed it. Although anything is possible, most likely theprogram generated a sequence of unique IDs, and if you hadn’t looked at the actual values but simply used thegenerate_id class in a program, you might never have noticed. At least, not until you ported the program to a differentenvironment. A different compiler or linker may be able to detect this error. A different operating system may causethe program to crash. Once you enter the realm of undefined behavior, anything goes. A class or function definition must be identical in all files, or else the results are undefined. The compiler andlinker are not required to detect this kind of error. You have been warned. To avoid these kinds of problems, you have to host the definition of the generate_id class in a single file andsomehow use that file in every source file that makes use of the class. This way, you can assure yourself that everysource file is using the same definition of generate_id. You must write your own #include file.
286 EXPLORATION 41 ■ Using Multiple Source Files
#include FilesRemember that the compiler needs only a function’s declaration, not its definition, in order to call the function. If youput the declaration in its own file, then #include that file in every source file that calls the function, you ensure thatthe compiler sees the same declaration every time, allowing it to generate the correct code for calling the function.The same is true for a class. Put only the class definition in its own file, and then #include the file in every source filethat needs it. Listing 41-5 shows you the #include file that contains the generate_id class. Common conventions for#include files are to use extensions such as .h or .hpp (for header or C++ header) or, sometimes, .hh or .hxx. Somefiles have to preserve compatibility with C, and these typically use .h as the extension. For all other files, I prefer .hpp,because it is a nice parallel to the extension .cpp that I use for C++ source files. Thus, name this file generate_id.hpp.If you use .cc for source files, you may like .hh for header files, ditto for .cxx and .hxx.
287EXPLORATION 41 ■ Using Multiple Source Files
288 EXPLORATION 41 ■ Using Multiple Source Files
C++ does not allow you to define a class, function, or object more than once in the same source file, andgenerate_id.hpp contains a class definition. Thus, if you #include that file more than once, the compiler issues anerror message. The next section explains how to prevent this problem.
Include GuardsYou cannot define the same class, function, or object more than once in a source file. On the other hand, you cannotprevent anyone from including your header more than once. Somehow, you must ensure that the compiler sees thedefinitions only once, even if a source file includes your header more than once. Fortunately, C++ offers additional #directives to help you.
289EXPLORATION 41 ■ Using Multiple Source Files
The most widely used idiom is to pick a name to uniquely identify your header, such as VITAL_STATS_HPP_ orGENERATE_ID_HPP_, and then use that name to control the compilation of your header. Use the following two lines asthe first two lines of the header file: #ifndef GENERATE_ID_HPP_#define GENERATE_ID_HPP_ Next, use the following as the last line of the header file: #endif Convention is to use all capital letters, because no other names in your program should be made up of allcapital letters. I use the file name but change dot (.) and other characters that aren’t allowed in C++ identifiers intounderscores (_). I use a trailing underscore to further ensure that I avoid collisions with other names. (Remember thatnames beginning with an underscore are not allowed, nor are names with two adjacent underscores.) The #ifndef directive means “if not defined.” The directive controls whether the compiler compiles the sourcecode normally or skips the entire section of the file. If the name is not defined, compilation proceeds normally. If it isdefined, the compiler skips rapidly over the file until it finds #endif, thereby ignoring the definitions that you want toprotect. The #define directive defines a name. Thus, the first time the compiler processes this header, the name is notdefined, the #ifndef condition is true, and the compiler processes the definitions normally. Among the code itprocesses normally is the #define. If the file is included more than once, the second and subsequent times the nameis defined, the #ifndef directive is false, so the compiler skips the bulk of the file and does not try to compile thedefinitions more than once.
BAD ADVICE
A few books and programmers recommend putting the conditional directives in the file that does the #include-ing. For example, they maintain that main.cpp should begin like this: #ifndef GENERATE_ID_HPP_ #include "generate_id.hpp" #endif They are wrong. The name GENERATE_ID_HPP_ is an internal, private detail of the included file. You should never use it outside of that file, and no one else should ever use that name. The purported reason to export the guard name and use it outside of the file is to improve compilation speed by eliminating the compiler’s need to open and process the file that would otherwise be included. With modern compilers, the improvement is negligible. A better solution is for you to use pre-compiled headers. With large, complicated programs, much of the compiler’s time and effort is spent compiling included files. In other words, the compiler spends a lot of its time recompiling the same definitions over and over again. The idea of using pre-compiled headers is that the compiler saves some information about the definitions it sees in an included file. When you include the same file in other source files, the compiler fetches its pre-compiled data and saves time by avoiding a recompilation of the same definitions. This compiler hack is completely unrelated to the C++ language and is purely an artifact of the compiler program. Check your compiler’s documentation, to learn how to set up and use pre-compiled headers.
290 EXPLORATION 41 ■ Using Multiple Source Files
Forward DeclarationsThe <istream> header contains the full declaration of std::istream and other, related declarations, and <ostream>declares std::ostream. These are large classes in large headers. Sometimes, you don’t need the full class declarations.For example, in vital_stats.hpp, the only use of std::ostream is to declare a reference parameter to the print()member function. The compiler doesn’t have to know all the details of std::ostream; it merely needs to know thatstd::ostream is a class. The header <iosfwd> is a small header that declares the names std::istream, std::ostream, etc., withoutproviding the complete class declarations. Thus, you can reduce compilation time for any file that includesvital_stats.hpp by changing <istream> and <ostream> to <iosfwd>. You can do the same for your own classes by declaring the class name after the class keyword, with nothing elsedescribing the class: class ostream; This is known as a forward declaration. You can use a forward declaration when the compiler has to know a nameis a class but doesn’t have to know the size of the class or any of the class’s members. A common case is using a classsolely as a reference function parameter. If your header uses <iosfwd> or other forward declarations, be sure to include the full class declarations(e.g., <iostream>) in the .cpp source file.
DocumentationRecall the Doxygen tool from Exploration 27. With headers, you now face the problem that you have two places todocument certain entities: the declaration in the header file, and the definition in the source file. One option is to putthe documentation in the header file. The documentation is usually aimed at the user of the entity, and the header isthe proper place to document the interface. Another option is to document the public interface in the header file and the private implementation details in thesource file. The Doxygen tool cannot merge documentation from two sources for a single entity. Instead, you shouldtake advantage of its conditional processing features to compile a set of interface documentation and a separate set ofimplementation documentation. Listing 41-9 shows the final version of the vital_stats.hpp header file.
291EXPLORATION 41 ■ Using Multiple Source Files
/// @param num a serial number, for use when prompting the user /// @return true for success or false for eof or input failure bool read(std::istream& in, int num); /// Print this record to @p out. /// @param out the output stream /// @param threshold mark records that have a BMI >= this threshold void print(std::ostream& out, int threshold) const; /// Return the BMI. int bmi() const { return bmi_; } /// Return the height in centimeters. int height() const { return height_; } /// Return the weight in kilograms. int weight() const { return weight_; } /// Return the sex: 'M' for male or 'F' for female char sex() const { return sex_; } /// Return the person's name. std::string const& name() const { return name_; } private: /// Return BMI, based on height_ and weight_ /// This is called only from read(). int compute_bmi() const; int height_; ///< height in centimeters int weight_; ///< weight in kilograms int bmi_; ///< Body-mass index char sex_; ///< 'M' for male or 'F' for female std::string name_; ///< Person's name}; #endif Listing 41-10 presents the implementation in vital_stats.cpp.
292 EXPLORATION 41 ■ Using Multiple Source Files
int vital_stats::compute_bmi()const{ return static_cast<int>(weight_ * 10000 / (height_ * height_) + 0.5);} bool vital_stats::read(std::istream& in, int num){ std::cout << "Name " << num << ": "; if (not std::getline(in, name_)) return false; std::cout << "Height (cm): "; if (not (in >> height_)) return false; skip_line(in); std::cout << "Weight (kg): "; if (not (in >> weight_)) return false; skip_line(in); std::cout << "Sex (M or F): "; if (not (in >> sex_)) return false; skip_line(in); sex_ = std::toupper(sex_, std::locale{}); bmi_ = compute_bmi(); return true;} void vital_stats::print(std::ostream& out, int threshold)const{ out << std::setw(6) << height_ << std::setw(7) << weight_ << std::setw(3) << sex_ << std::setw(6) << bmi_; if (bmi_ >= threshold) out << '*'; else out << ' '; out << ' ' << name_ << '\n';}
extern VariablesIf you define a global variable, and you want to use it in multiple files, you need a declaration in every file that uses thevariable. The most common use for global objects is for useful constants. For example, one of the omissions many scientificprogrammers notice when they first begin to use C++ is that the standard math header, <cmath>, lacks a definition for p.
293EXPLORATION 41 ■ Using Multiple Source Files
Suppose you decide to remedy this oversight by creating your own header file, math.hpp. This header file contains thedeclarations for pi and other useful constants. Declare a global variable by using the extern keyword, followed by thevariable’s type and name. Write math.hpp, with proper #include guards and a declaration for a double constant named pi. Compareyour file with Listing 41-11.
inline FunctionsTypically, you declare functions in a header file and define them in a separate source file, which you then link withyour program. This is true for free functions and for member functions. Thus, most member functions are definedseparately from the class. Inline functions follow different rules than ordinary functions. Any source file that calls an inline functionrequires the function’s definition. Each source file that uses an inline function must have no more than one definitionof that inline function, and every definition in the program must be the same. Thus, the rule for functions in headerfiles is slightly more complicated: the header file contains declarations for non-inline functions and definitions forinline functions. The separate source file defines only the non-inline functions. Inline functions have their uses, but they also have some significant drawbacks. • Increased compilation time. With inline functions, the header file is larger, and the compiler does more work for every compilation in the project. The problem grows as the number of source files grows. • More frequent recompilation. If you modify a function body, only that body really needs to be recompiled. However, if the function body is in a header file, you end up recompiling every source file that includes that header. By putting function bodies in a separate source file, only that file needs to be recompiled, and you can save time. In large projects, the amount of time you save can be significant. Separating function declarations and definitions makes sense in real programs, but it complicates thisbook. Instead of a single code listing, sometimes I will need multiple code listings. Nonetheless, I’ll do my best todemonstrate good programming practices in the judicious use of inline functions and the separation of declarationsand definitions.
294 EXPLORATION 41 ■ Using Multiple Source Files
One-Definition RuleThe compiler enforces the rule that permits one definition of a class, function, or object per source file. Another ruleis that you can have only one definition of a function or global object in the entire program. You can define a class inmultiple source files, provided the definition is the same in all source files. As mentioned in the previous section, inline functions follow different rules than ordinary functions. You candefine an inline function in multiple source files. Each source file must have no more than one definition of the inlinefunction, and every definition in the program must be the same. These rules are collectively known as the One-Definition Rule (ODR). The compiler enforces the ODR within a single source file. However, the standard does not require a compiler orlinker to detect any ODR violations that span multiple source files. If you make such a mistake, the problem is all yoursto find and fix. Imagine that you are maintaining a program, and part of the program is the header file shown in Listing 41-13.
The problem is that the order of the data member declarations is different from the order of the data members inthe constructors’ initializer lists. It’s a minor error, but one that can lead to confusion, or worse, in more complicatedclasses. It’s a good idea to ensure the orders are the same. Suppose, you decide to fix the problem by reordering thedata members. Then you recompile the program, but the program fails in mysterious ways. Some of your regression tests passand some fail, including trivial tests that have never failed in the past. What went wrong? _____________________________________________________________ _____________________________________________________________
295EXPLORATION 41 ■ Using Multiple Source Files
With such limited information, you can’t determine for certain what went wrong, but the most likely scenario isthat the recompilation failed to capture all the source files. Some part of the program (not necessarily the part that isfailing) is still using the old definition of the point class, and other parts of the program use the new definition. Theprogram fails to adhere to the ODR, resulting in undefined behavior. Specifically, when the program passes a pointobject from one part of the program to another, one part of the program stores a value in x_, and another part readsthe same data member as y_. This is only one small example of how ODR violations can be both subtle and terrible at the same time. Byensuring that all class definitions are in their respective header files, and that any time you modify a header file yourecompile all dependent source files, you can avoid most accidental ODR violations. Now that you have the tools needed to start writing some serious programs, it’s time to embark on some moreadvanced techniques. The next Exploration introduces function objects—a powerful technique for using the standardalgorithms.
296EXPLORATION 42
Function Objects
Classes have many, many uses in C++ programs. This Exploration introduces one powerful use of classes to replacefunctions. This style of programming is especially useful with the standard algorithms.
297EXPLORATION 42 ■ Function Objects
Function ObjectsA function object or functor is an object of class type for a class that overloads the function call operator. Informally,programmers sometimes also speak of the class as a “function object,” with the understanding that the actual functionobjects are the variables defined with that class type. C++ 03 programs often used functors, but C++ 11 has lambdas, which are much easier to read and write.(Recall lambdas from Exploration 23?) So what do functors offer that lambdas lack? To answer that question, considerthe following problem. Suppose you need a vector that contains integers of increasing value. For example, a vector of size 10 wouldcontain the values 1, 2, 3, . . . , 8, 9, 10. The std::generate algorithm takes an iterator range and calls a function orfunctor for each element of the range, assigning the result of the functor to successive elements. Write a lambdato use as the final argument to std::generate. (Construct a vector of a specific size by passing the desired size toa constructor. Remember to use parentheses instead of curly braces to call the right constructor.) Compare yoursolution with mine in Listing 42-4.
299EXPLORATION 42 ■ Function Objects
int first{}; std::cout << "What is the first integer? "; std::cin >> first; int step{}; std::cout << "What is the interval between successive integers? "; std::cin >> step; std::vector<int> data(size); // Generate the integers to fill the vector. std::generate(data.begin(), data.end(), sequence(first, step)); // Print the resulting integers, one per line. std::copy(data.begin(), data.end(), std::ostream_iterator<int>(std::cout, "\n"));} Compare your solution with mine, shown in Listing 42-6.
300EXPLORATION 43
Useful Algorithms
The standard library includes a suite of functions, which the library calls algorithms, to simplify many programmingtasks that involve repeated application of operations over data ranges. The data can be a container of objects, a portionof a container, values read from an input stream, or any other sequence of objects that you can express with iterators.I’ve introduced a few algorithms when appropriate. This Exploration takes a closer look at a number of the mostuseful algorithms.
SearchingThe standard algorithms include many flavors of searching, divided into two broad categories: linear and binary.The linear searches examine every element in a range, starting from the first and proceeding to subsequent elementsuntil reaching the end (or the search ends because it is successful). The binary searches require the elements besorted in ascending order, using the < operator, or according to a custom predicate, that is, a function, functor, orlambda that returns a Boolean result.
301EXPLORATION 43 ■ Useful Algorithms
else { *iter = 0; std::cout << "Value 42 changed to 0:\n"; write_data(data); }} Listing 43-2 shows the data.hpp file, which provides a few utilities for working with vectors of integers. Most ofthe examples in this Exploration will #include this file.
302 EXPLORATION 43 ■ Useful Algorithms
A companion to the find algorithm is find_if. Instead of searching for a matching value, find_if takes a predicatefunction or function object (from now on, I will write functor to indicate a free function, a function object, or a lambda).It calls the functor for every element in the range, until the functor returns true (or any value that can be convertedautomatically to true, such as a nonzero numeric value). If the functor never returns true, find_if returns the enditerator. Every search algorithm comes in two forms. The first compares items using an operator (== for linear searches and< for binary searches). The second form uses a caller-supplied functor instead of the operator. For most algorithms, thefunctor is an additional argument to the algorithm, so the compiler can distinguish the two forms. In a few cases, bothforms take the same number of arguments, and the library uses distinct names, because the compiler could not otherwisedistinguish between the two forms. In these cases, the functor form has _if added to the name, such as find and find_if. Suppose you want to search a vector of integers, not for a single value, but for any value that falls within acertain range. You can write a custom predicate to test a hard-coded range, but a more useful solution is to writea general-purpose functor that compares an integer against any range. Use this functor by supplying the rangelimits as argument to the constructor. Is this best implemented as a free function, function object, or lambda?________________________ Because it must store state, I recommend writing a functor. A lambda is good when you need to search for specific values, but a functor is easier if you want to write a genericcomparator that can store the limits. Write the intrange functor. The constructor takes two int arguments. Thefunction call operator takes a single int argument. It returns true, if the argument falls within the inclusive rangespecified in the constructor, or false, if the argument lies outside the range. Listing 43-3 shows my implementation of intrange. As a bonus, I decided to allow the caller to specify the rangelimits in either order. That way, I neatly avoid the issue of error-checking and error-handling, if the caller tries to usea meaningless range, such as [10, 0].
303EXPLORATION 43 ■ Useful Algorithms
Listing 43-4. Using find_if and intrange to Find an Integer That Lies Within a Range#include <algorithm>#include <iostream> #include "data.hpp"#include "intrange.hpp" int main(){ intvector data{}; read_data(data); write_data(data); auto iter(std::find_if(data.begin(), data.end(), intrange{10, 20})); if (iter == data.end()) std::cout << "No values in [10,20] found\n"; else std::cout << "Value " << *iter << " in range [10,20].\n";} A few of the following examples generate random data and apply algorithms to the data. The standard library hasa rich, complicated library for generating pseudo-random numbers. The details of this library are beyond the scopeof this book. Only the mathematically adventuresome should crack open the details of the <random> header. For yourconvenience, Listing 43-5 presents the randomint.hpp header, which defines the randomint class, which generatesrandom integers in a caller-supplied range.
304 EXPLORATION 43 ■ Useful Algorithms
Listing 43-6. Finding a Sub-range That Matches the First Four Digits of p#include <algorithm>#include <cstdlib>#include <iostream>#include <iterator>#include <vector> #include "data.hpp"#include "randomint.hpp" int main(){ intvector pi{ 3, 1, 4, 1 }; intvector data(10000); // The randomint functor generates random numbers in the range [0, 9].
305EXPLORATION 43 ■ Useful Algorithms
WHAT’S IN A NAME?
The find function performs a linear search for a single item. The search function performs a linear search for a matching series of items. So why isn’t binary_search called binary_find? On the other hand, find_end searches for the match that is furthest right in a range of values, so why isn’t it called search_end? The equal function is completely different from equal_range, in spite of the similarity in their names. The C++ standards committee did its best to apply uniform rules for algorithm names, such as appending _if to functions that take a functor argument but cannot be overloaded, but it faced some historical constraints with a number of names. What this means for you is that you have to keep a reference close at hand. Don’t judge a function by its name, but read the description of what the function does and how it does it, before you decide whether it’s the right function to use.
The lower_bound function is similar to binary_search, except it returns an iterator. The iterator points to the firstoccurrence of the value, or it points to a position where the value belongs if you want to insert the value into the rangeand keep the values in sorted order. The upper_bound function is similar to lower_bound, except it returns an iteratorthat points to the last position where you can insert the value and keep it in sorted order. If the value is found, thatmeans upper_bound points to one position past the last occurrence of the value in the range. To put it another way, therange [lower_bound, upper_bound]) is the sub-range of every occurrence of the value in the sorted range. As with anyrange, if lower_bound == upper_bound, the result range is empty, which means the value is not in the search range. Listing 43-7 shows a variation on Listing 43-1, sorting the integer vector and searching for a value usinglower_bound to perform a binary search.
306 EXPLORATION 43 ■ Useful Algorithms
Fill in Table 43-1 with the expected values for the lower and upper bounds of each value. Then run the programto check your answers.
307EXPLORATION 43 ■ Useful Algorithms
Value Expected Lower Bound Expected Upper Bound Actual Lower Bound Actual Upper Bound348010
You have already seen how min returns the minimum of two values. It has a partner, min_element, which takesan iterator range and returns the iterator for the minimum value in the range. Ditto for max_element. You can guesswhat minmax_element returns: a pair of iterators for the minimum and maximum values in the range. All three comein the usual overloaded forms: one uses the < operator, and the other takes an additional argument for a comparisonpredicate. Unlike all the other algorithms, min, max, and minmax also have a form that takes a curly brace–enclosed list ofvalues and returns the minimum, maximum, or both of those values. These are the only algorithms that take a singlecurly brace–enclosed list as a single argument. The only constraint is that all the values must have the same type.For example, the following int a{1}, b{2}, c{3};int smallest{ std::min({ a, b, c }) }; is equivalent to the traditional style of using iterators int a{1}, b{2}, c{3};std::vector<int> tmp{ a, b, c};int smallest{ *std::min_element(tmp.begin(), tmp.end()) };
ComparingTo check whether two ranges are equal, that is, that they contain the same values, call the equal algorithm. This algorithmtakes a start and one-past-the-end iterator for one range and the start of the second range. You must ensure that the tworanges have the same size. If every element of the two ranges is equal, it returns true. If any element doesn’t match, itreturns false. The function has two forms: pass only the iterators to equal, and it compares elements with the == operator;pass a comparison functor as the last argument, and equal compares elements by calling the functor. The first argumentto the functor is the element from the first range, and the second argument is the element from the second range. The mismatch function is the opposite. It compares two ranges and returns a std::pair of iterators that refer tothe first elements that do not match. The first member in the pair is an iterator that refers to an element in the firstrange, and the second member refers to the second range. If the two ranges are equal, the return value is a pair ofend iterators. The lexicographical_compare algorithm sets the record for the longest algorithm name. It compares two rangesand determines whether the first range is “less than” the second. It does this by comparing the ranges one elementat a time. If the ranges are equal, the function returns false. If the ranges are equal up to the end of one range, and theother range is longer, the shorter range is less than the longer range. If an element mismatch is found, whichever rangecontains the smaller element is the smaller range. All elements are compared using the < operator (or a caller-suppliedpredicate) and checked for equivalence, not equality. Recall that elements a and b are equivalent if the following is true: not (a < b) and not (b < a) If you apply lexicographical_compare to two strings, you get the expected less-than relationship, which explainsthe name. In other words, if you call this algorithm with the strings "hello" and "help", it returns true; if you call itwith "help" and "hello", it returns false; and if you call it with "hel" and "hello", it returns true. Write a test program that reads two sequences of integers into separate vectors. (Remember to clear the stateafter reading the first vector’s data.) Then test the equal, mismatch, and lexicographical_compare functions on thetwo ranges. Remember that equal and mismatch require their input ranges to have the same size. You can ensure thatyou compare only the number of elements in the shorter vector by computing the end iterator instead of calling theend() member function.
309EXPLORATION 43 ■ Useful Algorithms
std::equal(data1.begin(), data1.begin() + std::min(data1.size(), data2.size()), data2.begin()) Not all iterators allow addition, but a vector’s iterators do allow it. Adding an integer n to begin() offsets theiterator as though you had advanced it n times with the ++ operator. (Discover more about iterators in the nextExploration.) Table 43-2 lists some suggested input data sets.
310 EXPLORATION 43 ■ Useful Algorithms
Rearranging DataYou’ve already seen the sort algorithm many times. Other algorithms are also adept at rearranging values in a range.The merge algorithm merges two sorted input ranges into a single output range. As always, you must ensure theoutput range has sufficient room to accept the entire merged result from both input ranges. The two input ranges canbe different sizes, so merge takes five or six arguments: two for the first input range, two for the second input range,one for the start of the output range, and an optional argument for a functor to use instead of the < operator. The replace algorithm scans an input range and replaces every occurrence of an old value with a new value.The replacement occurs in place, so you specify the range with the usual pair of iterators, but no write iterator. Thereplace_if function is similar but takes a predicate instead of an old value. Write a program that reads a vector ofintegers and replaces all occurrences of values in the range [10, 20] with 0. Reuse the intrange functor class orwrite a lambda. Compare your program with mine in Listing 43-10.
Listing 43-10. Using replace_if and intrange to Replace All Integers in [10, 20] with 0#include <algorithm> #include "data.hpp"#include "intrange.hpp" int main(){ intvector data{}; read_data(data); write_data(data); std::replace_if(data.begin(), data.end(), intrange{10, 20}, 0); write_data(data);} Listing 43-11 shows the same program using a lambda.
Listing 43-11. Using replace_if and intrange to Replace All Integers in [10, 20] with 0#include <algorithm> #include "data.hpp"#include "intrange.hpp" int main(){ intvector data{}; read_data(data); write_data(data);
311EXPLORATION 43 ■ Useful Algorithms
std::replace_if(data.begin(), data.end(), [](int x) { return x >= 10 and x <= 20; }, 0); write_data(data);} A fun algorithm is random_shuffle, which shuffles elements in place into random order. This function takes twoarguments, specifying the range to shuffle. Another form of the function takes three arguments. The final argument isa functor that returns a random number in the range [0, n], where n is the size of the input range, or it can be a uniformrandom number generator from the standard library (similar to what randomint uses in Listing 43-5). Use the sequence.hpp file (from Listing 42-6) and generate a vector of 100 sequential integers. Then shuffle it intorandom order and print it. Compare your solution with mine in Listing 43-12.
312 EXPLORATION 43 ■ Useful Algorithms
The binary form of transform takes an input range, the start of a second input range (it assumes the size is thesame as the size of the first input range), the start of an output range, and a binary functor. The functor is called for eachelement in the input ranges; the first argument comes from the first input range, and the second argument comes fromthe second input range. As with the unary form, the function copies the result to the output range, which can be thesame as either input range. Note that the types of the two input ranges do not have to be the same.
Copying DataSome algorithms operate in place, and others copy their results to an output range. For example, reverse reversesitems in place, and reverse_copy leaves the input range intact and copies the reversed items to an output range.If a copying form of an algorithm exists, its name has _copy appended. (Unless it is also a predicate form of a function,in which case it has _if appended after _copy, as in replace_copy_if.) In addition to just plain copy, which you’ve seen many times already, the standard library offers copy_backward,which makes a copy but starts at the end and works toward the beginning, preserving the original order; copy_n,which takes the start of a range, a count, and a write iterator; and copy_if, which is like copy but takes a predicate andcopies an element only if the predicate returns true. Distinguish copy_backward from reverse_copy. The latter startsat the beginning and works toward the end of the input range but copies the values into reverse order. If you have to move elements instead of copy them, call std::move or std::move_backward. This std::move isdifferent from the one you encountered in Exploration 39. This one is declared in <algorithm>. Like copy, the movealgorithm takes two read iterators and a write iterator. It calls the other form of std::move for each element of theinput range, moving the element into the output range. As with all algorithms that write output, it is your responsibility to ensure the output range is large enough tohandle everything you write to it. Some implementations of the standard library offer debugging modes to help detectviolations of this rule. If your library offers such a feature, by all means, take full advantage of it.
Deleting ElementsThe trickiest algorithms to use are those that “remove” elements. As you learned in Exploration 22, algorithms suchas remove don’t actually delete anything. Instead, they rearrange the elements in the range, so that all the elementsslated for removal are packed at the end of the range. You can then decide either to use the sub-range of elements youwant to keep or erase the “removed” elements by calling the erase member function. The remove function takes an iterator range and a value, and it removes all elements equal to that value. You canalso use a predicate with remove_if to remove all elements for which a predicate returns true. These two functionshave copying counterparts that don’t rearrange anything but merely copy the elements that are not being removed:remove_copy copies all the elements that are not equal to a certain value and remove_copy_if copies all elements forwhich a predicate returns false. Another algorithm that removes elements is unique (and unique_copy). It takes an input range and removes alladjacent duplicates, thereby ensuring that every item in the range is unique. (If the range is sorted, then all duplicatesare adjacent.) Both functions can take a comparison functor instead of using the default == operator. Write a program that reads integers into a vector, erases all elements equal to zero, copies only thoseelements that lie in the range [24, 42] to another vector, sorts the other vector, and removes duplicates. Print theresulting vector. My solution is in Listing 43-13.
313EXPLORATION 43 ■ Useful Algorithms
int main(){ intvector data{}; read_data(data); data.erase(std::remove(data.begin(), data.end(), 0), data.end()); intvector copy{}; std::remove_copy_if(data.begin(), data.end(), std::back_inserter(copy), [](int x) { return x< 24 or x > 42; }); std::sort(copy.begin(), copy.end()); copy.erase(std::unique(copy.begin(), copy.end()), copy.end()); write_data(copy);}
IteratorsAlgorithms and iterators are closely related. All the algorithms (except min, max, and minmax) take two or moreiterators as arguments. To use algorithms effectively, you must understand iterators. Therefore, the next Explorationwill help you master iterators, all five flavors. That’s right. Iterators come in five different varieties. Keep reading tolearn more.
314EXPLORATION 44
Iterators
Iterators provide element-by-element access to a sequence of things. The things can be numbers, characters, orobjects of almost any type. The standard containers, such as vector, provide iterator access to the container contents,and other standard iterators let you access input streams and output streams, for example. The standard algorithmsrequire iterators for operating on sequences of things. Until now, your view and use of iterators has been somewhat limited. Sure, you’ve used them, but do you reallyunderstand them? This Exploration helps you understand what’s really going on with iterators.
Kinds of IteratorsSo far, you have seen that iterators come in multiple varieties, in particular, read and write. The copy function, for example,takes two read iterators to specify an input range and one write iterator to specify the start of an output range. As always,specify the input range as a pair of read iterators: one that refers to the first element of the range and one that refers to theone-past-the-end element of the input range. The copy function returns a write iterator: the value of the result iterator afterthe copy is complete. WriteIterator copy(ReadIterator start, ReadIterator end, WriteIterator result); All this time, however, I’ve oversimplified the situation by referring to “read” and “write” iterators. In fact, C++has five different categories of iterators: input, output, forward, bidirectional, and random access. Input and outputiterators have the least functionality, and random access has the most. You can substitute an iterator with morefunctionality anywhere that calls for an iterator with less. Figure 44-1 illustrates the substitutability of iterators. Don’tbe misled by the figure, however. It does not show class inheritance. What makes an object an iterator is its behavior.If it fulfills all the requirements of an input iterator, for example, it is an input iterator, regardless of its type.
All iterators can be copied and assigned freely. The result of a copy or an assignment is a new iterator that refersto the same item as the original iterator. The other characteristics depend on the iterator category, as described in thefollowing sections.
315EXPLORATION 44 ■ Iterators
Input IteratorsAn input iterator, unsurprisingly, supports only input. You can read from the iterator (using the unary * operator) onlyonce per iteration. You cannot modify the item that the iterator refers to. The ++ operator advances to the next inputitem. You can compare iterators for equality and inequality, but the only meaningful comparison is to compare aniterator with an end iterator. You cannot, in general, compare two input iterators to see if they refer to the same item. That’s about it. Input iterators are quite limited, but they are also extremely useful. Almost every standardalgorithm expresses an input range in terms of two input iterators: the start of the input range and one past the end ofthe input range. The istream_iterator type is an example of an input iterator. You can also treat any container’s iterator as aninput iterator; e.g., the iterator that a vector’s begin() member function returns.
Output IteratorsAn output iterator supports only output. You can assign to an iterator item (by applying the * operator to the iteratoron the left-hand side of an assignment), but you cannot read from the iterator. You can modify the iterator value onlyonce per iteration. The ++ operator advances to the next output item. You cannot compare output iterators for equality or inequality. In spite of the limitations on output iterators, they, too, are widely used by the standard algorithms. Everyalgorithm that copies data to an output range takes an output iterator to specify the start of the range. One caution when dealing with output iterators is that you must ensure that wherever the iterator isactually writing has enough room to store the entire output. Any mistakes result in undefined behavior. Someimplementations offer debugging iterators that can check for this kind of mistake, and you should certainly takeadvantage of such tools when they are available. Don’t rely solely on debugging libraries, however. Careful codedesign, careful code implementation, and careful code review are absolutely necessary to ensure safety when usingoutput (and other) iterators. The ostream_iterator type is an example of an output iterator. You can also treat many container’s iterators asoutput iterators; e.g., the iterator that a vector’s begin() member function returns.
Forward IteratorsA forward iterator has all the functionality of an input iterator and an output iterator, and a little bit more. You canfreely read from, and write to, an iterator item (still using the unary * operator), and you can do so as often as youwish. The ++ operator advances to the next item, and the == and != operators can compare iterators to see if they referto the same item or to the end position. Some algorithms require forward iterators instead of input iterators. I glossed over that detail in the previousExplorations, because it rarely affects you. For example, the binary search algorithms require forward iterators tospecify the input range, because they might have to refer to a particular item more than once. That means you cannotdirectly use an istream_iterator as an argument to, say, lower_bound, but then you aren’t likely to try that in a realprogram. All the containers’ iterators meet the requirements of forward iterators, so in practical terms, this restrictionhas little impact.
Bidirectional IteratorsA bidirectional iterator has all the functionality of a forward iterator, but it also supports the -- operator, which movesthe iterator backward one position to the previous item. As with any iterator, you are responsible for ensuring that younever advance the iterator past the end of the range or before the beginning. The reverse and reverse_copy algorithms (and a few others) require bidirectional iterators. Most of thecontainers’ iterators meet at least the requirements of bidirectional iterators, so you rarely have to worry aboutthis restriction.
316 EXPLORATION 44 ■ Iterators
317EXPLORATION 44 ■ Iterators
318 EXPLORATION 44 ■ Iterators
The data vector is filled with even numbers, starting at 0. The iterator, iter, initially refers to the first elementof the vector, namely, 0. The iterator advances four positions, to value 8, and then back two positions, to 4. So theoutput is
8, 4
Declaring variables to store iterators is clumsy. The type names are long and cumbersome. Therefore, I often useauto to define a variable. If I really need to name a type, say, because I have to write a function that takes an iteratorparameter, I use typedef declarations to create shorter aliases for the long, clumsy type names, as follows: typedef std::vector<std::string>::iterator strvec_iterator; std::vector<std::string> lines(2, "hello");strvec_iterator iter{lines.begin()};std::string first{*iter}; // dereference the first itemstd::size_t size{iter->size()}; // dereference and call a member function
319EXPLORATION 44 ■ Iterators
If you aren’t sure, try a test. Read Listing 44-3 and predict its output.
320 EXPLORATION 44 ■ Iterators
Error MessagesWhen you compiled Listing 44-3, the compiler issued an error message, or diagnostic, as the C++ standard writers callit. For example, the compiler that I use every day, g++, prints the following:
321EXPLORATION 44 ■ Iterators
So what does all that gobbledygook mean? Although a C++ expert can figure it out, it may not be much help toyou. Buried in the middle is the line number and source file that identify the source of the error. That’s where you haveto start looking. The compiler didn’t detect the error until it started working through various #include files. These filenames depend on the implementation of the standard library, so you can’t always tell from those file names what isthe actual error. In this case, the error arises from within the std::advance function. That’s when the compiler detects thatit has a const iterator, but it does not have any functions that work with a const iterator. Instead of complainingabout the const-ness, however, all it manages to do is to complain that it lacks a “match” for the function it seeks.That means it is looking for argument types that match the parameter types, to resolve an overloaded operator.Because a const argument cannot match a non-const parameter, the compiler failed to find an overload thatmatches the arguments. Don’t give up hope for ever understanding C++ compiler error messages. By the end of the book, you will havegained quite a bit more knowledge that will help you understand how the compiler and library really work, and thatunderstanding will help you make sense of these error messages. If you can, compile your code with clang++, whichdoes a good job of issuing helpful messages. Even if your production code must be compiled with a different compiler,you can use clang++ to help diagnose problems. My advice for dealing with the deluge of confusing error messages is to start by finding the first mention of yoursource file. That should tell you the line number that gives rise to the problem. Check the source file. You might see anobvious mistake. If not, check the error message text. Ignore the “instantiated from here” and similar messages. Try tofind the real error message, which often starts with error: instead of warning: or note:.
Specialized IteratorsThe <iterator> header defines a number of useful, specialized iterators, such as back_inserter, which you’ve seenseveral times already. Strictly speaking, back_inserter is a function that returns an iterator, but you rarely have toknow the exact iterator type. In addition to back_inserter, you can also use front_inserter, which also takes a container as an argumentand returns an output iterator. Every time you assign a value to the dereferenced iterator, it calls the container’spush_front member function to insert the value at the start of the container. The inserter function takes a container and an iterator as arguments. It returns an output iterator that calls thecontainer’s insert function. The insert member function requires an iterator argument, specifying the positionat which to insert the value. The inserter iterator initially passes its second argument as the insertion position. Aftereach insertion, it updates its internal iterator, so subsequent insertions go into subsequent positions. In other words,inserter just does the right thing.
322 EXPLORATION 44 ■ Iterators
Other specialized iterators include istream_iterator and ostream_iterator, which you’ve also seen.An istream_iterator is an input iterator that extracts values from a stream when you dereference the iterator.With no arguments, the istream_iterator constructor creates an end-of-stream iterator. An iterator is equal tothe end-of-stream iterator when an input operation fails. An ostream_iterator is an output iterator. The constructor takes an output stream and an optional string asarguments. Assigning to the dereferenced iterator writes a value to the output stream, optionally followed by the string(from the constructor). Another specialized iterator is thereverse_iterator class. It adapts an existing iterator (called the base iterator),which must be bidirectional (or random access). When the reverse iterator goes forward (++), the base iteratorgoes backward (--). Containers that support bidirectional iterators have rbegin() and rend() member functions,which return reverse iterators. The rbegin() function returns a reverse iterator that points to the last element of thecontainer, and rend() returns a special reverse iterator value that represents one position before the beginning ofthe container. Thus, you treat the range [rbegin(), rend()] as a normal iterator range, expressing the values of thecontainer in reverse order. C++ doesn’t permit an iterator to point to one position before the beginning, so reverse iterators have asomewhat funky implementation. Ordinarily, implementation details don’t matter, but reverse_iterator exposesthis particular detail in its base() member function, which returns the base iterator. I could tell you what the base iterator actually is, but that would deprive you of the fun. Write a program toreveal the nature of the reverse_iterator’s base iterator. (Hint: Fill a vector with a sequence of integers. Use areverse iterator to get to the middle value. Compare with the value of the iterator’s base() iterator.) If a reverse_iterator points to position x of a container, what does its base() iterator point to? _____________________________________________________________ If you did not answer x + 1, try running the program in Listing 44-6.
323EXPLORATION 44 ■ Iterators
Now do you see? The base iterator always points to one position after the reverse iterator’s position. That’s thetrick that allows rend() to point to a position “before the beginning,” even though that’s not allowed. Under the hood,the rend() iterator actually has a base iterator that points at the first item in the container, and the reverse_iterator’simplementation of the * operator performs the magic of taking the base iterator, retreating one position, and thendereferencing the base iterator. As you can see, iterators are a little more complicated than they initially seem to be. Once you understand howthey work, however, you will see that they are actually quite simple, powerful, and easy-to-use. But first, it’s timeto pick up a few more important C++ programming techniques. The next Exploration introduces exceptions andexception-handling, necessary topics for properly handling programmer and user errors.
324EXPLORATION 45
Exceptions
You may have been dismayed by the lack of error checking and error handling in the Explorations so far. That’s aboutto change. C++, like most modern programming languages, supports exceptions as a way to jump out of the normalflow of control in response to an error or other exceptional condition. This Exploration introduces exceptions: how tothrow them, how to catch them, when the language and library use them, and when and how you should use them.
Introducing ExceptionsExploration 9 introduced vector’s at member function, which retrieves a vector element at a particular index. Atthe time, I wrote that most programs you read would use square brackets instead. Now is a good time to examine thedifference between square brackets and the at function. First, take a look at two programs. Listing 45-1 shows a simpleprogram that uses a vector.
325EXPLORATION 45 ■ Exceptions
326 EXPLORATION 45 ■ Exceptions
Catching ExceptionsAn exception handler is said to catch an exception. Write an exception handler at the end of a try: the try keywordis followed by a compound statement (it must be compound), followed by a series of handlers. Each handler startswith a catch keyword, followed by parentheses that enclose the declaration of an exception-handler object. After theparentheses is a compound statement that is the body of the exception handler. When the type of the exception object matches the type of the exception-handler object, the handler is deemed amatch, and the handler object is initialized with the exception object. The handler declaration is usually a reference,which avoids copying the exception object unnecessarily. Most handlers don’t have to modify the exception object, sothe handler declaration is typically a reference to const. A “match” is when the exception object’s type is the same asthe handler’s declared type or a class derived from the handler’s declared type, ignoring whether the handler is constor a reference. The exception-handling system destroys all objects that it constructed in the try part of the statement prior tothrowing the exception, then it transfers control to the handler, so the handler’s body runs normally, and controlresumes with the statement after the end of the entire try-catch statement, that is, after the statement’s last catchhandler. The handler types are tried in order, and the first match wins. Thus, you should always list the most specifictypes first and base class types later. A base class exception handler type matches any exception object of a derived type. To handle all exceptions thatthe standard library might throw, write the handler to catch std::exception (declared in <exception>), which is thebase class for all standard exceptions. Listing 45-3 demonstrates some of the exceptions that the std::string classcan throw. Try out the program by typing strings of varying length.
327EXPLORATION 45 ■ Exceptions
Throwing ExceptionsA throw expression throws an exception. The expression consists of the throw keyword followed by an expression,namely, the exception object. The standard exceptions all take a string argument, which becomes the value returnedfrom the what() member function. throw std::out_of_range("index out of range");
328 EXPLORATION 45 ■ Exceptions
The messages that the standard library uses for its own exceptions are implementation-defined, so you cannotrely on them to provide any useful information. You can throw an exception anywhere an expression can be used, sort of. The type of a throw expression is void,which means it has no type. Type void is not allowed as an operand for any arithmetic, comparison, etc., operator.Thus, realistically, a throw expression is typically used in an expression statement, all by itself. You can throw an exception inside a catch handler, which low-level code and libraries often do. You can throwthe same exception object or a brand-new exception. To re-throw the same object, use the throw keyword withoutany expression. catch (std::out_of_range const& ex){ std::cout << "index out of range\n"; throw;} A common case for re-throwing an exception is inside a catch-all handler. The catch-all handler performs someimportant cleanup work and then propagates the exception so the program can handle it. If you throw a new exception, the exception-handling system takes over normally. Control leaves the try-catchblock immediately, so the same handler cannot catch the new exception.
Program StackTo understand what happens when a program throws an exception, you must first understand the nature of the programstack, sometimes called the execution stack. Procedural and similar languages use a stack at runtime to keep track offunction calls, function arguments, and local variables. The C++ stack also helps keep track of exception handlers. When a program calls a function, the program pushes a frame onto the stack. The frame has information such asthe instruction pointer and other registers, arguments to the function, and possibly some memory for the function’sreturn value. When a function starts, it might set aside some memory on the stack for local variables. Each local scopepushes a new frame onto the stack. (The compiler might be able to optimize away a physical frame for some localscopes, or even an entire function. Conceptually, however, the following applies.) While a function executes, it typically constructs a variety of objects: function arguments, local variables,temporary objects, and so on. The compiler keeps track of all the objects the function must create, so it can properlydestroy them when the function returns. Local objects are destroyed in the opposite order of their creation. Frames are dynamic, that is, they represent function calls and the flow of control in a program, not the staticrepresentation of source code. Thus, a function can call itself, and each call results in a new frame on the stack, andeach frame has its own copy of all the function arguments and local variables. When a program throws an exception, the normal flow of control stops, and the C++ exception-handlingmechanism takes over. The exception object is copied to a safe place, off the execution stack. The exception-handlingcode looks through the stack for a try statement. When it finds a try statement, it checks the types for each handlerin turn, looking for a match. If it doesn’t find a match, it looks for the next try statement, farther back in the stack.It keeps looking until it finds a matching handler or it runs out of frames to search. When it finds a match, it pops frames off the execution stack, calling destructors for all local objects in eachpopped frame, and continues to pop frames until it reaches the handler. Popping frames from the stack is also calledunwinding the stack. After unwinding the stack, the exception object initializes the handler’s exception object, and then the catchbody is executed. After the catch body exits normally, the exception object is freed, and execution continues with thestatement that follows the end of the last sibling catch block. If the handler throws an exception, the search for a matching handler starts anew. A handler cannot handle anexception that it throws, nor can any of its sibling handlers in the same try statement. If no handler matches the exception object’s type, the std::terminate function is called, which aborts theprogram. Some implementations will pop the stack and free local objects prior to calling terminate, but others won’t.
329EXPLORATION 45 ■ Exceptions
Listing 45-4 can help you visualize what is going on inside a program when it throws and catches an exception.
330 EXPLORATION 45 ■ Exceptions
49 {50 ex.print("catch ");51 throw;52 }53 std::cout << "end count_down(" << n << ")\n";54 }5556 int main()57 {58 try59 {60 count_down(2);61 count_down(4);62 }63 catch (visual const ex)64 {65 ex.print("catch ");66 }67 std::cout << "All done!\n";68 } The visual class helps show when and how objects are constructed, copied, and destroyed. The count_downfunction throws an exception when its argument equals 3, and it calls itself when its argument is positive. Therecursion stops for non-positive arguments. To help you see function calls, it prints the argument upon entry to, andexit from, the function. The first call to count_down does not trigger the exception, so you should see normal creation and destruction ofthe local visual object. Write exactly what the program should print as a result of line 60 (count_down(2)). _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________ _____________________________________________________________
The next call to count_down from main (line 61) allows count_down to recurse once before throwing an exception.So count_down(4) calls count_down(3). The local object, v, is constructed inside the frame for count_down(4), and a
331EXPLORATION 45 ■ Exceptions
v: { id_= 4 } n: 3 count_down(3) return: line 48 v: { id_= 3 } n: 4 count_down(4) return: line 63
main()
new instance of v is constructed inside the frame for count_down(3). Then the exception object is created and thrown.(See Figure 45-1.) The exception is caught inside count_down, so its frame is not popped. The exception object is then copied to ex(line 48), and the exception handler begins. It prints a message and then re-throws the original exception object (line 51).The exception-handling mechanism treats this exception the same way it treats any other: the try statement’s frame ispopped, and then the count_down function’s frame is popped. Local objects are destroyed (including ex and v).The final statement in count_down does not execute. The stack is unwound, and the try statement inside the call to count_down(4) is found, and once again, theexception object is copied to a new instance of ex. (See Figure 45-2.) The exception handler prints a message andre-throws the original exception. The count_down(4) frame is popped, returning control to the try statement in main.Again, the final statement in count_down does not execute.
main( )
The exception handler in main gets its turn, and this handler prints the exception object one last time (line 63).After the handler prints a message, and the catch body reaches its end, the local exception object and the originalexception object are destroyed. Execution then continues normally on line 67. The final output is
332 EXPLORATION 45 ■ Exceptions
start count_down(2)visual(count_down local: 0)start count_down(1)visual(count_down local: 1)start count_down(0)visual(count_down local: 2)end count_down(0)~visual(count_down local: 2)end count_down(1)~visual(count_down local: 1)end count_down(2)~visual(count_down local: 0)start count_down(4)visual(count_down local: 3)start count_down(3)visual(count_down local: 4)visual(exception: 5)copy visual(exception: 5)catch visual(exception: 5)~visual(exception: 5)~visual(count_down local: 4)copy visual(exception: 5)catch visual(exception: 5)~visual(exception: 5)~visual(count_down local: 3)copy visual(exception: 5)catch visual(exception: 5)~visual(exception: 5)~visual(exception: 5)All done!
Standard ExceptionsThe standard library defines several standard exception types. The base class, exception, is declared in the<exception> header. Most of the other exception classes are defined in the <stdexcept> header. If you want to defineyour own exception class, I recommend deriving it from one of the standard exceptions in <stdexcept>. The standard exceptions are divided into two categories (with two base classes that derive directly from exception). • Runtime errors (std::runtime_error) are exceptions that you cannot detect or prevent merely by examining the source code. They arise from conditions that you can anticipate, but not prevent. • Logic errors (std::logic_error) are the result of programmer error. They represent violations of preconditions, invalid function arguments, and other errors that the programmer should prevent in code. The other standard exception classes in <stdexcept> derive from these two. Most standard library exceptionsare logic errors. For example, out_of_range inherits from logic_error. The at member function (and others) throwsout_of_range when the index is out of range. After all, you should check indices and sizes, to be sure your vector andstring usage are correct, and not rely on exceptions. The exceptions are there to provide clean, orderly shutdown ofyour program when you do make a mistake (and we all make mistakes).
333EXPLORATION 45 ■ Exceptions
Your library reference tells you which functions throw which exceptions, such as at can throw out_of_range.Many functions might throw other, undocumented exceptions too, depending on the library’s and compiler’simplementation. In general, however, the standard library uses few exceptions. Instead, most of the library yieldsundefined behavior when you provide bad input. The I/O streams do not ordinarily throw any exceptions, but you canarrange for them to throw exceptions when bad errors happen, as I explain in the next section.
I/O ExceptionsYou learned about I/O stream state bits in Exploration 31. State bits are important, but checking them repeatedly iscumbersome. In particular, many programs fail to check the state bits of output streams, especially when writing tothe standard output. That’s just plain, old-fashioned laziness. Fortunately, C++ offers an avenue for programmers togain I/O safety without much extra work: the stream can throw an exception when I/O fails. In addition to state bits, each stream also has an exception mask. The exception mask tells the stream to throwan exception if a corresponding state bit changes value. For example, you could set badbit in the exception maskand never write an explicit check for this unlikely occurrence. If a serious I/O error were to occur, causing badbit tobecome set, the stream would throw an exception. You can write a handler at a high level to catch the exception andterminate the program cleanly, as shown in Listing 45-5.
334 EXPLORATION 45 ■ Exceptions
Recall that when an input stream reaches the end of the input, it sets eofbit in its state mask. Although you canalso set this bit in the exceptions mask, I can’t see any reason why you would want to. If an input operation does notread anything useful from the stream, the stream sets failbit. The most common reasons that the stream might notread anything is end of file (eofbit is set) or an input formatting error (e.g., text in the input stream when the programtries to read a number). Again, it’s possible to set failbit in the exception mask, but most programs rely on ordinaryprogram logic to test the state of an input stream. Exceptions are for exceptional conditions, and end-of-file is anormal occurrence when reading from a stream. The loop ends when failbit is set, but you have to test further to discover whether failbit is set, because of anormal end-of-file condition or because of malformed input. If eofbit is also set, you know the stream is at its end.Otherwise, failbit must be owing to malformed input. As you can see, exceptions are not the solution for every error situation. Thus, badbit is the only bit in theexception mask that makes sense for most programs, especially for input streams. An output stream sets failbit if itcannot write the entire value to the stream. Usually, such a failure occurs because of an I/O error that sets badbit, butit’s at least theoretically possible for output failure to set failbit without also setting badbit. In most situations, anyoutput failure is cause for alarm, so you might want to throw an exception for failbit with output streams and badbitwith input streams. std::cin.exceptions(std::ios_base::badbit);std::cout.exceptions(std::ios_base::failbit);
Custom ExceptionsExceptions simplify coding by removing exceptional conditions from the main flow of control. You can and should useexceptions for many error situations. For example, the rational class (most recently appearing in Exploration 40) has,so far, completely avoided the issue of division by zero. A better solution than invoking undefined behavior (which iswhat happens when you divide by zero) is to throw an exception anytime the denominator is zero. Define your ownexception class by deriving from one of the standard exception base classes, as shown in Listing 45-6. By defining yourown exception class, any user of rational can easily distinguish its exceptions from other exceptions.
335EXPLORATION 45 ■ Exceptions
{ if (denominator_ == 0) throw zero_denominator{"zero denominator"}; reduce(); }... omitted for brevity ...};#endif Notice how the zero_denominator class nests within the rational class. The nested class is a perfectly ordinaryclass. It has no connection with the outer class (as with a Java inner class), except the name. The nested class getsno special access to private members in the outer class, nor does the outer class get special access to the nestedclass name. The usual rules for access levels determine the accessibility of the nested class. Some nested classes areprivate helper classes, so you would declare them in a private section of the outer class definition. In this case, zero_denominator must be public, so that callers can use the class in exception handlers. To use a nested class name outside the outer class, you must use the outer class and the nested class names,separated by a scope operator (::). The nested class name has no significance outside of the outer class’s scope. Thus,nested classes help avoid name collisions. They also provide clear documentation for the human reader who sees thetype in an exception handler: catch (rational::zero_denominator const& ex) { std::cerr << "zero denominator in rational number\n";} Find all other places in the rational class that have to check for a zero denominator and add appropriateerror-checking code to throw zero_denominator.
All roads lead to reduce(), so one approach is to replace the assertion with a check for a zero denominator, andthrow the exception there. You don’t have to modify any other functions, and even the extra check in the constructor(illustrated in Listing 45-6) is unnecessary. Listing 45-7 shows the latest implementation of reduce().
336 EXPLORATION 45 ■ Exceptions
If your program looks anything like mine in Listing 45-8, the catch is supposed to catch the exception, but itdoesn’t. The compiler trusted noexcept and did not generate the normal exception-handling code. As a result, whenfunction() throws an exception, the only thing the program can do is terminate immediately.
337EXPLORATION 45 ■ Exceptions
Exceptional AdviceThe basic mechanics of exceptions are easy to grasp, but their proper use is more difficult. The applicationsprogrammer has three distinct tasks: catching exceptions, throwing exceptions, and avoiding exceptions. You should write your programs to catch all exceptions, even the unexpected ones. One approach is for yourmain program to have a master try statement around the entire program body. Within the program, you might usetargeted try statements to catch specific exceptions. The closer you are to the source of the exception, the morecontextual information you have, and the better you can ameliorate the problem, or at least present the user withmore useful messages. This outermost try statement catches any exceptions that other statements miss. It is a last-ditch attempt topresent a coherent and helpful error message before the program terminates abruptly. At a minimum, tell the userthat the program is terminating because of an unexpected exception. In an event-driven program, such as a GUI application, exceptions are more problematic. The outermost trystatement shuts down the program, closing all windows. Most event handlers should have their own try statement tohandle exceptions for that particular menu pick, keystroke event, and so on. Within the body of your program, better than catching exceptions is avoiding them. Use the at member functionto access elements of a vector, but you should write the code so you are confident that the index is always valid. Indexand length exceptions are signs of programmer error. When writing low-level code, throw exceptions for most error situations that should not happen or that otherwisereflect programmer error. Some error conditions are especially dangerous. For example, in the rational class,a denominator should never be zero or negative after reduce() returns. If a condition arises when the denominatoris indeed zero or negative, the internal state of the program is corrupt. If the program were to attempt a gracefulshutdown, saving all files, etc., it might end up writing bad data to the files. Better to terminate immediately and relyon the most recent backup copy, which your program made while its state was still known to be good. Use assertions,not exceptions, for such emergencies. Ideally, your code should validate user input, check vector indices, and make sure all arguments to all functionsare valid before calling the functions. If anything is invalid, your program can tell the user with a clear, directmessage and avoid exceptions entirely. Exceptions are a safety net when your checks fail or you forget to check forcertain conditions. As a general rule, libraries should throw exceptions, not catch them. Applications tend to catch exceptionsmore than throw them. As programs grow more complex, I will highlight situations that call for exceptions,throwing or catching. Now that you know how to write classes, overload operators, and handle errors, you need only learn about someadditional operators before you can start implementing fully functional classes of your own. The next Explorationrevisits some familiar operators and introduces a few new ones.
338EXPLORATION 46
More Operators
C++ has lots of operators. Lots and lots. So far, I’ve introduced the basic operators that you require for most programs:arithmetic, comparison, assignment, subscript, and function call. Now it’s time to introduce some more: additionalassignment operators, the conditional operator (which is like having an if statement in the middle of an expression),and the comma operator (most often used in for loops).
Conditional OperatorThe conditional operator is a unique entry in the C++ operator bestiary, being a ternary operator, that is, an operatorthat takes three operands. condition ? true-part : false-part The condition is a Boolean expression. If it evaluates to true, the result of the entire expression is the true-part.If the condition is false, the result is the false-part. As with an if statement, only one part is evaluated; the branch nottaken is skipped. For example, the following statement is perfectly safe: std::cout << (x == 0 ? 0 : y / x); If x is zero, the y / x expression is not evaluated, and division by zero never occurs. The conditional operator hasvery low precedence, so you often see it written inside parentheses. A conditional expression can be the source of anassignment expression. So the following expression assigned 42 or 24 to x, depending on whether test is true. x = test ? 42 : 24; And an assignment expression can be the true-part or false-part of a conditional expression. That is, thefollowing expression: x ? y = 1 : y = 2; is parsed as x ? (y = 1) : (y = 2); The true-part and false-part are expressions that have the same or compatible types, that is, the compiler canautomatically convert one type to the other, ensuring that the entire conditional expression has a well-defined type.
339EXPLORATION 46 ■ More Operators
For example, you can mix an integer and a floating-point number; the expression result is a floating-point number.The following statement prints 10.000000 if x is positive: std::cout << std::fixed << (x > 0 ? 10 : 42.24) << '\n'; Do not use the conditional operator as a replacement for if statements. If you have a choice, use an if statement,because a statement is almost always easier to read and understand than a conditional expression. Use conditionalexpressions in situations when if statements are infeasible. Initializing a data member in a constructor, for example,does not permit the use of an if statement. Although you can use a member function for complicated conditions,you can also use a conditional expression for simple conditions. The rational class (last seen in Exploration 45), for example, takes a numerator and a denominator as constructorarguments. The class ensures that its denominator is always positive. If the denominator is negative, it negates thenumerator and denominator. In past Explorations, I loaded the reduce() member function with additional responsibilities,such as checking for a zero denominator and a negative denominator to reverse the signs of the numerator anddenominator. This design has the advantage of centralizing all code needed to convert a rational number to canonicalform. An alternate design is to separate the responsibility and let the constructor check the denominator prior tocalling reduce(). If the denominator is zero, the constructor throws an exception; if the denominator is negative,the constructor negates the numerator and the denominator. This alternative design makes reduce() simpler, andsimple functions are less error prone than complicated functions. Listing 46-1 shows how you can do this usingconditional operators.
Listing 46-2. Using a Function and Conditional Statements Instead of Conditional Expressions/// Construct a rational object from a numerator and a denominator./// If the denominator is zero, throw zero_denominator. If the denominator/// is negative, normalize the value by negating the numerator and denominator.
340 EXPLORATION 46 ■ More Operators
Short-Circuit OperatorsC++ lets you overload the and and or operators, but you must resist the temptation. By overloading these operators,you defeat one of their key benefits: short-circuiting. Recall from Exploration 12 that the and and or operators do not evaluate their right-hand operands if they don’thave to. That’s true of the built-in operators, but not if you overload them. When you overload the Boolean operators,they become normal functions, and C++ always evaluates function arguments before calling a function. Therefore,overloaded and and or operators behave differently from the built-in operators, and this difference makes themsignificantly less useful.
Comma OperatorThe comma (,) serves many roles: it separates arguments in a function call, parameters in a function declaration,declarators in a declaration, and initializers in a constructor’s initializer list. In all these cases, the comma is a punctuator,that is, a symbol that is part of the syntax that serves only to show where one thing (argument, declarator, etc.) ends andanother thing begins. It is also an operator in its own right, which is a completely different use for the same symbol. Thecomma as operator separates two expressions; it causes the left-hand operand to be evaluated, and then the right-handoperand is evaluated, which becomes the result of the entire expression. The value of the left-hand operand is ignored. At first, this operator seems a little pointless. After all, what’s the purpose of writing, say, x = 1 + 2, y = x + 3, z = y + 4
341EXPLORATION 46 ■ More Operators
instead of x = 1 + 2;y = x + 3;z = y + 4; The comma operator is not meant to be a substitute for writing separate statements. There is one situation,however, when multiple statements are not possible, but multiple expressions have to be evaluated. I speak of noneother than the for loop. Suppose you want to implement the search algorithm. Implementing a fully generic algorithm requirestechniques that you haven’t learned yet, but you can write this function so that it works with the iterators of avector<int>. The basic idea is simple, search looks through a search range, trying to find a sequence of elementsthat are equal to elements in a match range. It steps through the search range one element at a time, testing whethera match starts at that element. If so, it returns an iterator that refers to the start of the match. If no match is found,search returns the end iterator. To check for a match, use a nested loop to compare successive elements in the tworanges. Listing 46-3 shows one way to implement this function.
342 EXPLORATION 46 ■ More Operators
The boldface lines demonstrate the comma operator. The initialization portion of the first for loop does notinvoke the comma operator. The comma in the declaration is only a separator between declarators. The commaoperator appears in the postiteration part of the loops. Because the postiteration part of a for loop is an expression,you cannot use multiple statements to increment multiple objects. Instead, you have to do it in a single expression.Hence, the need for the comma operator. On the other hand, some programmers prefer to avoid the comma operator, because the resulting code can behard to read. Rewrite Listing 46-3 so that it does not use the comma operator. Which version of the function doyou prefer? ________________ Listing 46-4 shows my version of search without the comma operator.
Listing 46-4. The search Function Without Using the Comma Operator#include <vector> typedef std::vector<int>::iterator viterator;typedef std::vector<int>::difference_type vdifference; viterator search(viterator first1,viterator last1,viterator first2,viterator last2){ // s1 is the size of the untested portion of the first range // s2 is the size of the second range // End the search when s2 > s1 because a match is impossible if the remaining // portion of the search range is smaller than the test range. Each iteration // of the outer loop shrinks the search range by one, and advances the first1 // iterator. The inner loop searches for a match starting at first1. for (vdifference s1(last1-first1), s2(last2-first2); s2 <= s1; --s1) { // Is there a match starting at first1? viterator f2(first2); for (viterator f1(first1); f1 != last1 and f2 != last2 and *f1 == *f2; ) { ++f1; ++f2; } if (f2 == last2) return first1; // match starts at first1 ++first1; } // no match return last1;} The comma operator has very low precedence, even lower than assignment and the conditional operator. If a loophas to advance objects by steps of 2, for example, you can use assignment expressions with the comma operator. for (int i{0}, j{size-1}; i < j; i += 2, j -= 2) do_something(i, j); By the way, C++ lets you overload the comma operator, but you shouldn’t take advantage of this feature. The commais so basic, C++ programmers quickly grasp its standard use. If the comma does not have its usual meaning, readers ofyour code will be confused, bewildered, and stymied when they try to understand it.
343EXPLORATION 46 ■ More Operators
344 EXPLORATION 46 ■ More Operators
Implement the /=, +=, and -= operators for class rational. You can implement these operators in many ways.I recommend putting the arithmetic logic in the assignment operators and reimplementing the /, +, and - operatorsto use the assignment operators, as I did with the multiplication operators. My solution appears in Listing 46-7.
345EXPLORATION 46 ■ More Operators
346 EXPLORATION 46 ■ More Operators
348Exploration 47
Your task for Project 2 is to implement a simple fixed-point number class. The class represents fixed-point numbersusing an integer type. The number of places after the decimal point is a fixed constant, four. For example, representthe number 3.1415 as the integer 31415 and 3.14 as 31400. You must overload the arithmetic, comparison, and I/Ooperators to maintain the fixed-point fiction. Name the class fixed. It should have the following public members:
value_typeA typedef for the underlying integer type, such as int or long. By using the value_type typedef throughout the fixedclass, you can easily switch between int and long by changing only the declaration of value_type.
placesA static const int equal to 4, or the number of places after the decimal point. By using a named constant instead ofhard-coding the value 4, you can easily change the value to 2 or something else in the future.
places10A static const int equal to 10places, or the scale factor for the fixed-point values. Divide the internal integer byplaces10 to obtain the true value. Multiply a number by places10 to scale it to an integer that the fixed objectstores internally.
fixed( )Default constructor.
349Exploration 47 ■ Project 2: Fixed-Point Numbers
fixed(double val)A constructor to make a fixed-point value from a floating-point number. Round off the fraction and discard excessdigits. Thus, fixed{12.3456789} == fixed{12, 3456789} == fixed{12, 3457}. Implement the arithmetic operators, arithmetic assignment operators, comparison operators, and I/O operators.Don’t concern yourself with overflow. Do your best to check for errors when reading fixed-point numbers. Be sure tohandle integers without decimal points (42) and values with too many decimal points (3.14159). Implement a member function to convert the fixed-point value to std::string.
to_string( )Convert the value to a string representation; e.g., 3.1416 becomes "3.1416" and -21 becomes "-21.0000". To convert to an integer means discarding information. To make it abundantly clear to the user, call the functionround(), to emphasize that the fixed-point value must be rounded off to become an integer.
round( )Round off to the nearest integer. If the fractional part is exactly 5000, round to the nearest even integer (banker’s rounding).Be sure to handle negative and positive numbers. Other useful member functions give you access to the raw value (good for debugging, implementing additionaloperations, etc.) or the parts of the fixed-point value: the integer part and the fractional part.
integer( )Return just the integer part, without the fractional part.
fraction( )Return just the fraction part, without the integer part. The fraction part is always in the range [0, places10]. Be sure to write a header file (fixed.hpp) with #include guards. Write a separate implementation file (fixed.cpp).Decide which member functions should be inline (if any), and be sure to define all inline functions in fixed.hpp, notfixed.cpp. After you finish, review your solution carefully and run some tests, comparing your results to mine, whichyou can download from the book’s web site. If you need help testing your code, try linking your fixed.cpp file with the test program in Listing 47-1. The testprogram makes use of the test and test_equal functions, declared in test.hpp. The details are beyond the scope ofthis book. Just call test with a Boolean argument. If the argument is true, the test passed. Otherwise, the test failed,and test prints a message. The test_equal function takes two arguments and prints a message if they are not equal.Thus, if the program produces no output, all tests passed.
350 Exploration 47 ■ Project 2: Fixed-Point Numbers
int main(){ fixed f1{}; test_equal(f1.value(), 0); fixed f2{1}; test_equal(f2.value(), 10000); fixed