EXPLORATION 13

image

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.

Statements

C++ has some hairy, scary syntax rules. By comparison though, the syntax for statements is downright simplistic. The C++ 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 syntax rules. 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 in Pascal, 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: in this case, an expression statement. The expression in the expression statement is sum = sum + x. Expressions in expression statements are often assignments or function calls, but the language permits any expression. The following, therefore, is a valid statement:

42;

What do you think happens if you use this statement in a program?

_____________________________________________________________

Try it. What actually happens?

_____________________________________________________________

Modern compilers are usually able to detect statements that serve no useful purpose and eliminate them from the program. Typically, the compiler tells you what it’s doing, but you may have to supply an extra option to tell the compiler to be extra picky. For example, try the -Wall option for g++ or /Wall for Microsoft Visual C++. (That’s Wall, as in 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 no semicolon 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 loop body 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.

9781430261933_Fig13-01.jpg

Figure 13-1. Simplified parse tree for C++ statements

Consider the body of main(), such as the one in Listing 13-1. What do you see? That’s right, it’s a compound statement. It’s an ordinary block, and it follows the same rules as any other block. In case you were wondering, the body of main() must be a compound statement. This is one of the few circumstances in which C++ requires a specific kind 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 you think you found and fixed them all, try compiling and running the program.

Listing 13-1.  Finding Statement Errors

 1 #include <iostream>
 2 #include <vector>
 3 // find errors in this program
 4 int main()
 5 {
 6   std::vector<int> positive_data{}, negative_data{};
 7
 8   for (int x{0}; std::cin >> x ;) {
 9     if (x < 0);
10     {
11       negative_data.push_back(x)
12     };
13     else
14     {
15       positive_data.push_back(x)
16     }
17   };
18 }

Record all the errors in Listing 13-1.

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

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 not affect the program’s behavior?

_____________________________________________________________

_____________________________________________________________

If you answered “the extra semicolon on line 17,” give yourself a star. Strictly speaking, the extra semicolon represents 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 example in Listing 12-4.)

Thus, the way the compiler interprets line 9 is that the semicolon is the statement body for the if statement. The next statement is a compound statement, which is followed by an else, which has no corresponding if, hence the error. Every else must be a counterpart to an earlier if in the same statement. In other words, every if condition must be followed by exactly one statement, then by an optional else keyword and another statement. You cannot use else 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 compound statements. The next section explains why you might want to nest a compound statement within another compound statement.

Line 6 shows that you can declare more than one variable at a time, using a comma separator. I prefer to define one variable at a time but wanted to show you this style too. Each variable receives its own initializer.

Local Definitions and Scope

Compound statements do more than simply group multiple statements into a single statement. You can also group definitions within the block. Any variable that you define in a block is visible only within the confines of the block. The region where you can use a variable is known as the variable’s scope. A good programming practice is to limit the scope to as small a region as possible. Limiting the scope of a variable serves several purposes.

  • Preventing mistakes: You can’t accidentally use a variable’s name outside of its scope.
  • Communicating intent: Anyone who reads your code can tell how a variable is used. If variables are defined at the broadest scope possible, whoever reads your code must spend more time and effort trying to determine where different variables are used.
  • Reusing names: How many times can you use the variable i as a loop control variable? You can use and reuse it as often as you like, provided each time you limit the variable’s scope to its loop.
  • Reusing memory: When execution reaches the end of a block, all the variables defined in that block are destroyed, and the memory is available to be used again. Thus, if your code creates many large objects but needs only one at a time, you can define each variable in its own scope, so only one large object exists at a time.

Listing 13-2 shows some examples of local definitions. The lines highlighted in bold indicate local definitions.

Listing 13-2.  Local Variable Definitions

#include <algorithm>
#include <cassert>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
 
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{" "};
  for (int element : data)
  {
    std::cout << separator << element;
    separator = ", ";
  }
  std::cout << " } ";
}

Listing 13-2 has a lot of new functions and features, so let’s look at the code one section at a time.

The definition of data is a local definition in a block. True, almost all your definitions have been at this outermost level, but a compound statement is a compound statement, and any definition in a compound statement is a local definition. That begs the question of whether you can define a variable outside of all blocks. The answer is yes, but you rarely want to. C++ permits global variables, but no program in this book has needed to define any yet. I’ll cover global variables when the time is right (which would be Exploration 52).

