Embercode
✕
C++ OperatorsLesson 13 of 14

40 min read

Guards with short-circuit

Why this matters

An array has a fixed number of slots, numbered from 0. A read such as scores[i] asks for the value in slot i. If i is past the last slot, the read reaches memory that does not belong to the array. The program may crash at once, or it may read a random number and keep going. Neither result tells you where the bug is, and the random number can make the rest of the program wrong in ways that are hard to trace.

A guard is a check placed in front of the read. It asks whether the index is valid, and the read happens only when the answer is yes. In C++ the two operators that make a guard cheap to write are &&, which means "and", and ||, which means "or". They have one special property that makes the guard safe: they stop as soon as the answer is known. This lesson covers that property and how to order a check so that the read is always protected. The order of operators in a long expression is covered in the precedence lesson of this module.

The idea: stop as soon as the answer is known

Consider a && b. The answer is true only when both sides are true. If a is false, the answer is already false, so C++ does not evaluate b at all. The right side is skipped.

Consider a || b. The answer is true when either side is true. If a is true, the answer is already true, so C++ skips b.

This skipping is called short-circuit evaluation. Short-circuit means "stop early". It changes what your program does, not only how fast it runs. A skipped side does not run any code, read any array, or print anything. A short example makes this clear. Each function below prints its name when it runs, so you can see which ones are called:

C++ · editable

The output is:

And with a false first side:
  checking first
Or with a true first side:
  checking first
  at least one true

In the first if, the first side is false, so the second side never runs. checking second is never printed. In the second if, the first side is true, so the answer is already known, and again the second side never runs. A guard depends on this. The check on the left decides whether the read on the right is allowed to happen.

Worked example 1: a read that is safe when the index is valid

Here is an array of three scores, and a question about one slot. Is the score in slot i zero? The number of slots is n, which is 3. A valid index is 0, 1 or 2, so the check is i < n.

C++ · editable

The output is:

scores[1] is zero
guard stopped the read

When i is 1, the check i < n is true, so C++ goes on and reads scores[1], which is 0. The if body runs. When i is 3, the check i < n is false, the && already has its answer, and scores[3] is never read. The else branch runs instead.

Read the condition in words: "the index is in range, and the slot holds zero". The two parts are in that order on purpose. The first part protects the second.

The static_cast<int> turns the size from size_t, the unsigned type that size() returns, into an int. The reason is explained in the unsigned example further down.

Worked example 2: a check on both ends

An index can be too small as well as too large. Slot -1 does not exist, and C++ does not treat a negative index as "count from the end", as some other languages do. A full range check tests both ends with &&. The index must be at least 0, and it must be less than n.

C++ · editable
readAt(scores, 2) = 7
readAt(scores, -1) = -1
readAt(scores, 3) = -1

The & in const vector<int>& values means the function reads the caller's vector without copying it. The function returns -1 as a marker that the slot does not exist. A caller that receives -1 knows the index was not valid. The index >= 0 part stops the read for negative numbers, and index < n stops the read for numbers at or past the end. Both parts sit before the read, so the read itself is never reached with a bad index.

Worked example 3: || as a guard for skipping

|| is used the other way around. You want to skip a slot when it is out of range or when it holds zero. If either is true, the slot is skipped. The check with || reads: "the index is past the end, or the slot is zero".

C++ · editable
use 4
skip 1
use 7
skip 3
skip 4

continue skips the rest of the loop body and moves to the next slot. Trace slot 3. The first part, k >= n, is true because 3 is not less than 3. The || already has its answer, so scores[3] == 0 is never evaluated. Slot 4 works the same way. Slot 1 is in range, so the first part is false, and the second part reads scores[1], which is 0, and the slot is skipped.

The rule is the same as for &&. The side that can fail must come after the side that protects it.

Order matters: check first, read second

Now swap the two parts of the earlier check and see what happens.

vector<int> scores = {4, 0, 7};
int n = static_cast<int>(scores.size());
int i = 3;
if (scores[i] == 0 && i < n) {
    cout << "zero" << endl;
}

The read scores[i] now comes first. With i equal to 3, it reads past the end. C++ does not check the index for you when you use []. On the g++ 15 build used for this lesson on Ubuntu, the vector read stops the program with this message:

