C++ For C# Developers: Part 6 – Control Flow
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
- Part 1: Introduction
- Part 2: Primitive Types and Literals
- Part 3: Variables and Initialization
- Part 4: Functions
- Part 5: Build Model
- Part 6: Control Flow
- Part 7: Pointers, Arrays, and Strings
- Part 8: References
- Part 9: Enumerations
- Part 10: Struct Basics
- Part 11: Struct Functions
- Part 12: Constructors and Destructors
- Part 13: Initialization
- Part 14: Inheritance
- Part 15: Struct and Class Permissions
- Part 16: Struct and Class Wrapup
- Part 17: Namespaces
- Part 18: Exceptions
- Part 19: Dynamic Allocation
- Part 20: Implicit Type Conversion
- Part 21: Casting and RTTI
- Part 22: Lambdas
- Part 23: Compile-Time Programming
- Part 24: Preprocessor
- Part 25: Intro to Templates
- Part 26: Template Parameters
- Part 27: Template Deduction and Specialization
- Part 28: Variadic Templates
- Part 29: Template Constraints
- Part 30: Type Aliases
- Part 31: Deconstructing and Attributes
- Part 32: Thread-Local Storage and Volatile
- Part 33: Alignment, Assembly, and Language Linkage
- Part 34: Fold Expressions and Elaborated Type Specifiers
- Part 35: Modules, The New Build Model
- Part 36: Coroutines
- Part 37: Missing Language Features
- Part 38: C Standard Library
- Part 39: Language Support Library
- Part 40: Utilities Library
- Part 41: System Integration Library
- Part 42: Numbers Library
- Part 43: Threading Library
- Part 44: Strings Library
- Part 45: Array Containers Library
- Part 46: Other Containers Library
- Part 47: Containers Library Wrapup
- Part 48: Algorithms Library
- Part 49: Ranges and Parallel Algorithms
- Part 50: I/O Library
- Part 51: Missing Library Features
- Part 52: Idioms and Best Practices
- Part 53: Conclusion
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) { index++; 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.
#1 by Shachar Langbeheim on June 23rd, 2020 ·
demonstrating how switch cases are error-prone, you have a couple of bugs in your switch examples :)
in the fallthrough switch code:
case 2:
DoAtLeast2();
DoAtLeast2(); -> should be DoAtLeast1();
in the curly braces in switch code:
switch (someVal)
{
case 1:
{
….
} -> no break here. This will cause a “someVal is two” log to be printed when someVal is 1
case 2:
#2 by jackson on June 23rd, 2020 ·
Hah, good catches! Who’s to say I didn’t mean to fall through? ;-)
I updated the article with both fixes.
#3 by cod on July 7th, 2020 ·
which version of c++ allow an optional initialization inside if statement?
#4 by jackson on July 9th, 2020 ·
C++17.
#5 by renato on October 9th, 2021 ·
The entire series is great, thanks a lot! Just one comment on this chapter, the “break / continue” while loop is missing an “index++”.
#6 by jackson on November 21st, 2021 ·
Thanks, I’ve updated the article to add it.