C++ Conditional Statements
Some time ago I wrote about beginning C++ and I want to extend my blog with some articles about junior programmers. In this one I will delve into the conditional statements. There is also an advanced section for those of you who already know the basics but want to learn some additinal stuff.
Table of Contents
Introduction
Well conditional statemetns are pretty easy. They are one of the first concepts that you learn in any program. I will assume you already know how to program a simple “Hello, World” in C++. Every serious program that you are going to write as a programmer will require you to have some conditional code. This means you telling the program do this if X happens or do that if Y happens. Conditional statements are marked with the if
keyword along with some other keywords being involved like else
or else if
.
To see how this works imagine a program that asks the user for his age:
#include <iostream>
int main()
{
int age;
std::cout << "Enter your age: ";
std::cin >> age;
return 0;
}
You might want to do something only if the age is above 18. A typical age gating. So what do we do? We add a conditional statement:
#include <iostream>
int main()
{
int age;
std::cout << "Enter your age: ";
std::cin >> age;
if(age > 18)
{
std::cout << "Good, You're above 18!";
}
return 0;
}
So in summary the if condition just checks a condition and then executes the code inside the curly brackets only if the condition evaluates to true. It is important to note here that C++ will evaluate some other values to true or false too. If you have a number x that is equal to 0 that will be considered a false. Anything different than that will be considered true. This means that you can easily check pointers, numbers, characters, etc. for having a 0 value.
Now if you want something to happen both when the condition is met or not met then you would use the if...else
construct:
if(age > 18)
{
std::cout << "Good, You're above 18!";
}
else
{
std::cout << "Bad, You're below or equal to 18!";
}
And you can also often get into a shortcut with a couple of chained else if statetments that are a simplified version of a lot of inner conditions. To explain this imagine the following construct:
if(age > 18)
{
std::cout << "Good, You're above 18!";
}
else
{
if(age == 18)
{
std::cout << "Well, You're equal to 18! Go on";
}
else
{
std::cout << "Bad, You're below 18!";
}
}
Since this adds visual complexity you can sckip a couple of identations and brackets and write the following:
if(age > 18)
{
std::cout << "Good, You're above 18!";
}
else if(age == 18)
{
std::cout << "Well, You're equal to 18! Go on";
}
else
{
std::cout << "Bad, You're below 18!";
}
Well this is everything simple there is to know about conditions. If you’re up for it you can examine some more advanced topics now.
If you want to know more on how to compile these examples check more about CMake on this blog to get you started. If you do not know what CMake is start here.
Advanced
In this section I will explain a few more advanced concepts about the conditions. If you’re not familiar C++ is a langauge that is compiled essentially to an assembly language. Assembly language is very simple it represents instructions to the processor with some values that tell what the processor should do. They are much more verbose than your normal C++ code. For example if you want to add two numbers stored in a variable using the assembly language you would have to write code that:
- load number from memory X into the processor register A
- load number from memory Y into the processor register B
- add the numbers stored into register A and B and store the result in register A
- save the number A into memory location X
And this in C++ will just be represented as x = x + y
. It is good to appreciate the simplicity that you get even with a language that is considered low level.
Why do I tell you this. Well let’s get into the first concept jumps.
Avoiding Jumps
Whenever the code needs to do a different thing based on a condition there is a jump. This means that the program was executing the code one after the other but suddenly it has to determine whether to take path A or path B. Usually when the code is compiled it will decide that path A is the main path and it will jump if the condition tells it to go to path B.
A jump is a common assembly language statement. Normal jumps are usually fast but jumps based on conditions have some caveats.
If we take the example from above:
int age;
std::cout << "Enter your age: ";
std::cin >> age;
if(age > 18)
{
std::cout << "Good, You're above 18!";
}
return 0;
The compiler almost always decides the if condition as being the main path and skipping it as being the path B. This means that it will not execute the jump if the condition is true and it will jump to return 0
if the condition is false.
Why do we care about jumps?
The processor is very good at predicting the next code to be executed. So good in fact that there are optimizations that load the next instructions to be executed beforehand. But when a conditional statement is reached the processor has no way of knowing which code to prepare. If it preprares some code and then you execute a jump statement it literally has to trash that prepared code and recalculate its next statement. This means that the processor is faster if it predicts right and slower if it predicts wrong. We can also help the compiler produce better code for the processor if we have insight on how hte code is going to be executed.
Not writing else statements (more of a myth)
One of the common places where a jump is executed is an if...else
construct. This is because the code has to jump in both cases:
- if the condition is true it will execute the if code and then jump after the else
- if the condition is false it will conditionally jump to the else code and not jump after that and just continue
These jumps are not so bad though since the processor will predict the next statements based on regular jumps. Regular jumps are the ones that tell the CPU to go on a specific line of code without any conditions. If we have a condition though there is the real slowdown. So the problem here is really going to be optimized away from the compiler and the processor will be able to take the right decision on what to execute next. You won’t really speed it up if you remove the else statement.
There is a case that it would matter though and this is in a function. Since most programmers write conditions with early outs so that the if statements don’t ident code a lot you could have this optimization where writing an else statement might actually have more harm if you want the early out to be the path B. Consider the following code on godbolt:
If you don’t understand engouh about assembly which is on the right – just look at the collored sections they convey which part on the left has been translated to which part on the right. What you will see is that it will execute bar - 1
first and execute a jump bar + 1
if bar is larger than 1.0f
If you had an else block here you would have a different result:
In this case it will execute them the other way arround where it will do the addition and jump for the subtraction.
C++ 20’s [[likely]] and [[unlikely]] attributes
The above behavior might not be what you want to achieve though. And if you’re not doing early out but know that the if statement is going to be much more common then you could actually hint at the compiler using the [[likely]]
and [[unlikely]]
. They are put right after the if statement and condition like this:
int age;
std::cout << "Enter your age: ";
std::cin >> age;
if(age > 18) [[likely]]
{
std::cout << "Good, You're above 18!";
}
return 0;
This way the compiler will optimize it in such a way that it doesn’t do a dynamic jump if age is greater than 18 and do the jump if it turns out to be less than 18. If you examine the godbolt code you will see this:
Avoiding Conditions (aka Branchless Programming)
Well it is curious but some people have found out that you can do the so called branchless programming which essentially means that you do not do jumps. This can be achieved through some clever math. If you remember from very basic math multiplication if you multiply by 0 you always get a 0. How does this help us? Well consider a boolean value. It is represented by a true or false statement but the computer stores everything as numbers so true is 1 and false is 0.
If we take this example from above:
#include <iostream>
int main()
{
int age;
std::cout << "Enter your age: ";
std::cin >> age;
if(age > 18)
{
std::cout << "Good, You're above 18!";
}
else
{
std::cout << "Bad, You're below or equal to 18!";
}
return 0;
}
You can very easily rewrite it without any branching:
#include <iostream>
#include <cstdint>
int main()
{
int age;
std::cout << "Enter your age: ";
std::cin >> age;
const char* above_18_message = "Good, You're above 18!";
const char* below_18_message = "Bad, You're below or equal to 18!";
const int is_above_18 = static_cast<int>(age > 18);
const char* result = reinterpret_cast<const char*>(is_above_18 * reinterpret_cast<std::intptr_t>(above_18_message)
+ !is_above_18 * reinterpret_cast<std::intptr_t>(below_18_message));
std::cout << result << std::endl;
return 0;
}
This code has no branching but still achieves the same result that you had in the introduction section. This is a lot more uglier though due to the many casts used. You should only do this in really performance critical situations. If you want you can check the assembly code and see for yourself that there is no jump statements:
Ternary Operator
It is important to note that the above example is very ugly and also could produce more assembly code in simple examples like the one I provide here. If the case is as simple as choosing between two values it is good to know that the ternary operator actually doesn’t produce any branching as well:
Compile Time Conditions
If a condition can be evaluated at compile time with constant values then suggest to the compiler to do it at compile time. This can be done through the constexpr
keyword placed after the if
and before the condition:
const int a = 5;
if constexpr (a < 5)
{
return 1;
}
return 0;
The compiler will optimize this away and no branching will be generated at runtime.
Conclusion
Well conditional statements are pretty simple as a concept but as you can see you can go even deeper with some good practices. I really enjoy the option to optimize using the likely and unlikely attributes but to know how to use them you need to know a bit about the generated assembly code. I hope you found this article useful and if you did you can check out more articles on this blog.
You can get a branchless version without ugly casts:
static const char* messages[] = { "Bad, You're below or equal to 18!", "Good, You're above 18!" };
const char* result = messages[age > 18];
Leave a comment