Let’s continue the series with another nuts-and-bolts topic: control flow. The Venn diagram is largely overlap here, but both C# and C++ have their own unique features and some of the features in common have important differences between the two languages. Read on for the nitty-gritty!

Table of Contents

If and Else

Let’s start with the lowly if statement, which is just like in C#:

if (someBool)
{
    // ... execute this if someBool is true
}

Unlike C#, there’s an optional initialization statement that can be tacked on to the beginning. It’s like the first statement of a for loop and is usually used to declare a variable scoped to the if statement. Here’s how it’s typically used:

if (ResultCode code = DoSomethingThatCouldFail(); code == FAILURE)
{
    // ... execute this if DoSomethingThatCouldFail returned FAILURE
}

The else part is just like C#:

if (someBool)
{
    // ... execute this if someBool is true
}
else
{
    // ... execute this if someBool is false
}
Goto and Labels

The goto statement is also similar to in C#. We create a label and then name it in our goto statement:

void DoLotsOfThingsThatMightFail()
{
    if (!DoThingA())
    {
        goto handleFailure;
    }
    if (!DoThingB())
    {
        goto handleFailure;
    }
    if (!DoThingC())
    {
        goto handleFailure;
    }
 
    handleFailure:
        DebugLog("Critical operation failed. Aborting program.");
        exit(1);
}

Like in C#, the label to goto must be in the same function. Unlike in C#, the label can’t be inside of a try or catch block.

One subtle difference is that a C++ goto can be used to skip past the declaration of variables, but not the initialization of them. For example:

void Bad()
{
    goto myLabel;
    int x = 1; // Un-skippable initialization
    myLabel:
    DebugLog(x);
}
 
void Ok()
{
    goto myLabel;
    int x; // No initialization. Can be skipped.
    myLabel:
    DebugLog(x); // Using uninitialized variable
}

As with any use of an uninitialized variable, this is undefined behavior and will likely lead to severe errors. Care should be taken to ensure that the variable is eventually initialized before it’s read.

Switch

C++ switch, case, and default are similar to their C# counterparts:

switch (someVal)
{
    case 1:
        DebugLog("someVal is one");
        break;
    case 2:
        DebugLog("someVal is two");
        break;
    case 3:
        DebugLog("someVal is three");
        break;
    default:
        DebugLog("Unhandled value");
        break;
}

One difference is that a case that’s not empty can omit the break and “fall through” to the next case. This is sometimes considered error-prone, but can also reduce duplication. These two are equivalent:

// C#
switch (someVal)
{
    case 3:
        DoAtLeast3();
        DoAtLeast2();
        DoAtLeast1();
        break;
    case 2:
        DoAtLeast2();
        DoAtLeast1();
        break;
    case 1:
        DoAtLeast1();
        break;
}
 
// C++
switch (someVal)
{
    case 3:
        DoAtLeast3();
    case 2:
        DoAtLeast2();
    case 1:
        DoAtLeast1();
}

Another difference that curly braces are required in a case in order to declare variables:

switch (someVal)
{
    case 1:
    {
        int points = CalculatePoints();
        DebugLog(points);
        break;
    }
    case 2:
        DebugLog("someVal is two");
        break;
    case 3:
        DebugLog("someVal is three");
        break;
}

C++ switch statements also support initialization statements, much like with if:

switch (ResultCode code = DoSomethingThatCouldFail(); code)
{
    case FAILURE:
        DebugLog("Failed");
        break;
    case SUCCESS:
        DebugLog("Succeeded");
        break;
    default:
        DebugLog("Unhandled error code");
        DebugLog(code);
        break;
}

Unlike C#, a switch can only be used on integer and enumeration types. An chain of if and else is needed to handle anything else:

if (player == localPlayer)
{
    // .. handle the local player
}
else if (player == adminPlayer)
{
    // .. handle the admin player
}

C#’s pattern matching is also not supported, so there’s no ability to write case int x: to match all int values and bind their value to x. There’s also no when clauses, so we can’t write case Player p when p.NumLives > 0:. Instead, we again do these with if and else in C++.

Also not supported is goto case X;. Instead, we need to create our own label and goto it:

switch (someVal)
{
    case DO_B:
        doB:
        DoB();
        break;
    case DO_A_AND_B:
        DoA();
        goto doB;
}
Ternary

The ternary operator in C++ is also similar to the C# version:

int damage = hasQuadDamage ? weapon.Damage * 4 : weapon.Damage;

As in C#, this is equivalent to:

int damage;
if (hasQuadDamage)
    damage = weapon.Damage * 4;
else
    damage = weapon.Damage;

The C++ version is much looser with what we can put into the ? and : parts. For example, we can throw an exception:

SaveHighScore() ? Unpause() : throw "Failed to save high score";

In this case, the type of the expression is whatever type the non-throw part has: the return value of Unpause. We could even throw in both parts:

errorCode == FATAL ? throw FatalError() : throw RecoverableError();