A for loop has its own special scope rules. As you learned in Exploration 7, the initialization part of a for loop can, and often does, define a loop control variable. The scope of that variable is limited to the for loop, as though the for statement were enclosed in an extra set of curly braces.

The value variable is also local to the for loop’s body. If you try to use this variable outside of the loop, the compiler issues an error message. In this case, you have no reason to use this variable outside the loop, so define the variable inside the loop.

The lower_bound algorithm performs a binary search that tries to find a value in a range of sorted values. It returns an iterator that points to the first occurrence of the value in the range, or, if the value is not found, the position where you can insert the value and keep the range in order. This is exactly what this program needs to sort the data vector.

The erase member function deletes an element from a vector, reducing the vector’s size by one. Pass an iterator to erase to designate which element to delete, and save the return value, which is an iterator that refers to the new value at that position in the vector. The insert function inserts a value (the second argument) just before the position designated by an iterator (the first argument).

Notice how you can use and reuse the name iter. Each loop has its own distinct variable named iter. Each iter is local to its loop. If you were to write sloppy code and fail to initialize iter, the variable’s initial value would be junk. It is not the same variable as the one defined earlier in the program, so its value is not the same as the old value of the old variable.

The separator variable holds a separator string to print between elements when printing the vector. It, too, is a local variable, but local to the main program’s block. However, by defining it just before it is used, you communicate the message that this variable is not needed earlier in main. It helps prevent mistakes that can arise if you reuse a variable from one part of main in another part.

Another way you can help limit the scope of a variable such as separator is to define it in a block within a block, as shown in Listing 13-3. (This version of the program replaces the loops with calls to standard algorithms, which is a better way to write C++ programs when you are not trying to make a point.)

Listing 13-3.  Local Variable Definitions in a Nested Block

#include <algorithm>
#include <cassert>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
 
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 << " } ";
  }
  // Cannot use separator out here.
}

Most C++ programmers nest blocks infrequently. As you learn more C++, you will discover a variety of techniques that improve on nested blocks and keep your main program from looking so cluttered.

Definitions in for Loop Headers

What if you did not define loop control variables inside the for loop header, but defined them outside the loop instead? Try it.

Rewrite Listing 13-2, so you don’t define any variables in the for loop headers.

What do you think? Does the new code look better or worse than the original? _________ Why?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Personally, I find for loops can become cluttered very easily. Nonetheless, keeping loop control variables local to the loop is critical for clarity and code comprehension. When faced with a large, unknown program, one of the difficulties you face in understanding that program is knowing when and how variables can take on new values. If a variable is local to a loop, you know the variable cannot be modified outside the loop. That is valuable information. If you still need convincing, try reading and understanding Listing 13-4.

Listing 13-4.  Mystery Function

#include <algorithm>
#include <cassert>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
 
int main()
{
  int v{};
  std::vector<int> data{};
  std::vector<int>::iterator i{}, p{};
  std::string s{};
 
  std::copy(std::istream_iterator<int>(std::cin),
            std::istream_iterator<int>(),
            std::back_inserter(data));
  i = data.begin();
 
  while (i != data.end())
  {
    v = *i;
    p = std::lower_bound(data.begin(), i, v);
    i = data.erase(i);
    data.insert(p, v);
  }
 
  s = " ";
  for (p = i, i = data.begin(); i != data.end(); p = i, ++i)
  {
    if (p != data.end())
      assert(not (*i < *p));
  }
 
  std::cout << '{';
  for (i = data.begin(); i != data.end(); ++i)
  {
    v = *p;
    std::cout << s << v;
    s = ", ";
  }
  std::cout << " } ";
}

Well, that wasn’t too hard, was it? After all, you recently finished reading Listing 13-2, so you can see that Listing 13-4 is intended to do the same thing but is reorganized slightly. The difficulty is in keeping track of the values of p and i, and ensuring that they have the correct value at each step of the program. Try compiling and running the program. Record your observations.

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

What went wrong?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

I made a mistake and wrote v = *p instead of v = *i. Congratulations if you spotted this error before you ran the program. If the variables had been properly defined local to their respective scopes, this error could never have occurred.

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.

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

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