There are two kinds of if
statements in modern C++: runtime if
and compile-time if constexpr
.
Runtime if
looks like:
1 2 |
if (condition) statement-true if (condition) statement-true else statement-false |
In both forms of above if the result of condition
yields to true
, then statement-true
is executed. Otherwise (in the second form of above) the statement-false
is executed. Any expression that yields something convertible to bool
in a context of if
can be used as a condition
, for example:
1 2 3 4 5 6 7 8 9 |
// 1. Case for integers int number{}; if (number) {/*...*/} if (number != 0) {/*...*/} // 2. Case for pointers char* pointer{}; if (pointer) {/*...*/} if (pointer != nullptr) {/*...*/} |
In the example above every couple of if
statements performs the similar checks. In case 1 both if
statements checks the number is not zero, while in case 2 both if
statements checks the pointer contains a not null address that points to some memory area.
Logical operators &&
and ||
evaluates it’s second operand only if it is necessary, for example:
1 2 3 4 5 |
// 3. Case for operator && if (value && *value) {/*...*/} // 4. Case for operator || if (!value || value.is_null()) {/*...*/} |
The example above demonstrates two cases: in case 3 *value
is evaluated only if value
yields to true
, while in case 4 value.is_null()
is evaluated only if !value
yields to false
. In other words operands of &&
only evaluates until the first false
while operands of ||
only evaluates until the first true
.
Please note, that there is no “else-if” statement in C++. Therefore, when it is necessary to specify multiple if
branches (alternatives), then two if
statements must be combined as shown below:
1 2 3 |
if (condition) statement-true-1 else if (condition) statement-true-2 else statement-false |
To make the code looks more compact and to avoid the usage of variables before assigning initial values to them it is recommended to introduce variables into the smallest scope possible, in particular, it is possible to declare variable in condition of if
statement, for example:
1 2 |
if (const auto err = error()) handle_error(err); |
In the example above the variable “err” is declared and the value assigned to it at runtime is evaluated and yielded to bool
to check the if
condition. The scope of variable (“err” in the example) that is initialized inside an if
statement extends to the end of statements which are follows the conditions, i.e. to the end of statement-true
and statement-false
(if else
part is specified).
Since C++17 it is possible to specify the expression for condition checking just after the variable declaration, for example:
1 2 3 4 5 |
#include <string_view> const std::string_view ide{"Embarcadero RAD Studio is a powerful IDE!"}; if (const auto pos = ide.find("Embarcadero"); pos != std::string_view::npos) std::cout << ide << "\n"; |
Since C++17 it is possible to perform compile-time checks by using if constexpr
statement which looks similar to runtime if
except the presence of the keyword constexpr
immediately after the if
. The condition of if constexpr
statement must be a compile-time expression, for example:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <type_traits> // std::is_floating_point_v, ... #include <utility> // std::forward template<typename T> std::string to_string(T&& value) { if constexpr (std::is_floating_point_v<T>) return float_to_string(value); // branch 1 else if constexpr (std::is_integral_v<T>) return integral_to_string(value); // branch 2 else return generic_to_string(std::forward<T>(value)); // branch 3 } |
In the example above if the function to_string()
is called with an integral argument, the only branch 2 will be included to the result of compilation which reduces the size of the resulting binary. By replacing if constexpr
with if
in this example this effect is also possible without constexpr
because compilers like the latest generation of Embarcadero RAD Studio C++ compilers can optimize code that is not used (so called “unreachable code”). But with constexpr
this behavior is guaranteed by the standard.
When using logical operators &&
and ||
in if constexpr
statements please keep in mind that compile-time condition is always instantiated and must be valid as a whole. Therefore all the functions and operators specified in the condition of if constexpr
must be available at compile-time. Consider:
1 2 3 4 5 6 7 8 9 10 11 |
#include <type_traits> // std::is_floating_point_v #include <utility> // std::forward template<typename T> std::string to_string(T&& value) { if constexpr (std::is_floating_point_v<T> && value == 3.1415) return pi_to_string(value); // extra special conversion of PI to string else return generic_to_string(std::forward<T>(value)); } |
The example above won’t compile in case when T
represents std::string
for instance, because the comparison operation (value == 3.1415)
cannot be performed against the value of type std::string
(neither runtime nor compile-time). To make this example to compile it should be refactored as follows:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <type_traits> // std::is_floating_point_v #include <utility> // std::forward template<typename T> std::string to_string(T&& value) { if constexpr (std::is_floating_point_v<T>) { if constexpr (value == 3.1415) return pi_to_string(value); // extra special conversion of PI to string } else return generic_to_string(std::forward<T>(value)); } |
As in case of runtime if
it is possible to declare variables in a condition of if constexpr
as well as specify an expression to be evaluated to perform the condition checking just after such a declaration. Both the declaration and the condition checking expression must be compile-time expressions, for example:
1 2 |
if constexpr (constexpr auto a = avg({3, 5, 7}); a < 8) std::cout << "The average value is less than 8\n"; |
Please note that variable a
is declared with using of constexpr
keyword.