/usr/include/c++/15/bits/stl_vector.h:1263: constexpr std::vector<_Tp, _Alloc>::reference std::vector<_Tp, _Alloc>::operator[](size_type) [with _Tp = int; _Alloc = std::allocator<int>; reference = int&; size_type = long unsigned int]: Assertion '__n < this->size()' failed.

The exit code is 134, which means the program was stopped by an abort signal. Your build may print a different message, or none. That is the danger. The standard says that reading outside an array is undefined behaviour, which means the language makes no promise about what happens. The same mistake with a plain C array, which has no size check at all, produced no message in this build:

C++ · editable
done, scores[3] read as 149567744

The number 149567744 is whatever bytes happened to sit after the array. Two runs of this same program printed 149567744 and then 421823488, so the value can change even without a rebuild. The program looks finished and correct, but the value it used was never part of the array.

A safe read that throws instead

When you want a clear failure rather than a crash, .at() on a vector checks the index and throws an error if it is out of range. It does not replace a guard. It is a second line of defence that tells you where the problem was:

C++ · editable

On this build, the program ends with:

terminate called after throwing an instance of 'std::out_of_range'
  what():  vector::_M_range_check: __n (which is 3) >= this->size() (which is 3)

The message names the bad index and the size. .at() stops the program with a message that names the index, but it does not let the program recover from the bad index, so read it only as a way to make a bad index loud. The guard is still the right tool when the bad index is expected, such as a user typing a slot number.

Unsigned indexes need more care

size() returns a size_t, which is an unsigned type. An unsigned type can never be less than 0. This creates two traps.

The first trap is a comparison between int and size_t. If you write i < scores.size() with i as an int, the compiler warns, because a negative i would be compared as a very large unsigned number. Here is a program that does this:

#include <iostream>
#include <vector>
using namespace std;
 
int main() {
    vector<int> scores = {4, 0, 7};
    int i = -1;
    if (i >= 0 && i < scores.size()) {
        cout << scores[i] << endl;
    }
    cout << "done" << endl;
}

Compiled with -Wall, the compiler prints:

