A statement is a unit of execution, and appears only inside a function body [stmt.stmt]. Statements run in order, except where one transfers control.
Kinds of statement
| Kind | Forms | Section |
|---|---|---|
| Labelled | identifier:, case constant:, default: | [stmt.label] |
| Expression | expression; or just ; | [stmt.expr] |
| Compound | { statements } | [stmt.block] |
| Selection | if, switch | [stmt.select] |
| Iteration | while, do, for, range-based for | [stmt.iter] |
| Jump | break, continue, return, goto, co_return | [stmt.jump] |
| Declaration | A block-level declaration | [stmt.dcl] |
| Try block | try and handlers | [except] |
Since C++23 a label may stand at the end of a compound statement, with nothing after it.
if
selection-statement:
if [constexpr] ( [init-statement] condition ) statement
if [constexpr] ( [init-statement] condition ) statement else statement
if [!] consteval compound-statement
if [!] consteval compound-statement else statement
The condition converts to bool. An optional init-statement (C++17) declares
something with the scope of the whole if, including else.
if (auto it = map.find(key); it != map.end()) // C++17 init-statement
use(it->second);
else
report_missing(key);
if constexpr
if constexpr (C++17) chooses a branch at compile time. Its condition is a
constant expression. Within a template the other branch is a discarded
statement: it is not instantiated [stmt.if], and so it can contain code
that would not compile for that argument.
template<class T>
auto to_text(const T& v)
{
if constexpr (std::is_arithmetic_v<T>)
return std::to_string(v);
else
return std::string(v); // never instantiated for an arithmetic T
}
Outside a template, the discarded branch is still fully checked.
if consteval
if consteval (C++23) picks the first branch during constant evaluation and the
second otherwise. It needs braces, and if !consteval reverses the test
[stmt.if].
switch
switch ( [init-statement] condition ) statement
The condition must have an integer or enumeration type, or a class type with a single conversion to one [stmt.switch]. Integer promotions apply. Case labels are constant expressions converted to that type.
switch (auto n = next(); n) {
case 0:
start();
[[fallthrough]]; // intended: falls into case 1
case 1:
step();
break;
default:
stop();
}
- Without
break, control falls through into the next case. - A jump to a case label may not skip a variable’s initialisation, except for a scalar or similar type declared without an initialiser [stmt.dcl]. Put it in a block.
- No two case labels may have the same value, and there can be only one
default.
Loops
| Form | Syntax | Section |
|---|---|---|
while | while (condition) statement | [stmt.while] |
do | do statement while (expression); | [stmt.do] |
for | for (init; condition; expression) statement | [stmt.for] |
Range-based for | for ([init;] declaration : range) statement | [stmt.ranged] |
In for, the condition is optional, and missing means true. A variable
declared in the init is in scope in the condition, the increment and the
body. A loop without side effects may be assumed to end; see
Undefined behaviour.
Range-based for
Range-based for (C++11) iterates over a range. An init-statement is allowed
since C++20 [stmt.ranged]. It is defined as this rewrite:
{
init-statement
auto&& range = for-range-initializer;
auto it = begin-expr;
auto last = end-expr;
for (; it != last; ++it) {
for-range-declaration = *it;
statement
}
}
| Range type | begin-expr | end-expr |
|---|---|---|
Array of N elements | range | range + N |
Class with members begin and end | range.begin() | range.end() |
| Anything else | begin(range), by ADL only | end(range), by ADL only |
std::vector<int> values{1, 2, 3};
for (int v : values) { } // a copy of each element
for (int& v : values) { v *= 2; } // a reference
for (const auto& v : values) { } // avoids copies
for (auto&& [k, v] : map) { } // a structured binding
for (auto n = 0; int v : values) { ++n; } // init-statement (C++20)
Since C++17 the iterator and sentinel may have different types. Since C++23, temporaries created in the range initialiser are destroyed only at the end of the loop [class.temporary]. Before C++23 they were destroyed at the end of the initialiser, which made a call chain on a temporary dangle. A function that returns a reference to a by-value parameter is still undefined behaviour.
std::vector<std::string> make_names();
for (char c : make_names().front()) { } // fine since C++23, dangles before it
Jumps
| Statement | Effect | Section |
|---|---|---|
break; | Leaves the innermost loop or switch | [stmt.break] |
continue; | Starts the next iteration of the innermost loop | [stmt.cont] |
return [expr]; | Leaves the function | [stmt.return] |
return braced-init-list; | Copy-list-initialises the result (C++11) | [stmt.return] |
co_return [expr]; | Finishes a coroutine (C++20) | [stmt.return.coroutine] |
goto label; | Jumps to a label in the same function | [stmt.goto] |
Leaving a scope destroys its automatic objects in reverse order of construction.
Flowing off the end of a function that returns a value is undefined behaviour, except for main.
goto may jump backwards past a declaration, which destroys the object. Jumping
forwards past a non-trivial initialisation is ill-formed [stmt.dcl].
Declaration statements
A block can declare variables anywhere [stmt.dcl]. Each is destroyed on leaving the block, in
reverse order. A static local is constructed once, the first time control
reaches it. Since C++11 this is safe when threads race to do it.
Where a statement could be read either as an expression or a declaration, it is a declaration [stmt.ambig]. See the vexing parse.
try blocks
try {
risky();
} catch (const std::exception& e) {
log(e.what());
} catch (...) {
throw; // rethrows the current exception
}
The rules, including function try blocks and how handlers are matched, are on
the Exceptions page.