1.6 Control Flow

C++ offers 5 types of control flow statements. if...else, for loop and while loop are pretty much the same as in Rust, but the switch statement is much less powerful than Rust’s match. Additionally there is a goto statement which performs unconditional jump.

If you have experience in Javascript or Java, most of C++’s control flow syntax will be familiar to you. The conditional test associated with if, for, while and switch must be surrounded by parentheses.

1.6.1 if...else

Like Rust, C++ offers if and else keywords to work with conditionals. Unlike Rust, C++ is not expression-oriented, so you cannot write:

int a = if (true) { 5 } else { 10 };

But this kind of conditional assignment is a very common pattern, so C++ invented yet another syntax specifically designed for this single task: the ternary operator. So, instead of writing:

int a;
if (true) { a = 5; } else { a = 10; };

you could write:

int a = true ? 5 : 10;

The braces around the statement after the condition of if can be omitted if the statement can be written in a single line. For example:

if (i > 5) {
    std::cout << "i is greater than 5" << std::endl;
}

can be reduced to:

if (i > 5)
    std::cout << "i is greater than 5" << std::endl;

1.6.2 while Loop

The while loop in C++ has nothing different from Rust, just remember to wrap the test expression with parentheses.

#include <iostream>
int i = 10;
while (i > 0)
{
    i--;
    if (i == 8)
    {
        continue;
    }
    if (i == 5)
    {
        break;
    }
    std::cout << i << std::endl;
}

1.6.3 for Loop

A traditional C-style for loop looks like this:

for (<initializationStatement>; <testExpression>; <updateStatement>)
{
    // do something
}

For example:

#include <iostream>
puts("|i|j|");
for (int i = 0; i < 2; i++)
{
    for (int j = 0; j < 3; j++)
    {
        printf("|%d|%d|\n", i, j);
    }
}

C++11 introduced the range-based for statement, which is also known as a for...in loop in most other languages (Rust, Swift, Python, Ruby, …). The syntax itself is easy but knowing the relationship between the element and the iterable can be tricky. Fortuantely, we are Rustaceans, so an easy way for me to illustrate and for you to understand is to write a few equivalent examples in Rust and C++.

1.6.3.1 Scenario 1: Copying (Cloning)

// Rust
let v = vec!["a".to_string(), "b".to_string(), "c".to_string()];
for e.clone() in &v {
    println!("{}", e);
}
// C++
#include <iostream>
#include <vector>
std::vector<std::string> a{"a", "b", "c"};
for (auto s : a) // s has type `std::string`
{
    std::cout << s << std::endl;
}

Note how Rust makes it crystal clear that copying during iteration and using String for instead of &str for static strings are anti-patterns and how C++ makes it easy to write such inefficient code.

1.6.3.2 Scenario 2: As Reference (Borrowing)

// Rust
let v = vec![1, 2, 3, 4, 5];
for e in &v {
    println!("{}", *e);
}
// C++
#include <iostream>
#include <vector>
std::vector<int> a{1, 2, 3, 4, 5};
for (auto& num : a)
{
    printf("%d ", num); // no asterisk!
}

1.6.3.3 Scenario 3: Mutation

// Rust
let mut v = vec![1, 2, 3, 4, 5];
for e in &mut v {
    *e = *e + 1;
}
assert_eq!(v, vec![2, 3, 4, 5, 6]);
// C++
#include <vector>
#include <cassert>
std::vector<int> a{1, 2, 3, 4, 5};
for (auto &num : a)
{
    num++;
}
assert(a == (std::vector<int>{2, 3, 4, 5, 6}));

Note the parentheses surrounding the second argument of assert, without which we would get an error. This is because assert is a so called #define macro, which simply parses its arguments as comma-separated identifiers and does text replacement. Since std::vector<int>{2, 3, 4, 5, 6} contains commas, it has to be escaped with parentheses.1 This macro is actually defined in assert.h which is part of C’s std, and its more or less copied verbatim into C++’s cassert.

1.6.4 goto Statement and Breaking outer Loops

Rust, like Java and Python, allows you to break an outer loop from an inner loop:

for i in 0..3 {
    'for_j: for j in 0..3 {
        for k in 0..3 {
            if i == 1 {
                break 'for_j;
            }
            println!("{} {} {}", i, j, k);
        }
    }
}

C++ doesn’t support this natively, so it’s common to use the goto trick to achive this2.

#include <iostream>
void break_outer()
{
    puts("Break outer loop using goto:");
    puts("i j k");
    for (int i = 0; i < 3; ++i)
    {

        for (int j = 0; j < 3; ++j)
        {
            for (int k = 0; k < 3; ++k)
            {
                if (i == 1)
                {
                    goto for_j_end;
                }
                printf("%d %d %d\n", i, j, k);
            }
        }
    for_j_end:
    {
        // cannot be left blank. Needs a placeholder
    }
    }
}

The goto statement must be in the same function as the label it is referring, so duplicated labels across different functions won’t cause conflicts.

Alternatively you can use a flag, which is more verbose, and I think this is even less readable:

puts("Break outer loop using flag:");
puts("i j k");
bool i_is_1{false};
for (int i = 0; i < 3; ++i)
{

    for (int j = 0; j < 3; ++j)
    {
        for (int k = 0; k < 3; ++k)
        {
            if (i == 1)
            {
                i_is_1 = true;
                break;
            }
            else
            {
                i_is_1 = false;
            }
            printf("%d %d %d\n", i, j, k);
        }
        if (i_is_1)
        {
            break;
        }
    }
}

There is another cleaner way, using the lambda trick (less common than the goto approach):

puts("Break outer loop using lambda:");
puts("i j k");
for (int i = 0; i < 3; ++i)
{
    [&] {
        for (int j = 0; j < 3; ++j)
        {
            for (int k = 0; k < 3; ++k)
            {
                if (i == 1)
                {
                    return;
                }
                printf("%d %d %d\n", i, j, k);
            }
        }
    }();
}

I’ll get back to lamdas later. For now just accept that they are roughly equivalent to Rust’s closures.

1.6.5 switch

C++’s switch is essentially a shortcut for a series of if...else if...else if...else statements when you want to match the value a single expression to a range of constant values. For example:

The following C++ code

int d = 5;
if (d == 0) {
    puts("It's Sunday!");
} else if (d == 6) {
    puts("It's Saturday!");
} else {
    puts("It's weekday.");
}

is equivalent to:

switch (d)
{
case 0:
    puts("It's Sunday!");
    break;
case 6:
    puts("It's Saturday!");
    break;
default:
    puts("It's weekday.");
    break;
}

(I’ll start to omit headers from now on.)

Note the break statement at the end of each case, without which the default branch will always be triggered, which is clearly not we mean to do3.

You can group several values into a single branch:

int d = 5;
switch (d)
{
case 0:
    puts("It's Sunday!");
    break;
case 6:
    puts("It's Saturday!");
    break;
case 1:
case 2:
case 3:
case 4:
case 5:
    puts("It's weekday.");
    break;
default:
    puts("Not a valid day of week!");
    break;
}

This is all about switch in C++. (You need to unlearn the pattern matching in Rust)