sign.cpp: In function ‘int main()’:
sign.cpp:8:21: warning: comparison of integer expressions of different signedness: ‘int’ and ‘std::vector<int>::size_type’ {aka ‘long unsigned int’} [-Wsign-compare]
    8 |     if (i >= 0 && i < scores.size()) {
      |                   ~~^~~~~~~~~~~~~~~

The program still prints done, because i >= 0 is false, so the guard stops the read before the warning matters. The fix in the first example is to convert size() once to an int, as the examples above do. Another fix is to use a size_t index throughout, but then the subtraction below must be handled with care.

The second trap is a check that cannot fail. With a size_t index, i - 1 >= 0 is always true, because i - 1 is never negative. When i is 0, the subtraction wraps around to a very large number:

C++ · editable

With -Wall -Wextra, the compiler warns that the first comparison is always true:

uns.cpp:8:15: warning: comparison of unsigned expression in ‘>= 0’ is always true [-Wtype-limits]
    8 |     if (i - 1 >= 0) {
      |         ~~~~~~^~~~

The program then prints:

always true for size_t
guard protected the read
i - 1 = 18446744073709551615

The first if is a check that never fails, which is the bug. The second if is safe, because i >= 1 is checked first. When i is 0, the first part is false, and scores[i - 1] is skipped. The guard must test i >= 1 before it subtracts, not test the result of the subtraction.

Common mistakes

Putting the read before the check. scores[i] == 0 && i < n reads the array first. The read happens before the check that should have protected it. Write the check first: i < n && scores[i] == 0.

Using <= where < is needed. A valid index is less than n, not less than or equal to it. Slot n does not exist. This program checks with <= and reads slot 3 of a three-slot array:

vector<int> scores = {4, 0, 7};
int n = static_cast<int>(scores.size());
int i = n;
if (i <= n && scores[i] == 0) {
    cout << "zero" << endl;
}

The program stops with the same assertion message as before, with exit code 134. The guard must use <. Whenever you write a bounds check, read it aloud as "the index is less than the size".

Using & where && is needed. The single & is a bitwise operator on numbers. On bool values it gives the same answer as &&, but it does not short-circuit. Both sides run. This program uses & by mistake, with the same bad index:

vector<int> scores = {4, 0, 7};
int n = static_cast<int>(scores.size());
int i = 3;
if (i < n & scores[i] == 0) {
    cout << "zero" << endl;
} else {
    cout << "no zero" << endl;
}

The compiler warns:

bitand.cpp:9:11: warning: suggest parentheses around comparison in operand of ‘&’ [-Wparentheses]
    9 |     if (i < n & scores[i] == 0) {
      |         ~~^~~

The program then stops with the same assertion message, because scores[i] runs even though i < n is false. Use && for a guard. Treat the -Wparentheses warning as a sign that the operator may be wrong.

Checking the index after the read in a loop. A loop that reads values[i] and then tests i < n on the next line has the same problem. The read must sit inside the guard, not after it.

Assuming || is the opposite guard. i >= n || scores[i] == 0 is correct only because the first part is checked first. Swapping the two parts reads slot i before the range test, which fails in exactly the same way as the wrong && order.

Practice

Exercise 1. Write a condition that is true when i is a valid index into values, a vector<int>. Then say which part must come first and why.

Show the answer

The condition is i >= 0 && i < static_cast<int>(values.size()). Both parts are needed. The first part catches negative indexes, and the second part catches indexes at or past the end. The read values[i] goes inside the if body, after the whole condition has been checked.

Exercise 2. The loop below should print every positive score in scores, but it has a bug that appears only when i reaches the end. Find it and fix it.

vector<int> scores = {4, 0, 7};
int n = static_cast<int>(scores.size());
for (int i = 0; i <= n; ++i) {
    if (scores[i] > 0 && i < n) {
        cout << scores[i] << endl;
    }
}

Show the answer

The check i < n comes after the read scores[i] > 0. When i is 3, the read happens first and goes past the end. The fix is to put the check first: if (i < n && scores[i] > 0). The loop bound i <= n is also wrong, since slot n does not exist. The loop should run while i < n, which makes the && check unnecessary. Written correctly, the loop is for (int i = 0; i < n; ++i) { if (scores[i] > 0) { cout << scores[i] << endl; } }.

Exercise 3. Predict the output of this program, then run it.

#include <iostream>
using namespace std;
 
bool step(int value) {
    cout << "step " << value << endl;
    return value > 0;
}
 
int main() {
    if (step(0) && step(1)) {
        cout << "both" << endl;
    }
    if (step(1) || step(2)) {
        cout << "either" << endl;
    }
}

Show the answer

The output is:

step 0
step 1
either

The first if calls step(0), which returns false, so step(1) is skipped. The second if calls step(1), which returns true, so step(2) is skipped. The word "both" is never printed, and the word "either" is printed once.

Exercise 4. A function should return the value in slot i of a vector<int>, or 0 if the slot does not exist. Write it with one guard and no crash for any int input.

Show the answer

int valueOrZero(const vector<int>& values, int i) {
    if (i >= 0 && i < static_cast<int>(values.size())) {
        return values[i];
    }
    return 0;
}

The guard tests both ends before the read. For i equal to -5, the first part is false and the read is skipped. For i equal to 100 in a three-slot vector, the second part is false and the read is skipped. The function returns 0 in both cases.

Exercise 5. A loop reads the pair of slots i and i + 1 in an array of n ints, and adds them. The last pair must not run past the end. Write the guard for the loop body.

Show the answer

The pair is valid when i + 1 < n. That single check protects both reads, because i + 1 < n implies i < n. The guard is if (i + 1 < n && values[i] + values[i + 1] > 10). Checking i < n alone would still let values[i + 1] read past the end when i is n - 1. The guard must test the largest index it will use.

Summary

  • && stops at the first false side, and || stops at the first true side. The skipped side does not run at all.
  • Put the bounds check first and the array read second, always inside the same condition.
  • A valid index is at least 0 and less than the size. Use <, not <=.
  • Use && for guards. The single & runs both sides and does not protect the read.
  • With an unsigned size_t, a check such as i - 1 >= 0 can never fail. Test i >= 1 before subtracting.
Practice this topic: coding problems for C++ Operators →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Compound assignment narrowing