8
STATEMENTS

Progress doesn’t come from early risers—progress is made by lazy men looking for easier ways to do things.
—Robert A. Heinlein
, Time Enough for Love

Image

Each C++ function comprises a sequence of statements, which are programming constructs that specify the order of execution. This chapter uses an understanding of the object life cycle, templates, and expressions to explore the nuances of statements.

Expression Statements

An expression statement is an expression followed by a semicolon (;). Expression statements comprise most of the statements in a program. You can turn any expression into a statement, which you should do whenever you need to evaluate an expression but want to discard the result. Of course, this is only useful if evaluating that expression causes a side effect, like printing to the console or modifying the program’s state.

Listing 8-1 contains several expression statements.

#include <cstdio>

int main() {
  int x{};
  ++x; 
  42; 
  printf("The %d True Morty
", x); 
}
--------------------------------------------------------------------------
The 1 True Morty 

Listing 8-1: A simple program containing several expression statements

The expression statement at has a side effect (incrementing x), but the one at doesn’t. Both are valid (although the one at isn’t useful). The function call to printf is also an expression statement.

Compound Statements

Compound statements, also called blocks, are a sequence of statements enclosed by braces { }. Blocks are useful in control structures like if statements, because you might want multiple statements to execute rather than one.

Each block declares a new scope, which is called a block scope. As you learned in Chapter 4, objects with automatic storage duration declared within a block scope have lifetimes bound by the block. Variables declared within a block get destroyed in a well-defined order: the reverse of the order in which they were declared.

Listing 8-2 uses the trusty Tracer class from Listing 4-5 (on page 97) to explore block scope.

#include <cstdio>

struct Tracer {
  Tracer(const char* name) : name{ name } {
    printf("%s constructed.
", name);
  }
  ~Tracer() {
    printf("%s destructed.
", name);
  }
private:
  const char* const name;
};

int main() {
  Tracer main{ "main" }; 
  {
    printf("Block a
"); 
    Tracer a1{ "a1" }; 
    Tracer a2{ "a2" }; 
  }
  {
    printf("Block b
"); 
    Tracer b1{ "b1" }; 
    Tracer b2{ "b2" }; 
  }
}
--------------------------------------------------------------------------
main constructed. 
Block a 
a1 constructed. 
a2 constructed.
a2 destructed.
a1 destructed.
Block b 
b1 constructed. 
b2 constructed. 
b2 destructed.
b1 destructed.
main destructed.

Listing 8-2: A program exploring compound statements with the Tracer class

Listing 8-2 begins by initializing a Tracer called main . Next, you generate two compound statements. The first compound statement begins with a left brace { followed by the block’s first statement, which prints Block a . You create two Tracers, a1 and a2 , and then close the block with a right brace }. These two tracers get destructed once execution passes through Block a. Notice that these two tracers destruct in reverse order from their initialization: a2 then a1.

Also notice another compound statement following Block a, where you print Block b and then construct two tracers, b1 and b2 . Its behavior is identical: b2 destructs followed by b1. Once execution passes through Block b, the scope of main ends and Tracer main finally destructs.

Declaration Statements

Declaration statements (or just declarations) introduce identifiers, such as functions, templates, and namespaces, into your programs. This section explores some new features of these familiar declarations, as well as type aliases, attributes, and structured bindings.

NOTE

The expression static_assert, which you learned about in Chapter 6, is also a declaration statement.

Functions

A function declaration, also called the function’s signature or prototype, specifies a function’s inputs and outputs. The declaration doesn’t need to include parameter names, only their types. For example, the following line declares a function called randomize that takes a uint32_t reference and returns void:

void randomize(uint32_t&);

Functions that aren’t member functions are called non-member functions, or sometimes free functions, and they’re always declared outside of main() at namespace scope. A function definition includes the function declaration as well as the function’s body. A function’s declaration defines a function’s interface, whereas a function’s definition defines its implementation. For example, the following definition is one possible implementation of the randomize function:

void randomize(uint32_t& x) {
  x = 0x3FFFFFFF & (0x41C64E6D * x + 12345) % 0x80000000;
}

NOTE

This randomize implementation is a linear congruential generator, a primitive kind of random number generator. See “Further Reading” on page 241 for sources of more information on generating random numbers.

As you’ve probably noticed, function declarations are optional. So why do they exist?

The answer is that you can use declared functions throughout your code as long as they’re eventually defined somewhere. Your compiler tool chain can figure it out. (You’ll learn how this works in Chapter 21.)

The program in Listing 8-3 determines how many iterations the random number generator takes to get from the number 0x4c4347 to the number 0x474343.

#include <cstdio>
#include <cstdint>

void randomize(uint32_t&); 

int main() {
  size_t iterations{}; 
  uint32_t number{ 0x4c4347 }; 
  while (number != 0x474343) { 
    randomize(number); 
    ++iterations; 
  }
  printf("%zd", iterations); 
}

void randomize(uint32_t& x) {
  x = 0x3FFFFFFF & (0x41C64E6D * x + 12345) % 0x80000000; 
}
--------------------------------------------------------------------------
927393188 

Listing 8-3: A program that uses a function in main that isn’t defined until later

First, you declare randomize . Within main, you initialize an iterations counter variable to zero and a number variable to 0x4c4347 . A while loop checks whether number equals the target 0x4c4347 . If it doesn’t, you invoke randomize and increment iterations . Notice that you haven’t yet defined randomize. Once number equals the target, you print the number of iterations before returning from main. Finally, you define randomize . The program’s output shows that it takes almost a billion iterations to randomly draw the target value.

Try to delete the definition of randomize and recompile. You should get an error stating that the definition of randomize couldn’t be found.

You can similarly separate method declarations from their definitions. As with non-member functions, you can declare a method by omitting its body. For example, the following RandomNumberGenerator class replaces the randomize function with next:

struct RandomNumberGenerator {
  explicit RandomNumberGenerator(uint32_t seed) 
    : number{ seed } {} 
  uint32_t next(); 
private:
  uint32_t number;
};

You can construct a RandomNumberGenerator with a seed value , which it uses to initialize the number member variable . You’ve declared the next function using the same rules as non-member functions . To provide the definition of next, you must use the scope resolution operator and the class name to identify which method you want to define. Otherwise, defining a method is the same as defining a non-member function:

uint32_t RandomNumberGenerator::next() {
  number = 0x3FFFFFFF & (0x41C64E6D * number + 12345) % 0x80000000; 
  return number; 
}

This definition shares the same return type as the declaration . The RandomNumberGenerator:: construct specifies that you’re defining a method . The function details are essentially the same , except you’re returning a copy of the random number generator’s state rather than writing into a parameter reference .

Listing 8-4 illustrates how you can refactor Listing 8-3 to incorporate RandomNumberGenerator.

#include <cstdio>
#include <cstdint>

struct RandomNumberGenerator {
  explicit RandomNumberGenerator(uint32_t seed)
    : iterations{}, number { seed } {}
  uint32_t next(); 
  size_t get_iterations() const; 
private:
  size_t iterations;
  uint32_t number;
};

int main() {
  RandomNumberGenerator rng{ 0x4c4347 }; 
  while (rng.next() != 0x474343) { 
    // Do nothing...
  }
  printf("%zd", rng.get_iterations()); 
}

uint32_t RandomNumberGenerator::next() { 
  ++iterations;
  number = 0x3FFFFFFF & (0x41C64E6D * number + 12345) % 0x80000000;
  return number;
}

size_t RandomNumberGenerator::get_iterations() const { 
  return iterations;
}
--------------------------------------------------------------------------
927393188 

Listing 8-4: A refactor of Listing 8-3 using a RandomNumberGenerator class

As in Listing 8-3, you’ve separated declaration from definition. After declaring a constructor that initializes an iterations member to zero and sets its number member to a seed , the next and get_iterations method declarations don’t contain implementations. Within main, you initialize the RandomNumberGenerator class with your seed value of 0x4c4347 and invoke the next method to extract new random numbers . The results are the same . As before, the definitions of next and get_iterations follow their use in main ➑➒.

NOTE

The utility of separating definition and declaration might not be apparent because you’ve been dealing with single-source-file programs so far. Chapter 21 explores multiple-source-file programs where separating declaration and definition provides major benefits.

Namespaces

Namespaces prevent naming conflicts. In large projects or when importing libraries, namespaces are essential for disambiguating exactly the symbols you’re looking for.

Placing Symbols Within Namespaces

By default, all symbols you declare go into the global namespace. The global namespace contains all the symbols that you can access without adding any namespace qualifiers. Aside from several classes in the std namespace, you’ve been using objects living exclusively in the global namespace.

To place a symbol within a namespace other than the global namespace, you declare the symbol within a namespace block. A namespace block has the following form:

namespace BroopKidron13 {
  // All symbols declared within this block
  // belong to the BroopKidron13 namespace
}

Namespaces can be nested in one of two ways. First, you can simply nest namespace blocks:

namespace BroopKidron13 {
  namespace Shaltanac {
    // All symbols declared within this block
    // belong to the BroopKidron13::Shaltanac namespace
  }
}

Second, you can use the scope-resolution operator:

namespace BroopKidron13::Shaltanac {
  // All symbols declared within this block
  // belong to the BroopKidron13::Shaltanac namespace
}

The latter approach is more succinct.

Using Symbols in Namespaces

To use a symbol within a namespace, you can always use the scope-resolution operator to specify the fully qualified name of a symbol. This allows you to prevent naming conflicts in large projects or when you’re using a third-party library. If you and another programmer use the same symbol, you can avoid ambiguity by placing the symbol within a namespace.

Listing 8-5 illustrates how you can use fully qualified symbol names to access a symbol within a namespace.

#include <cstdio>

namespace BroopKidron13::Shaltanac { 
  enum class Color { 
    Mauve,
    Pink,
    Russet
  };
}

int main() {
  const auto shaltanac_grass{ BroopKidron13::Shaltanac::Color::Russet };
  if(shaltanac_grass == BroopKidron13::Shaltanac::Color::Russet) {
    printf("The other Shaltanac's joopleberry shrub is always "
           "a more mauvey shade of pinky russet.");
  }
}
--------------------------------------------------------------------------
The other Shaltanac's joopleberry shrub is always a more mauvey shade of pinky russet.

Listing 8-5: Nested namespace blocks using the scope-resolution operator

Listing 8-5 uses nested namespaces and declares a Color type . To use Color, you apply the scope-resolution operator to specify the full name of the symbol, BroopKidron13::Shaltanac::Color. Because Color is an enum class, you use the scope-resolution operator to access its values, as when you assign shaltanac_grass to Russet .

Using Directives

You can employ a using directive to avoid a lot of typing. A using directive imports a symbol into a block or, if you declare a using directive at namespace scope, into the current namespace. Either way, you have to type the full namespace path only once. The usage has the following pattern:

using my-type;

The corresponding my-type gets imported into the current namespace or block, meaning you no longer have to use its full name. Listing 8-6 refactors Listing 8-5 with a using directive.

#include <cstdio>

namespace BroopKidron13::Shaltanac {
  enum class Color {
    Mauve,
    Pink,
    Russet
  };
}

int main() {
  using BroopKidron13::Shaltanac::Color; 
  const auto shaltanac_grass = Color::Russet;
  if(shaltanac_grass == Color::Russet) {
    printf("The other Shaltanac's joopleberry shrub is always "
           "a more mauvey shade of pinky russet.");
  }
}
--------------------------------------------------------------------------
The other Shaltanac's joopleberry shrub is always a more mauvey shade of pinky russet.

Listing 8-6: A refactor of Listing 8-5 employing a using directive

With a using directive within main, you no longer have to type the namespace BroopKidron13::Shaltanac to use Color .

If you’re careful, you can introduce all the symbols from a given namespace into the global namespace with the using namespace directive.

Listing 8-7 elaborates Listing 8-6: the namespace BroopKidron13::Shaltanac contains multiple symbols, which you want to import into the global namespace to avoid a lot of typing.

#include <cstdio>

namespace BroopKidron13::Shaltanac {
  enum class Color {
    Mauve,
    Pink,
    Russet
  };

  struct JoopleberryShrub {
    const char* name;
    Color shade;
  };

  bool is_more_mauvey(const JoopleberryShrub& shrub) {
    return shrub.shade == Color::Mauve;
  }
}

using namespace BroopKidron13::Shaltanac; 
int main() {
  const JoopleberryShrub yours{
    "The other Shaltanac",
    Color::Mauve
  };

  if (is_more_mauvey(yours)) {
    printf("%s's joopleberry shrub is always a more mauvey shade of pinky"
           "russet.", yours.name);
  }
}
--------------------------------------------------------------------------
The other Shaltanac's joopleberry shrub is always a more mauvey shade of pinky
russet.

Listing 8-7: A refactor of Listing 8-6 with multiple symbols imported into the global namespace

With a using namespace directive , you can use classes , enum classes , functions , and so on within your program without having to type fully qualified names. Of course, you need to be very careful about clobbering existing types in the global namespace. Usually, it’s a bad idea to have too many using namespace directives appear in a single translation unit.

NOTE

You should never put a using namespace directive within a header file. Every source file that includes your header will dump all the symbols from that using directive into the global namespace. This can cause issues that are very difficult to debug.

Type Aliasing

A type alias defines a name that refers to a previously defined name. You can use a type alias as a synonym for the existing type name.

There is no difference between a type and any type aliases referring to it. Also, type aliases cannot change the meaning of an existing type name.

To declare a type alias, you use the following format, where type-alias is the type alias name and type-id is the target type:

using type-alias = type-id;

Listing 8-8 employs two type aliases, String and ShaltanacColor.

#include <cstdio>

namespace BroopKidron13::Shaltanac {
  enum class Color {
    Mauve,
    Pink,
    Russet
  };
}

using String = const char[260]; 
using ShaltanacColor = BroopKidron13::Shaltanac::Color; 

int main() {
  const auto my_color{ ShaltanacColor::Russet }; 
  String saying { 
    "The other Shaltanac's joopleberry shrub is "
    "always a more mauvey shade of pinky russet."
  };
  if (my_color == ShaltanacColor::Russet) {
    printf("%s", saying);
  }
}

Listing 8-8: A refactor of Listing 8-7 with a type alias

Listing 8-8 declares a type alias String that refers to a const char[260] . This listing also declares a ShaltanacColor type alias, which refers to BroopKidron13::Shaltanac::Color . You can use these type aliases as drop-in replacements to clean up code. Within main, you use ShaltanacColor to remove all the nested namespaces and String to make the declaration of saying cleaner .

NOTE

Type aliases can appear in any scope—block, class, or namespace.

You can introduce template parameters into type aliases. This enables two important usages:

  • You can perform partial application on template parameters. Partial application is the process of fixing some number of arguments to a template, producing another template with fewer template parameters.
  • You can define a type alias for a template with a fully specified set of template parameters.

Template instantiations can be quite verbose, and type aliases help you avoid carpal tunnel syndrome.

Listing 8-9 declares a NarrowCaster class with two template parameters. You then use a type alias to partially apply one of its parameters and produce a new type.

#include <cstdio>
#include <stdexcept>

template <typename To, typename From>
struct NarrowCaster const { 
  To cast(From value) {
    const auto converted = static_cast<To>(value);
    const auto backwards = static_cast<From>(converted);
    if (value != backwards) throw std::runtime_error{ "Narrowed!" };
    return converted;
  }
};

template <typename From>
using short_caster = NarrowCaster<short, From>; 

int main() {
  try {
    const short_caster<int> caster; 
    const auto cyclic_short = caster.cast(142857);
    printf("cyclic_short: %d
", cyclic_short);
  } catch (const std::runtime_error& e) {
    printf("Exception: %s
", e.what()); 
  }
}
--------------------------------------------------------------------------
Exception: Narrowed! 

Listing 8-9: A partial application of the NarrowCaster class using a type alias

First, you implement a NarrowCaster template class that has the same functionality as the narrow_cast function template in Listing 6-6 (on page 154): it will perform a static_cast and then check for narrowing . Next, you declare a type alias short_caster that partially applies short as the To type to NarrowCast. Within main, you declare a caster object of type short_caster<int> . The single template parameter in the short_caster type alias gets applied to the remaining type parameter from the type alias—From . In other words, the type short_cast<int> is synonymous with NarrowCaster<short, int>. In the end, the result is the same: with a 2-byte short, you get a narrowing exception when trying to cast an int with the value 142857 into a short .

Structured Bindings

Structured bindings enable you to unpack objects into their constituent elements. Any type whose non-static data members are public can be unpacked this way—for example, the POD (plain-old-data class) types introduced in Chapter 2. The structured binding syntax is as follows:

auto [object-1, object-2, ...] = plain-old-data;

This line will initialize an arbitrary number of objects (object-1, object-2, and so on) by peeling them off a POD object one by one. The objects peel off the POD from top to bottom, and they fill in the structured binding from left to right. Consider a read_text_file function that takes a string argument corresponding to the file path. Such a function might fail, for example, if a file is locked or doesn’t exist. You have two options for handling errors:

  • You can throw an exception within read_text_file.
  • You can return a success status code from the function.

Let’s explore the second option.

The POD type in Listing 8-10 will serve as a fine return type from the read_text_file function.

struct TextFile {
  bool success; 
  const char* contents; 
  size_t n_bytes; 
};

Listing 8-10: A TextFile type that will be returned by the read_text_file function

First, a flag communicates to the caller whether the function call was a success . Next is the contents of the file and its size n_bytes .

The prototype of read_text_file looks like this:

TextFile read_text_file(const char* path);

You can use a structured binding declaration to unpack a TextFile into its parts within your program, as in Listing 8-11.

#include <cstdio>

struct TextFile { 
  bool success;
  const char* data;
  size_t n_bytes;
};

TextFile read_text_file(const char* path) { 
  const static char contents[]{ "Sometimes the goat is you." };
  return TextFile{
    true,
    contents,
    sizeof(contents)
  };
}

int main() {
  const auto [success, contents, length] = read_text_file("REAMDE.txt"); 
  if (success) {
    printf("Read %zd bytes: %s
", length, contents);
  } else {
    printf("Failed to open REAMDE.txt.");
  }
}
--------------------------------------------------------------------------
Read 27 bytes: Sometimes the goat is you.

Listing 8-11: A program simulating the reading of a text file that returns a POD that you use in a structured binding

You’ve declared the TextFile and then provided a dummy definition for read_text_file . (It doesn’t actually read a file; more on that in Part II.)

Within main, you invoke read_text_file and use a structured binding declaration to unpack the results into three distinct variables: success, contents, and length . After structured binding, you can use all these variables as though you had declared them individually .

NOTE

The types within a structured binding declaration don’t have to match.

Attributes

Attributes apply implementation-defined features to an expression statement. You introduce attributes using double brackets [[ ]] containing a list of one or more comma-separated attribute elements.

Table 8-1 lists the standard attributes.

Table 8-1: The Standard Attributes

Attribute

Meaning

[[noreturn]]

Indicates that a function doesn’t return.

[[deprecated("reason")]]

Indicates that this expression is deprecated; that is, its use is discouraged. The "reason" is optional and indicates the reason for deprecation.

[[fallthrough]]

Indicates that a switch case intends to fall through to the next switch case. This avoids compiler errors that will check for switch case fallthrough, because it’s uncommon.

[[nodiscard]]

Indicates that the following function or type declaration should be used. If code using this element discards the value, the compiler should emit a warning.

[[maybe_unused]]

Indicates that the following element might be unused and that the compiler shouldn’t warn about it.

[[carries_dependency]]

Used within the <atomic> header to help the compiler optimize certain memory operations. You’re unlikely to encounter this directly.

Listing 8-12 demonstrates using the [[noreturn]] attribute by defining a function that never returns.

#include <cstdio>
#include <stdexcept>

[[noreturn]] void pitcher() { 
  throw std::runtime_error{ "Knuckleball." }; 
}

int main() {
  try {
    pitcher(); 
  } catch(const std::exception& e) {
    printf("exception: %s
", e.what()); 
  }
}
--------------------------------------------------------------------------
Exception: Knuckleball. 

Listing 8-12: A program illustrating the use of the [[noreturn]] attribute

First, you declare the pitcher function with the [[noreturn]] attribute . Within this function, you throw an exception . Because you always throw an exception, pitcher never returns (hence the [[noreturn]] attribute). Within main, you invoke pitcher and handle the caught exception . Of course, this listing works without the [[noreturn]] attribute. But giving this information to the compiler allows it to reason more completely on your code (and potentially to optimize your program).

The situations in which you’ll need to use an attribute are rare, but they convey useful information to the compiler nonetheless.

Selection Statements

Selection statements express conditional control flow. The two varieties of selection statements are the if statement and the switch statement.

if Statements

The if statement has the familiar form shown in Listing 8-13.

if (condition-1) {
  // Execute only if condition-1 is true 
} else if (condition-2) { // optional
  // Execute only if condition-2 is true 
}
// ... as many else ifs as desired
--snip--
} else { // optional
  // Execute only if none of the conditionals is true 
}

Listing 8-13: The syntax of the if statement

Upon encountering an if statement, you evaluate the condition-1 expression first. If it’s true, the block at is executed and the if statement stops executing (none of the else if or else statements are considered). If it’s false, the else if statements’ conditions evaluate in order. These are optional, and you can supply as many as you like.

If condition-2 evaluates to true, for example, the block at will execute and none of the remaining else if or else statements are considered. Finally, the else block at executes if all of the preceding conditions evaluate to false. Like the else if blocks, the else block is optional.

The function template in Listing 8-14 converts an else argument into Positive, Negative, or Zero.

#include <cstdio>

template<typename T>
constexpr const char* sign(const T& x) {
  const char* result{};
  if (x == 0) { 
    result = "zero";
  } else if (x > 0) { 
    result = "positive";
  } else { 
    result = "negative";
  }
  return result;
}

int main() {
  printf("float 100 is %s
", sign(100.0f));
  printf("int  -200 is %s
", sign(-200));
  printf("char    0 is %s
", sign(char{}));
}
--------------------------------------------------------------------------
float 100 is positive
int  -200 is negative
char    0 is zero

Listing 8-14: An example usage of the if statement

The sign function takes a single argument and determines if it’s equal to 0 , greater than 0 , or less than 0 . Depending on which condition matches, it sets the automatic variable result equal to one of three strings—zero, positive, or negative—and returns this value to the caller.

Initialization Statements and if

You can bind an object’s scope to an if statement by adding an init-statement to if and else if declarations, as demonstrated in Listing 8-15.

if (init-statement; condition-1) {
  // Execute only if condition-1 is true
} else if (init-statement; condition-2) { // optional
  // Execute only if condition-2 is true
}
--snip--

Listing 8-15: An if statement with initializations

You can use this pattern with structured bindings to produce elegant error handling. Listing 8-16 refactors Listing 8-11 using the initialization statement to scope a TextFile to the if statement.

#include <cstdio>

struct TextFile {
  bool success;
  const char* data;
  size_t n_bytes;
};

TextFile read_text_file(const char* path) {
  --snip--
}

int main() {
  if(const auto [success, txt, len] = read_text_file("REAMDE.txt"); success)
  {
    printf("Read %d bytes: %s
", len, txt); 
  } else {
    printf("Failed to open REAMDE.txt."); 
  }
}
--------------------------------------------------------------------------
Read 27 bytes: Sometimes the goat is you. 

Listing 8-16: An extension of Listing 8-11 using structured binding and an if statement to handle errors

You’ve moved the structured binding declaration into the initialization statement portion of the if statement . This scopes each of the unpacked objects—success, txt, and len—to the if block. You use success directly within the conditional expression of if to determine whether read_text_file was successful . If it was, you print the contents of REAMDE.txt . If it wasn’t, you print an error message .

constexpr if Statements

You can make an if statement constexpr; such statements are known as constexpr if statements. A constexpr if statement is evaluated at compile time. Code blocks that correspond to true conditions get emitted, and the rest is ignored.

Usage of the constexpr if follows usage for a regular if statement, as demonstrated in Listing 8-17.

if constexpr (condition-1) {
  // Compile only if condition-1 is true
} else if constexpr (condition-2) { // optional; can be multiple else ifs
  // Compile only if condition-2 is true
}
--snip--
} else { // optional
  // Compile only if none of the conditionals is true
}

Listing 8-17: Usage of the constexpr if statement

In combination with templates and the <type_traits> header, constexpr if statements are extremely powerful. A major use for constexpr if is to provide custom behavior in a function template depending on some attributes of type parameters.

The function template value_of in Listing 8-18 accepts pointers, references, and values. Depending on which kind of object the argument is, value_of returns either the pointed-to value or the value itself.

#include <cstdio>
#include <stdexcept>
#include <type_traits>

template <typename T>
auto value_of(T x) {
  if constexpr (std::is_pointer<T>::value) { 
    if (!x) throw std::runtime_error{ "Null pointer dereference." }; 
    return *x; 
  } else {
    return x; 
  }
}

int main() {
  unsigned long level{ 8998 };
  auto level_ptr = &level;
  auto &level_ref = level;
  printf("Power level = %lu
", value_of(level_ptr)); 
  ++*level_ptr;
  printf("Power level = %lu
", value_of(level_ref)); 
  ++level_ref;
  printf("It's over %lu!
", value_of(level++)); 
  try {
    level_ptr = nullptr;
    value_of(level_ptr);
  } catch(const std::exception& e) {
    printf("Exception: %s
", e.what()); 
  }
}
--------------------------------------------------------------------------
Power level = 8998 
Power level = 8999 
It's over 9000! 
Exception: Null pointer dereference. 

Listing 8-18: An example function template, value_of, employing a constexpr if statement

The value_of function template accepts a single argument x . You determine whether the argument is a pointer type using the std::is_pointer<T> type trait as the conditional expression in a constexpr if statement . In case x is a pointer type, you check for nullptr and throw an exception if one is encountered . If x isn’t a nullptr, you dereference it and return the result . Otherwise, x is not a pointer type, so you return it (because it is therefore a value) .

Within main, you instantiate value_of multiple times with an unsigned long pointer , an unsigned long reference , an unsigned long , and a nullptr respectively.

At runtime, the constexpr if statement disappears; each instantiation of value_of contains one branch of the selection statement or the other. You might be wondering why such a facility is useful. After all, programs are meant to do something useful at runtime, not at compile time. Just flip back to Listing 7-17 (on page 206), and you’ll see that compile time evaluation can substantially simplify your programs by eliminating magic values.

There are other examples where compile time evaluation is popular, especially when creating libraries for others to use. Because library writers usually cannot know all the ways their users will utilize their library, they need to write generic code. Often, they’ll use techniques like those you learned in Chapter 6 so they can achieve compile-time polymorphism. Constructs like constexpr can help when writing this kind of code.

NOTE

If you have a C background, you’ll immediately recognize the utility of compile time evaluation when considering that it almost entirely replaces the need for preprocessor macros.

switch Statements

Chapter 2 first introduced the venerable switch statement. This section delves into the addition of the initialization statement into the switch declaration. The usage is as follows:

switch (init-expression; condition) {
  case (case-a): {
    // Handle case-a here
  } break;
  case (case-b): {
    // Handle case-b here
  } break;
    // Handle other conditions as desired
  default: {
    // Handle the default case here
  }
}

As with if statements, you can instantiate within switch statements .

Listing 8-19 employs an initialization statement within a switch statement.

#include <cstdio>

enum class Color { 
  Mauve,
  Pink,
  Russet
};

struct Result { 
  const char* name;
  Color color;
};

Result observe_shrub(const char* name) { 
  return Result{ name, Color::Russet };
}

int main() {
  const char* description;
  switch (const auto result = observe_shrub("Zaphod"); result.color) {
  case Color::Mauve: {
    description = "mauvey shade of pinky russet";
    break;
  } case Color::Pink: {
    description = "pinky shade of mauvey russet";
    break;
  } case Color::Russet: {
    description = "russety shade of pinky mauve";
    break;
  } default: {
    description = "enigmatic shade of whitish black";
  }}
  printf("The other Shaltanac's joopleberry shrub is "
         "always a more %s.", description); 
}
--------------------------------------------------------------------------
The other Shaltanac's joopleberry shrub is always a more russety shade of
pinky mauve. 

Listing 8-19: Using an initialization expression in a switch statement

You declare the familiar Color enum class and join it with a char* member to form the POD type Result . The function observe_shrub returns a Result . Within main, you call observe_shrub within the initialization expression and store the result in the result variable . Within the conditional expression of switch, you extract the color element of this result . This element determines the case that executes (and sets the description pointer) .

As with the if-statement-plus-initializer syntax, any object initialized in the initialization expression is bound to the scope of the switch statement.

Iteration Statements

Iteration statements execute a statement repeatedly. The four kinds of iteration statements are the while loop, the do-while loop, the for loop, and the range-based for loop.

while Loops

The while loop is the basic iteration mechanism. The usage is as follows:

while (condition) {
  // The statement in the body of the loop
  // executes upon each iteration
}

Before executing an iteration of the loop, the while loop evaluates the condition expression. If true, the loop continues. If false, the loop terminates, as demonstrated in Listing 8-20.

#include <cstdio>
#include <cstdint>

bool double_return_overflow(uint8_t& x) { 
  const auto original = x;
  x *= 2;
  return original > x;
}
int main() {
  uint8_t x{ 1 }; 
  printf("uint8_t:
===
");
  while (!double_return_overflow(x)) {
    printf("%u ", x); 
  }
}
--------------------------------------------------------------------------
uint8_t:
===
2 4 8 16 32 64 128 

Listing 8-20: A program that doubles a uint8_t and prints the new value on each iteration

You declare a double_return_overflow function taking an 8-bit, unsigned integer by reference . This function doubles the argument and checks whether this causes an overflow. If it does, it returns true. If no overflow occurs, it returns false.

You initialize the variable x to 1 before entering the while loop . The conditional expression in the while loop evaluates double_return_overflow(x) . This has the side effect of doubling x, because you’ve passed it by reference. It also returns a value telling you whether the doubling caused x to overflow. The loop will execute when the conditional expression evaluates to true, but double_return_overflow is written so it returns true when the loop should stop. You fix this problem by prepending the logical negation operator (!). (Recall from Chapter 7 that this turns true to false and false to true.) So the while loop is actually asking, “If it’s NOT true that double_return_overflow is true . . .”

The end result is that you print the values 2, then 4, then 8, and so on to 128 .

Notice that the value 1 never prints, because evaluating the conditional expression doubles x. You can modify this behavior by putting the conditional statement at the end of a loop, which yields a do-while loop.

do-while Loops

A do-while loop is identical to a while loop, except the conditional statement evaluates after a loop completes rather than before. Its usage is as follows:

do {
  // The statement in the body of the loop
  // executes upon each iteration
} while (condition);

Because the condition evaluates at the end of a loop, you guarantee that the loop will execute at least once.

Listing 8-21 refactors Listing 8-20 into a do-while loop.

#include <cstdio>
#include <cstdint>

bool double_return_overflow(uint8_t& x) {
  --snip--
}

int main() {
  uint8_t x{ 1 };
  printf("uint8_t:
===
");
  do {
    printf("%u ", x); 
  } while (!double_return_overflow(x));
}
--------------------------------------------------------------------------
uint8_t:
===
1 2 4 8 16 32 64 128 

Listing 8-21: A program that doubles a uint8_t and prints the new value on each iteration

Notice that the output from Listing 8-21 now begins with 1 . All you needed to do was reformat the while loop to put the condition at the end of the loop .

In most situations involving iterations, you have three tasks:

  1. Initialize some object.
  2. Update the object before each iteration.
  3. Inspect the object’s value for some condition.

You can use a while or do-while loop to accomplish part of these tasks, but the for loop provides built-in facilities that make life easier.

for Loops

The for loop is an iteration statement containing three special expressions: initialization, conditional, and iteration, as described in the sections that follow.

The Initialization Expression

The initialization expression is like the initialization of if: it executes only once before the first iteration executes. Any objects declared within the initialization expression have lifetimes bound by the scope of the for loop.

The Conditional Expression

The for loop conditional expression evaluates just before each iteration of the loop. If the conditional evaluates to true, the loop continues to execute. If the conditional evaluates to false, the loop terminates (this behavior is exactly like the conditional of the while and do-while loops).

Like if and switch statements, for permits you to initialize objects with scope equal to the statement’s.

The Iteration Expression

After each iteration of the for loop, the iteration expression evaluates. This happens before the conditional expression evaluates. Note that the iteration expression evaluates after a successful iteration, so the iteration expression won’t execute before the first iteration.

To clarify, the following list outlines the typical execution order in a for loop:

  1. Initialization expression
  2. Conditional expression
  3. (Loop body)
  4. Iteration expression
  5. Conditional expression
  6. (Loop body)

Steps 4 through 6 repeat until a conditional expression returns false.

Usage

Listing 8-22 demonstrates the use of a for loop.

for(initialization; conditional; iteration) {
  // The statement in the body of the loop
  // executes upon each iteration
}

Listing 8-22: Using a for loop

The initialization , conditional , and iteration expressions reside in parentheses preceding the body of the for loop.

Iterating with an Index

The for loops are excellent at iterating over an array-like object’s constituent elements. You use an auxiliary index variable to iterate over the range of valid indices for the array-like object. You can use this index to interact with each array element in sequence. Listing 8-23 employs an index variable to print each element of an array along with its index.

#include <cstdio>

int main() {
  const int x[]{ 1, 1, 2, 3, 5, 8 }; 
  printf("i: x[i]
"); 
  for (int i{}; i < 6; i++) {
    printf("%d: %d
", i, x[i]);
  }
}
--------------------------------------------------------------------------
i: x[i] 
0: 1
1: 1
2: 2
3: 3
4: 5
5: 8

Listing 8-23: A program iterating over an array of Fibonacci numbers

You initialize an int array called x with the first six Fibonacci numbers . After printing a header for the output , you build a for loop containing your initialization , conditional , and iteration expressions. The initialization expression executes first, and it initializes the index variable i to zero.

Listing 8-23 shows a coding pattern that hasn’t changed since the 1950s. You can eliminate a lot of boilerplate code by using the more modern range-based for loop.

Ranged-Based for Loops

The range-based for loop iterates over a range of values without needing an index variable. A range (or range expression) is an object that the range-based for loop knows how to iterate over. Many C++ objects are valid range expressions, including arrays. (All of the stdlib containers you’ll learn about in Part II are also valid range expressions.)

Usage

Ranged-based for loop usage looks like this:

for(range-declaration : range-expression) {
  // The statement in the body of the loop
  // executes upon each iteration
}

A range declaration declares a named variable. This variable must have the same type as implied by the range expression (you can use auto).

Listing 8-24 refactors Listing 8-23 to use a range-based for loop.

#include <cstdio>

int main() {
  const int x[]{ 1, 1, 2, 3, 5, 8 }; 
  for (const auto element : x) {
    printf("%d ", element);
  }
}
--------------------------------------------------------------------------
1 1 2 3 5 8

Listing 8-24: A range-based for loop iterating over the first six Fibonacci numbers

You still declare an array x containing six Fibonacci numbers . The range-based for loop contains a range-declaration expression where you declare the element variable to hold each element of the range. It also contains the range expression x , which contains the elements you want to iterate over to print .

This code is a whole lot cleaner!

Range Expressions

You can define your own types that are also valid range expressions. But you’ll need to specify several functions on your type.

Every range exposes a begin and an end method. These functions represent the common interface that a range-based for loop uses to interact with a range. Both methods return iterators. An iterator is an object that supports operator!=, operator++, and operator*.

Let’s look at how all these pieces fit together. Under the hood, a range-based for loop looks just like the loop in Listing 8-25.

const auto e = range.end();
for(auto b = range.begin(); b != e; ++b) {
  const auto& element = *b;
}

Listing 8-25: A for loop simulating a range-based for loop

The initialization expression stores two variables, b and e , which you initialize to range.begin() and range.end() respectively. The conditional expression checks whether b equals e, in which case the loop has completed (this is by convention). The iteration expression increments b with the prefix operator . Finally, the iterator supports the dereference operator *, so you can extract the pointed-to element .

NOTE

The types returned by begin and end don’t need to be the same. The requirement is that operator!= on begin accepts an end argument to support the comparison begin != end.

A Fibonacci Range

You can implement a FibonacciRange, which will generate an arbitrarily long sequence of Fibonacci numbers. From the previous section, you know that this range must offer a begin and an end method that returns an iterator. This iterator, which is called FibonacciIterator in this example, must in turn offer operator!=, operator++, and operator*.

Listing 8-26 implements a FibonacciIterator and a FibonacciRange.

struct FibonacciIterator {
  bool operator!=(int x) const {
    return x >= current; 
  }

  FibonacciIterator& operator++() {
    const auto tmp = current; 
    current += last; 
    last = tmp; 
    return *this; 
  }

  int operator*() const {
    return current; 
  }
private:
  int current{ 1 }, last{ 1 };
};

struct FibonacciRange {
  explicit FibonacciRange(int max) : max{ max } { }
  FibonacciIterator begin() const { 
    return FibonacciIterator{};
  }
  int end() const { 
    return max;
  }
private:
  const int max;
};

Listing 8-26: An implementation of FibonacciIterator and FibonacciRange

The FibonacciIterator has two fields, current and last, which are initialized to 1. These keep track of two values in the Fibonacci sequence. Its operator!= checks whether the argument is greater than or equal to current . Recall that this argument is used within the range-based for loop in the conditional expression. It should return true if elements remain in the range; otherwise, it returns false. The operator++ appears in the iteration expression and is responsible for setting up the iterator for the next iteration. You first save current value into the temporary variable tmp . Next, you increment current by last, yielding the next Fibonacci number . (This follows from the definition of a Fibonacci sequence.) Then you set last equal to tmp and return a reference to this . Finally, you implement operator*, which returns current directly.

FibonacciRange is much simpler. Its constructor takes a max argument that defines an upper limit for the range . The begin method returns a fresh FibonacciIterator , and the end method returns max .

It should now be apparent why you need to implement bool operator!=(int x) on FibonacciIterator rather than, for example, bool operator!=(const FibonacciIterator& x): a FibonacciRange returns an int from end().

You can use the FibonacciRange in a ranged-based for loop, as demonstrated in Listing 8-27.

#include <cstdio>

struct FibonacciIterator {
  --snip--
};

struct FibonacciRange {
  --snip--;
};

int main() {
  for (const auto i : FibonacciRange{ 5000 }) {
    printf("%d ", i); 
  }
}
--------------------------------------------------------------------------
1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 

Listing 8-27: Using FibonacciRange in a program

It took a little work to implement FibonacciIterator and FibonacciRange in Listing 8-26, but the payoff is substantial. Within main, you simply construct a FibonacciRange with the desired upper limit , and the range-based for loop takes care of everything else for you. You simply use the resulting elements within the for loop .

Listing 8-27 is functionally equivalent to Listing 8-28, which converts the range-based for loop to a traditional for loop.

#include <cstdio>

struct FibonacciIterator {
  --snip--
};

struct FibonacciRange {
  --snip--;
};

int main() {
  FibonacciRange range{ 5000 };
  const auto end = range.end();
  for (const auto x = range.begin(); x != end ; ++x ) {
    const auto i = *x;
    printf("%d ", i);
  }
}
--------------------------------------------------------------------------
1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181

Listing 8-28: A refactor of Listing 8-27 using a traditional for loop

Listing 8-28 demonstrates how all of the pieces fit together. Calling range.begin() yields a FibonacciIterator. When you call range.end() , it yields an int. These types come straight from the method definitions of begin() and end() on FibonacciRange. The conditional statement uses operator!=(int) on FibonacciIterator to get the following behavior: if the iterator x has gone past the int argument to operator!=, the conditional evaluates to false and the loop ends. You’ve also implemented operator++ on FibonacciIterator so ++x increments the Fibonacci number within FibonacciIterator.

When you compare Listings 8-27 and 8-28, you can see just how much tedium range-based for loops hide.

NOTE

You might be thinking, “Sure, the range-based for loop looks a lot cleaner, but implementing FibonacciIterator and FibonacciRange is a lot of work.” That’s a great point, and for one-time-use code, you probably wouldn’t refactor code in this way. Ranges are mainly useful if you’re writing library code, writing code that you’ll reuse often, or simply consuming ranges that someone else has written.

Jump Statements

Jump statements, including the break, continue, and goto statements, transfer control flow. Unlike selection statements, jump statements are not conditional. You should avoid using them because they can almost always be replaced with higher-level control structures. They’re discussed here because you might see them in older C++ code and they still play a central role in a lot of C code.

break Statements

The break statement terminates execution of the enclosing iteration or switch statement. Once break completes, execution transfers to the statement immediately following the for, range-based for, while, do-while, or switch statement.

You’ve already used break within switch statements; once a case completes, the break statement terminates the switch. Recall that, without a break statement, the switch statement would continue executing all of the following cases.

Listing 8-29 refactors Listing 8-27 to break out of a range-based for loop if the iterator i equals 21.

#include <cstdio>

struct FibonacciIterator {
  --snip--
};

struct FibonacciRange {
  --snip--;
};

int main() {
  for (auto i : FibonacciRange{ 5000 }) {
    if (i == 21) { 
      printf("*** "); 
      break; 
    }
    printf("%d ", i);
  }
}
--------------------------------------------------------------------------
1 2 3 5 8 13 *** 

Listing 8-29: A refactor of Listing 8-27 that breaks if the iterator equals 21

An if statement is added that checks whether i is 21 . If it is, you print three asterisks *** and break . Notice the output: rather than printing 21, the program prints three asterisks and the for loop terminates. Compare this to the output of Listing 8-27.

continue Statements

The continue statement skips the remainder of an enclosing iteration statement and continues with the next iteration. Listing 8-30 replaces the break in Listing 8-29 with a continue.

#include <cstdio>

struct FibonacciIterator {
  --snip--
};

struct FibonacciRange {
  --snip--;
};

int main() {
  for (auto i : FibonacciRange{ 5000 }) {
    if (i == 21) {
      printf("*** "); 
      continue; 
    }
    printf("%d ", i);
  }
}
--------------------------------------------------------------------------
1 2 3 5 8 13 *** 34 55 89 144 233 377 610 987 1597 2584 4181

Listing 8-30: A refactor of Listing 8-29 to use continue instead of break

You still print three asterisks when i is 21, but you use continue instead of break . This causes 21 not to print, like Listing 8-29; however, unlike Listing 8-29, Listing 8-30 continues iterating. (Compare the output.)

goto Statements

The goto statement is an unconditional jump. The target of a goto statement is a label.

Labels

Labels are identifiers you can add to any statement. Labels give statements a name, and they have no direct impact on the program. To assign a label, prepend a statement with the desired name of the label followed by a semicolon.

Listing 8-31 adds the labels luke and yoda to a simple program.

#include <cstdio>

int main() {
luke: 
  printf("I'm not afraid.
");
yoda: 
  printf("You will be.");
}
--------------------------------------------------------------------------
I'm not afraid.
You will be.

Listing 8-31: A simple program with labels

The labels do nothing on their own.

goto Usage

The goto statement’s usage is as follows:

goto label;

For example, you can employ goto statements to needlessly obfuscate the simple program in Listing 8-32.

#include <cstdio>

int main() {
  goto silent_bob; 
luke:
  printf("I'm not afraid.
");
  goto yoda; 
silent_bob:
  goto luke; 
yoda:
  printf("You will be.");
}
--------------------------------------------------------------------------
I'm not afraid.
You will be.

Listing 8-32: Spaghetti code showcasing the goto statement

Control flow in Listing 8-32 passes to silent_bob , then to luke , and then to yoda .

The Role of goto in Modern C++ Programs

In modern C++, there is no good role for goto statements. Don’t use them.

NOTE

In poorly written C++ (and in most C code), you might see goto used as a primitive error-handling mechanism. A lot of system programming entails acquiring resources, checking for error conditions, and cleaning up resources. The RAII paradigm neatly abstracts all of these details, but C doesn’t have RAII available. See the Overture to C Programmers on page xxxvii for more information.

Summary

In this chapter, you worked through different kinds of statements you can employ in your programs. They included declarations and initializations, selection statements, and iteration statements.

NOTE

Keep in mind that try-catch blocks are also statements, but they were already discussed in great detail in Chapter 4.

EXERCISES

8-1. Refactor Listing 8-27 into separate translation units: one for main and another for FibonacciRange and FibonacciIterator. Use a header file to share definitions between the two translation units.

8-2. Implement a PrimeNumberRange class that can be used in a range exception to iterate over all prime numbers less than a given value. Again, use a separate header and source file.

8-3. Integrate PrimeNumberRange into Listing 8-27, adding another loop that generates all prime numbers less than 5,000.

FURTHER READING

  • ISO International Standard ISO/IEC (2017) — Programming Language C++ (International Organization for Standardization; Geneva, Switzerland; https://isocpp.org/std/the-standard/)
  • Random Number Generation and Monte Carlo Methods, 2nd Edition, by James E. Gentle (Springer-Verlag, 2003)
  • Random Number Generation and Quasi-Monte Carlo Methods by Harald Niederreiter (SIAM Vol. 63, 1992)
..................Content has been hidden....................

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