Why this matters
Many checks need a value before the test can run. You look up a key in a list, convert text to a number, or ask a function for a status. The test then looks at that value. Before C++17, the value had to be declared on its own line before the if. It stayed in scope after the if ended, even though only the if needed it. A later line could read it by accident, and a second variable with the same name could be declared without any warning from the reader.
C++17 added a form of if with a short setup step built in. The setup step runs first, its variable is visible to the test and to the branches, and the variable goes away when the if statement ends. This lesson covers that form. Other lessons in this module cover else if ladders, switch, and checking for null before a condition, so this one only looks at where the variable is declared.
The idea in plain words
A scope is the part of a program where a name can be used. A variable declared inside a pair of braces { } has a scope that ends at the closing brace. Code after the braces cannot use it.
An initializer here means a statement that runs before the test. The general form is:
if (setup; test) {
body
} else {
other body
}The setup can be a variable declaration, such as int n = 12, or an ordinary expression, such as ++calls. The test comes after the semicolon. The variables declared in the setup stay in scope through the test, the if body and every else part. When the whole if statement finishes, they are gone.
A concrete picture helps. A library clerk takes a book off the shelf, checks whether it is overdue, and then puts the book back on the trolley. Nobody else needs the book after the check. The C++ form lets you write the same thing: the book is taken, the check is made, and the book is gone.
The old way and the new way
Here is a small program with two versions of the same check. The first wraps the variable in its own block, so it cannot leak out. The second uses the new form.
Both functions print the same line:
many: 3
many: 3The first version works, but it needs an extra pair of braces that only exists to limit the scope. The second version puts the declaration and the test on one line. Read the header of the second version from left to right. The part before the semicolon is the setup. The part after the semicolon is the test. The body comes after the test.
The variable n is declared with size_t, which is the unsigned integer type that the standard library uses for sizes and counts. You will see it in the list and search examples below.
Worked example 1: a lookup with find
A map is a container that stores pairs of a key and a value, and looks up a value from its key. The find function of a map returns an iterator, which is a position inside the container. If the key is missing, find returns the position end(), which means "one past the last item". The usual check is to compare the result with end().
Before C++17, you had to write the iterator on its own line, then the check. The iterator then stayed in scope after the check, even though it was only needed for that check. The init form keeps everything together:
The output is:
found apple with 4
kiwi missing, end() reached: 1
calls is 1
total 30Trace the first if. The setup asks the map for "apple", and the result is stored in it. The test checks that it is not end(). The key exists, so the first branch runs and prints the key and the count. The else is skipped.
The second if declares a new it. This is allowed because the first it is gone. Its setup asks for "kiwi", which is not in the map, so it equals end(). The else branch runs. It can still read it, because the else is part of the same if statement. The output shows 1, which is how true is printed by default for a bool value.
The second example shows that the setup can be an expression with no new name at all. The ++calls runs once, before the test, and calls is 1 when the test runs. The third example shows the setup can declare two variables separated by a comma. Both total and count are visible in the test and the body. Both go away after the if.
The test is the part after the semicolon, and it must be a condition that is either true or false. In the first example the test is it != stock.end(). The next section shows what goes wrong when the test is the iterator itself.
Worked example 2: converting text to a number
The from_chars function in the <charconv> header turns text into a number. It takes a start pointer, an end pointer, and an output variable. It returns a small result with two parts. The first part, ptr, is where the reading stopped. The second part, ec, is an error code. When the conversion works, ec equals errc{}, which is the "no error" value.
A structured binding is a way to split a result with several parts into named variables in one line. Writing auto [end, ec] = ... gives you two names, end and ec, for the two parts of the result.
The next program reads three strings and prints what each one turned into. The name end and the error code ec exist only inside the if:
The output is:
42 parsed as 42, stopped at offset 2
7x parsed as 7, stopped at offset 1
abc failedThe value variable is declared on its own line before the if. The setup declares end and ec together as one structured binding, and that declaration cannot also create an int, so value has to come first.
The text "7x" is a useful surprise. from_chars read the 7, stopped at the x, and reported no error. The ec value was errc{}, so the test passed. The offset 1 shows where reading stopped. If your program must reject "7x", you have to check that the end pointer reached the end of the text. That check is shown in the practice answers.
Worked example 3: a chain of else if with a setup in each step
The setup can be used in every step of an else if chain. The next program looks up a product's price in two lists, a sale list and a regular list. Each lookup is written in the if where it is used:
The output is:
tea: sale 3
coffee: regular 7
milk: unknown item milk
i=0 step=0
i=2 step=4
i=4 step=8
loops 3The else if is a second if placed inside the else part of the first one. Because of that, the first setup variable is still in scope inside the else if. The else if in this example declares its own r.
The loop at the end of the program does the same thing with a number. The setup step = i * 2 is computed fresh for each value of i. The test checks whether step is a multiple of 4. The count loops is outside the if, because the loop needs it after the loop ends. This split is the pattern to aim for: the value that only the test needs goes in the setup, and the value the rest of the code needs is declared outside.
Common mistakes
Using the setup value as the test
A map's find returns an iterator, not a bool. Writing the iterator as the whole test is a common slip, because a pointer can be used that way and it is easy to think an iterator can too.
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
map<string, int> stock = {{"apple", 4}};
if (auto it = stock.find("apple")) {
cout << "found\n";
}
return 0;
}The compiler reports this error with g++ 15.2.0 and -std=c++20:
bad1.cpp: In function ‘int main()’:
bad1.cpp:8:38: error: could not convert ‘it’ from ‘std::_Rb_tree_iterator<std::pair<const std::__cxx11::basic_string<char>, int> >’ to ‘bool’
8 | if (auto it = stock.find("apple")) {
| ^
| |
| std::_Rb_tree_iterator<std::pair<const std::__cxx11::basic_string<char>, int> >The fix is to write the comparison that the test means: if (auto it = stock.find("apple"); it != stock.end()).
Using the name after the if ends
The name lives only until the end of the if statement. A line after it cannot use the name:
#include <iostream>
using namespace std;
int main() {
if (int n = 12; n > 10) {
cout << "big\n";
}
cout << "n is " << n << "\n";
return 0;
}The error is:
bad2.cpp: In function ‘int main()’:
bad2.cpp:8:24: error: ‘n’ was not declared in this scope
8 | cout << "n is " << n << "\n";
| ^This error is useful. It tells you the variable is gone, so you can decide whether to move the declaration out of the if. If the later code needs the value, declare the variable before the if and assign it there. If only the test needed it, leave the code as it is.
Declaring the same name again in the body
The body of the if is a block, but it shares a rule with the setup. A name declared in the setup cannot be declared again in the outer part of the body:
#include <iostream>
using namespace std;
int main() {
if (int n = 12; n > 10) {
int n = 3;
cout << "n is " << n << "\n";
}
return 0;
}The error is:
bad3.cpp: In function ‘int main()’:
bad3.cpp:6:13: error: redeclaration of ‘int n’
6 | int n = 3;
| ^
bad3.cpp:5:13: note: ‘int n’ previously declared here
5 | if (int n = 12; n > 10) {
| ^The fix is to pick a different name, such as small, so the reader can tell the two values apart.
Forgetting the semicolon in the setup
The setup and the test are separated by a semicolon, not a comma or a space. Leaving the semicolon out gives an error that can point to the wrong place:
#include <iostream>
using namespace std;
int main() {
if (int n = 12 n > 10) {
cout << "big\n";
}
return 0;
}The error is, with -Wall:
bad5.cpp: In function ‘int main()’:
bad5.cpp:5:19: error: expected ‘)’ before ‘n’
5 | if (int n = 12 n > 10) {
| ~ ^~
| )
bad5.cpp:5:13: warning: unused variable ‘n’ [-Wunused-variable]
5 | if (int n = 12 n > 10) {
| ^The error points at the second n, not at the missing semicolon. When you see expected ')' in an if line, check the separator first.
Checking only the error code
The "7x" case in worked example 2 shows a wrong result that the compiler cannot catch. from_chars reported no error, because it read a valid number and stopped at the x. A check of ec == errc{} alone accepts the text. Check the end pointer too when the whole text must be a number. The practice answer for exercise 4 does this.
Compiling with an old standard
The init form is a C++17 feature. With -std=c++14, g++ 15.2.0 accepts it and prints a warning when -Wall is on:
bad4.cpp: In function ‘int main()’:
bad4.cpp:5:9: warning: init-statement in selection statements only available with ‘-std=c++17’ or ‘-std=gnu++17’ [-Wc++17-extensions]
5 | if (int n = 12; n > 10) {
| ^~~A warning does not stop the build, so the program still runs. Add -pedantic-errors to turn this into an error, or compile with -std=c++17 or later. The course compiles with -std=c++20.
Practice
Each exercise has its answer below it. Try each one before you read the answer.
Exercise 1: move a value into the setup
The code below declares count in its own block. Rewrite it so that count is declared in the setup of the if, with no extra block. The output must not change.
{
size_t count = nums.size();
if (count > 2) {
cout << "count " << count << "\n";
}
}Show the answer
if (size_t count = nums.size(); count > 2) {
cout << "count " << count << "\n";
}The setup declares count, the test checks it, and the body prints it. The name goes away at the closing brace of the if, just as it did at the closing brace of the old block.
Exercise 2: find a value in a vector
Write a function exercise2 that takes a vector<int>. If the value 7 is in the vector, print its index. Otherwise print 7 is absent. Use an if with a setup that calls find from <algorithm>. The distance function from <iterator> gives the index.
Show the answer
#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
using namespace std;
void exercise2(const vector<int>& v) {
if (auto it = find(v.begin(), v.end(), 7); it != v.end()) {
cout << "7 is at index " << distance(v.begin(), it) << "\n";
} else {
cout << "7 is absent\n";
}
}
int main() {
exercise2({3, 7, 9});
exercise2({3, 5});
return 0;
}With the two calls in main, the output is 7 is at index 1 and then 7 is absent. The iterator it is only needed for the test and for the index in the body, so it belongs in the setup.
Exercise 3: predict the output
Read this code and write what it prints before you run it:
int x = 1;
if (int x = 2; x > 0) {
cout << "then x=" << x << "\n";
} else {
cout << "else x=" << x << "\n";
}
cout << "outer x=" << x << "\n";Show the answer
then x=2
outer x=1The setup creates a new x with the value 2. That new x hides the outer x while the if runs, so the body prints 2. The else is skipped. After the if ends, the new x is gone, and the outer x, which is 1, is used again. Names that hide other names are called shadowing. Shadowing is legal, but it is a common source of confusion, so avoid it in your own code.
Exercise 4: reject text with extra characters
Write a function exercise4 that takes a string. It returns "ok" followed by the number when the whole string is a valid integer, and "bad input" otherwise. Use from_chars in the setup, and check both the error code and the end pointer. Test it with "12", "12px" and "x".
Show the answer
#include <charconv>
#include <iostream>
#include <string>
using namespace std;
string exercise4(const string& text) {
int value = 0;
if (auto [end, ec] = from_chars(text.data(), text.data() + text.size(), value);
ec == errc{} && end == text.data() + text.size()) {
return "ok " + to_string(value);
}
return "bad input";
}
int main() {
for (string s : {"12", "12px", "x"}) {
cout << s << " -> " << exercise4(s) << "\n";
}
return 0;
}The output is:
12 -> ok 12
12px -> bad input
x -> bad inputThe test has two parts joined by &&. The error code must show success, and the end pointer must be at the end of the text. The line break inside the if header is only for layout. The compiler reads the whole header as one statement.
Exercise 5: fix the compile error
The code below does not compile. Fix the test so that it checks whether the key was found, and keep the setup as it is.
map<string, int> m = {{"apple", 4}};
if (auto it = m.find("apple")) {
cout << "found\n";
}Show the answer
map<string, int> m = {{"apple", 4}};
if (auto it = m.find("apple"); it != m.end()) {
cout << "found\n";
}The test is now a comparison that gives a bool. The program prints found. The error message for the original code is shown in the mistakes section.
Summary
if (setup; test)declares a variable that exists only through theifstatement, itselseparts and nothing else.- The setup can be a declaration, several declarations separated by commas, or an expression with no new name.
- A name declared in the setup cannot be used after the
ifends, and cannot be declared again at the top of the body. - An iterator or other object must be compared with a value, such as
it != m.end(), before it can be used as the test. - Structured bindings such as
auto [end, ec] = ...work in the setup and give named parts of a result. - The init form needs C++17 or later. Under C++14, g++ accepts it with a warning, and
-pedantic-errorsturns that warning into an error.