The type of the expression is void when we do this. Exceptions are, of course, their own category of control flow and one we’ll cover more in depth later on in the series.

There are many more rules to determine the type of the ternary expression, but normally we just use the same type in both the ? and the : parts like we did with the damage example. In this most typical case, the type of the ternary expression is the same as either part.

While, Do-While, Break, and Continue

while and do-while loops are essentially exactly the same as in C#:

while (NotAtTarget())
{
    MoveTowardTarget();
}
 
do
{
    MoveTowardTarget()
} while (NotAtTarget());

break and continue also work the same way:

int index = 0;
int winnerIndex = -1;
while (index < numPlayers)
{
    // Dead players can't be the winner
    // Skip the rest of the loop body by using `continue`
    if (GetPlayer(index).Health <= 0)
    {
        continue;
    }
 
    // Found the winner if they have at least 100 points
    // No need to keep searching, so use `break` to end the loop
    if (GetPlayer(index).Points >= 100)
    {
        winnerIndex = index;
        break;
    }
}
if (winnerIndex < 0)
{
    DebugLog("no winner yet");
}
else
{
    DebugLog("Player", index, "won");
}
For

The regular three-part for loop is also basically the same as in C#:

for (int i = 0; i < numBullets; ++i)
{
    SpawnBullet();
}

C++ has a variant of for that takes the place of foreach in C#. It’s called the “range-based for loop” and it’s denoted by a colon:

int totalScore = 0;
for (int score : scores)
{
    totalScore += score;
}

It even supports an optional initialization statement like we saw with if:

int totalScore = 0;
for (int index = 0; int score : scores)
{
    DebugLog("Score at index", index, "is", score);
    totalScore += score;
    index++;
}

The compiler essentially converts range-based for loops into regular for loops like this:

int totalScore = 0;
{
    int index = 0;
    auto&& range = scores;
    auto cur = begin(range); // or range.begin()
    auto theEnd = end(range); // or range.end()
    for ( ; cur != theEnd; ++cur)
    {
        int score = *cur;
        DebugLog("Score at index", index, "is", score);
        totalScore += score;
        index++;
    }
}

We’ll cover pointers and references soon, but for now auto&& range = scores is essentially making a synonym for scores called range and *cur is taking the value pointed at by the cur pointer.

There must be begin and end functions that take whatever type scores is, otherwise scores must have methods called begin and end that take no parameters. If the compiler can’t find either set of begin and end functions, there will be a compiler error. Regardless of where they are, these functions also need to return a type that can be compared for inequality (cur != end), pre-incremented (++cur), and dereferenced (*cur) or there will be a compiler error.

As we’ll see throughout the series, there are many types that fit this criteria and many user-created types are designed to fit it too.

C#-Exclusive Operators

Some of C#’s control flow operators don’t exist in C++ at all. First, there’s no ?? or ??= operator. The ternary operator or if is usually used in its place:

// Replacement for `??` operator
int* scores = m_Scores ? m_Scores : new Scores();
 
// Replacement for `??=` operator
if (!scores) scores = new Scores();

Second, there’s no ?. or ?[] operator so we usually just write it out with a ternary operator for one level of indirection and if for more levels:

// Replacement for `?.` operator
int* scores = m_Scores ? m_Scores->Today : nullptr;
 
// Replacement for `?[]` operator
int* highScore = scores ? &scores[0] : nullptr;

Note that nullptr is equivalent to null in C# and is simply a null value compliant with any type of pointer but not with integers.

Return

Suitably, we end today with return. The typical version is just like in C#:

int CalculateScore(int numKills, int numDeaths)
{
    return numKills*10 - numDeaths*2;
}

There’s an alternative version where curly braces are used to more-or-less pass parameters to the constructor of the return type:

CircleStats GetCircleInfo(float radius)
{
    return { 2*PI*radius, PI*radius*radius };
}

We’ll go further into constructors later in the series. For now, there’s an important guarantee in the C++ language about returned objects like CircleStats: copy elision. This means that if the values in the curly braces are “pure,” like these simple constants and primitives, then the CircleStats object will be initialized at the call site. This means CircleStats won’t be allocated on the stack within GetCircleInfo and then copied to the call site when GetCircleInfo returns. This helps us avoid expensive copies when copying the return value involves copying a large amount of data such as a big array.

Conclusion

A lot of the control flow mechanisms are shared between C++ and C#. We still have if, else, ?:, switch, goto, while, do, for, foreach/”range-based for“, break, continue, and return.

C# additionally has ??, ??=, ?., and ?[], but C++ additionally has “init expressions” on if, switch, and range-based for loops, return value copy elision, and more flexibility with ?:, goto, and switch.

These differences lead to different idioms in the way we write code in the two languages. For example, we need begin and end functions or methods in order to enable range-based for loops for our types in C++. If we were writing C#, we’d typically implement the IEnumerator<T> interface.