Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

3.3 C++ Exceptions and Operators

Modern C++ provides a structured way of handling errors and exceptional conditions through exceptions. Unlike error codes or return values, exceptions separate the normal flow of a program from its error-handling logic, making code cleaner and more maintainable.

Exceptions and compiler errors are very different concepts in C++. A compiler error occurs at compile time, before the program can run, and indicates that the code violates the rules of the language (for example, using an undeclared variable or mismatched types). The program will not produce an executable until the error is fixed.

An exception, on the other hand, occurs at run time while the program is executing. It signals that something unexpected has happened, such as trying to access an out-of-range array element or failing to open a file. Exceptions allow the program to respond to these conditions gracefully, whereas compiler errors must be corrected before the program can even start.

An exception is an event that occurs during program execution and disrupts the normal flow of instructions. When an exception is thrown, control is transferred to the nearest matching handler. If no handler is found, the program will terminate.

  • throw: Used to signal that an exception has occurred.
  • try: Defines a block of code in which exceptions might occur.
  • catch: Defines a block of code to handle a specific type of exception.
  • stack unwinding: When an exception is thrown, the runtime system destroys local objects (by calling their destructors) as it searches for a matching handler.

Example: Accessing an Out-of-Range Element

#include <iostream>
#include <vector>
#include <stdexcept> // for std::out_of_range
using namespace std;
int main() {
vector<int> data = {1, 2, 3}; // not yet covered!
try {
// Using .at() instead of [] gives bounds checking
int value = data.at(5);
cout << "Value: " << value << "\n";
}
catch (const std::out_of_range& e) {
cerr << "Caught exception: " << e.what() << "\n";
}
catch (...) {
cerr << "Caught an unknown exception" << "\n";
}
cout << "Program continues safely after handling the exception." << "\n";
return 0;
}

This will give the output:

Terminal window
Caught exception: vector::_M_range_check: __n (which is 5) >= this->size() (which is 3)
Program continues safely after handling the exception.
  • std::vector::at() performs bounds checking, unlike operator[]. If the index is invalid, it throws a std::out_of_range exception.
  • The try block attempts to access an element outside the valid range.
  • The catch (const std::out_of_range& e) block handles the specific exception and prints an error message via e.what().
  • A catch (...) block can act as a fallback for unexpected exceptions.
  • After handling, program execution continues normally without crashing.
  • Prefer throwing and catching exceptions by const reference (e.g., catch (const std::out_of_range& e)), not by value.
  • Use standard exception classes (std::out_of_range, std::invalid_argument, std::runtime_error, etc.) where possible.
  • Exceptions should signal exceptional conditions, not routine events (e.g., reaching the end of a loop is not an exception).
  • Use the noexcept keyword for functions that are guaranteed not to throw exceptions. This helps the compiler generate more efficient code and is critical for performance in embedded systems, in which case error codes or status objects can be used instead.

This virtual lab provides a range of exception scenarios, some of which are relatively advanced. You can come back to this virtual lab after exploring the topics in the next few chapters. On your first pass, review Scenarios 1 and 2.

When you return after the next chapter, work through Scenario 3: RAII unwinding, and watch ~Resource() fire during the unwind. That’s the foundation of std::unique_ptr and std::lock_guard. Scenario 7 noexcept is also interesting as the same throw with the same local Resource never gets a destructor call (noexcept is a contract, not a hint, and breaking it skips clean-up entirely). Compare Scenarios 3 and 4 as they have identical code in f(), but whether main() catches decides between graceful recovery and std::terminate.

C++ Exception Lab

Step through throws, catches, unwinding, and terminate — see what really happens to the stack.

Scenario

1 · Throw & catch

A throw inside a try block is caught by the matching catch — same function, no unwinding across frames.

Timeline

event 1 / 6

Running

Step 1 of 6

Call stacknewest on top
main()line 1· live
(no locals)
Source
1int main() {
2 try {
3 std::cout << "before throw\n";
4 throw std::runtime_error("oops");
5 std::cout << "unreachable\n";
6 } catch (const std::runtime_error& e) {
7 std::cout << "caught: " << e.what() << "\n";
8 }
9 return 0;
10}
Console / runtime trace
→ enter main()
Deep dive:When the throw expression evaluates, control transfers immediately to the matching catch — the line after the throw is never reached. Because the throw is in the same function as the try, no frames are popped. The catch parameter is bound to the exception object and the program continues normally after the catch block.

This reference section consolidates the fundamental syntax and logical structures required to build complex C++ expressions and manage program execution. We cover the foundational rules for identifier naming and character representation before transitioning into a detailed analysis of the C++ operator set (including arithmetic, logical, and hardware-centric bitwise operations). By understanding how these operators interact through precedence and associativity, and how control flow statements like loops and switches direct the logic of your code, you will gain the precision necessary to implement efficient and maintainable algorithms in resource-constrained environments.

  • can begin with a letter or _
  • can be followed by letters and digits
  • are case sensitive
  • cannot use any of the standard language keywords (new, class etc.)

Table 1 Character Constants Table

newline \nhorizontal tab \tvertical tab \v
backspace \bcarriage return \rform feed \f
bell \abackslash \\question mark \?
single quote \'double quote \"null character \0
  • Multiplicative operators: a*b a/b a%b (remainder after division, e.g., 10 modulo 4 = 2)
  • Additive operators: a+b a-b
  • Equality operators (Boolean return): a==b (is equal?) a!=b (is not equal?)
  • Relational operators (Boolean) a<b a<=b a>b a>=b
  • Logical operators a==b && a==c (AND) a==c || a<b (OR). Some compilers accept the and, or and not keywords.
  • Bitwise operators << (left shift) >> (right shift) & (bitwise AND) ^ (bitwise XOR) | (bitwise OR) ~ (bit complement)
  • Increment and decrement a++ a-- ++a --a
  • Unary operators -a (negative of a) +i (i) ! (negative logical values) !(a==b) same as (a!=b)

The “Sizeof” operator sizeof(type) (number of bytes of type is returned) sizeof expression (number of bytes of expression returned). Note: The sizeof operator returns a value of type size_t. So for example:

int p[5] = {10, 20, 30, 40, 50};
cout << "Size of int is " << sizeof(int) << " bytes" << "\n";
cout << "Size of the array p is " << sizeof p << " bytes" << "\n";

Will result in:

Terminal window
Size of int is 4 bytes
Size of the array p is 20 bytes

Conditional operator ?. It has the form conditionalExpression ? trueExpression1 : falseExpression2, for example:

// Display the shorter of s and t
cout << (s.length() < t.length() ? s : t);

Note that the expressions must have the same type.

Note precedence: a + b * c is the same as a + (b * c) — so be careful and always use ()!

Remember to be careful when comparing values to use the = operator. If you write if (x=0) then this is an assignment, not an equality test, and the effect will be to assign 0 to x. How does it evaluate the expression to be true or false? If the value assigned is zero, it evaluates it as false; if it is anything else, it evaluates it as true.

An assignment takes the form: Variable = Expression. There are some shorthand versions, for example ++x or x++ is the same as x=x+1;, or x*=2; is the same as x = x * 2;

Note that:

int i = 0;
while ( ++i < 10) {
cout << i << "\n"; // outputs 1 to 9
}

Whereas:

int i = 0;
while ( i++ < 10) {
cout << i << "\n"; // outputs 1 to 10
}

In the second case the increment takes place after the “less than” comparison. However, be very careful with the for loop case:

for(int i=0; i<10; i++) {
cout << i << "\n"; // outputs 0 to 9
}

Whereas:

for(int i=0; i<10; ++i) {
cout << i << "\n"; // outputs 0 to 9
}

These are both equivalent as increment occurs after the statement in the loop has been executed.

In embedded programming, bitwise operations are fundamental to working directly with the individual bits of an integer. Unlike arithmetic operations that manipulate numbers as a whole, bitwise operations perform logical and shift operations on a bit-by-bit basis. This makes them incredibly useful for tasks that require fine-grained control over data representation, such as low-level programming, embedded systems, and edge computing.

Bitwise operations are essential in edge programming because they provide a direct way to interact with binary registers. In hardware, registers are small, fast memory locations that hold data or control instructions. Many hardware components, like sensors and microcontrollers, have registers where each bit has a specific meaning. For example, a single bit might represent the state of a switch (on/off), a flag (error/no error), or a mode (high power/low power).

By using bitwise operations, programmers can:

  • Set and clear specific bits to configure hardware settings.
  • Check the status of individual flags to read sensor data or device states.
  • Efficiently pack and unpack data to save memory and transmission bandwidth, which is critical in resource-constrained environments.

In modern C++ (C++17 and later), the std::byte type (from the <cstddef> header) is the preferred way to represent raw memory. Unlike unsigned char or uint8_t, std::byte is not a numeric type; it is strictly a collection of bits. This prevents accidental arithmetic errors while still allowing all bitwise operations.

#include <cstddef>
std::byte b{0b00011001};
b <<= 1; // Bitwise operations are allowed
// int i = b + 1; // Compile-time error! No arithmetic on bytes.

For instance, instead of storing a Boolean for each of eight different states, you can store all eight states in a single 8-bit integer using bitwise operations. This is a common practice when communicating with devices over a protocol like I2C or SPI, where you’re often reading and writing to registers.

Here’s an overview of the most common bitwise operators, using your code example for illustration. The example uses uint8_t, which is an unsigned 8-bit integer, a common data type in embedded systems.

Bitwise AND (&): This operator compares two numbers bit by bit. The resulting bit is 1 only if both corresponding bits are 1. It’s often used to mask a value, isolating or clearing specific bits.

Example: a & b where a = 25 (00011001) and b = 5 (00000101).
00011001 (25)
& 00000101 (5)
----------
00000001 (1)

Bitwise OR(|): This operator compares two numbers bit by bit. The resulting bit is 1 if at least one of the corresponding bits is 1. It’s used to set specific bits.

Example: a | b where a = 25 (00011001) and b = 5 (00000101).
00011001 (25)
| 00000101 (5)
----------
00011101 (29)

Bitwise NOT (~): This is a unary operator that inverts all the bits of a number. Each 1 becomes a 0, and each 0 becomes a 1.

Example: ~a where a = 25 (00011001). The result is all bits flipped. Because a is a uint8_t, the result is 11100110 (230). The sign bit isn't an issue since it's an unsigned integer.

Bitwise XOR (^): This operator compares two numbers bit by bit. The resulting bit is 1 if the corresponding bits are different. It’s useful for toggling bits or encryption.

Example: a ^ b where a = 25 (00011001) and b = 5 (00000101).
00011001 (25)
^ 00000101 (5)
----------
00011100 (28)

Shift operators move the bits of a number to the left or right.

Left Shift (<<): Shifts all bits to the left by a specified number of positions. Empty positions on the right are filled with zeros. A left shift by n positions is equivalent to multiplying the number by 2n.

Example: a << 1 where a = 25 (00011001).
Shifting left by 1 results in 00110010, which is 50.

Right Shift (>>): Shifts all bits to the right by a specified number of positions. For unsigned integers, empty positions on the left are filled with zeros (logical shift). A right shift by n positions is equivalent to dividing by 2n and discarding any remainder.

Example: b >> 1 where b = 5 (00000101).
Shifting right by 1 results in 00000010, which is 2.

Shift Overflows: Be cautious when shifting. In the example below, 1 << 8 results in a value of 0 when assigned to an 8-bit integer. This is because the bit is shifted beyond the 8th bit, and is lost when the value is stored in an 8-bit container. This is a common source of bugs in low-level code, especially on microcontrollers with limited data sizes.

Here is a full code example that you will find as a useful reference:

/* Bits test by Derek Molloy */
#include<iostream>
#include<stdint.h>
#include<bitset>
#include<sstream>
#include<iomanip>
using namespace std;
string display(uint8_t a) {
stringstream ss;
ss << setw(3) << <int>(a) << "(" << bitset<8>(a) << ")";
return ss.str();
}
int main() {
uint8_t a = 25, b = 5;
cout << "A is " << display(a) << " and B is " << display(b) << "\n";
cout << "A & B (AND) is " << display(a & b) << "\n";
cout << "A | B (OR) is " << display(a | b) << "\n";
cout << " ~A (NOT) is " << display(~a) << "\n";
cout << "A ^ B (XOR) is " << display(a ^ b) << "\n";
cout << "A << 1 (LSL) is " << display(a << 1) << "\n";
cout << "B >> 1 (LSR) is " << display(b >> 1) << "\n";
cout << "1 << 8 (LSL) is " << display(1 << 8) <<"\n"; // ignore warning!
return 0;
}

This gives the following output:

A is 25(00011001) and B is 5(00000101)
A & B (AND) is 1(00000001)
A | B (OR) is 29(00011101)
~A (NOT) is 230(11100110)
A ^ B (XOR) is 28(00011100)
A << 1 (LSL) is 50(00110010)
B >> 1 (LSR) is 2(00000010)
1 << 8 (LSL) is 0(00000000)

Lab: Bitwise Operations on 8-bit unsigned int (uint8_t)

Click bits on A and B, choose an operation, and the result updates live.

Bit #
7
6
5
4
3
2
1
0
A
B
A = 25 0x19B = 5 0x05
A & BBitwise AND
A
0
0
0
1
1
0
0
1
B
0
0
0
0
0
1
0
1
A & B
0
0
0
0
0
0
0
1
Try this:Click Textbook values to load A=25, B=5 and step through each operation to match the chapter's output table. Then try A & B with B set to 0000 1111 to isolate the low nibble of A — a classic masking idiom.

if statement:

if (expression) if (x == y)
{statement} { x = x + 5; }

if else statement:

if (expression) if (x>5)
{if true statement} { x++; }
else else
{if false statement} { x = x + 5; }

while statement

while (expression) while(x<3)
{statement} { x++; }

do while statement:

do do
{statement} { x++; }
while (expression) while (x<3)

Figure 1 Listen to Derek when he explains the difference between while() and do {} while (adapted from a Wile E. Coyote and the Road Runner meme!).

for statement:

for (init; comparison; modifier) for (i=0; i<10; i++)
{statement} { someFunct(); }

Note: For loop counters that index into arrays, size_t (an unsigned type) is often preferred over int to avoid signed/unsigned comparison warnings and to ensure compatibility with large arrays.

Introduced in C++11, the range-based for loop (often called a “for-each” loop) provides a simpler way to iterate over all elements in an array or container.

int scores[] = {88, 92, 75, 96};
for (int score : scores) {
std::cout << score << "\n";
}

Combined with the auto keyword (to let the compiler deduce the type) and const reference (to avoid copying large objects), this is the preferred way to iterate in modern C++:

for (const auto &val : someContainer) {
// efficient and safe
}

C++ Loop Lab

Pick a loop type and watch the program counter walk it — see exactly where each loop checks, steps, and jumps back.

int arr[] = {10, 20, 30, 40, 50};
int n = 5;
 
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
Speed
initconditionincrementbody
Trace
Press Start or Step to begin…
Output
(nothing printed yet)

All four loops produce the same output for this array, but their structure differs. for packs init, condition, and increment into one header. while separates them — init before the loop, increment inside the body. do-while tests the condition after the body, so the body always runs at least once — even if the condition starts false. range-based for (C++11) hides the counter, condition, and increment entirely — the compiler generates them so you can just say "for each element, do this".

switch statement:

int x = 5;
switch (expression) switch(x)
{ {
case (constant): statement case 1: cout << "test1";
case (constant): statement break;
default : statement case 5: cout << "test5";
} break;
default: cout << "none";
}

Within all of these control statements we can also control the flow using break and continue keywords. break quits out of the loop/switch, without completing the remaining statements in the loop/switch. continue on the other hand continues directly to the next iteration of loop without executing the remaining statements in the loop. The use of these keywords is frowned upon to some extent in programming as loops must be constructed so that it is safe to skip the remaining statements. For example if the first line of a loop opened a database connection, and the last line closed that connection, a break in the middle of the loop would cause a “lost” open database connection. Below is a short example to show the use of break and continue.

#include<iostream>
using namespace std;
int main() {
for(int i=0; i<10; ++i) {
if (i==5) { continue; }
if (i==7) { break; }
cout <<"Loop number:" << i << "\n";
}
}

The source code for this is in BreakContinue.cpp. The output of this application is:

Terminal window
Loop Number:0
Loop Number:1
Loop Number:2
Loop Number:3
Loop Number:4
Loop Number:6

When i==5 the cout will be skipped and the loop will continue to the next iteration, but when i==7 the loop will exit. The most likely place to see a break statement is in some form of infinite loop, such as while(true){}, so that there is some exit point, as there is no condition to evaluate as false.

Here is a simulation of the Control Flow code above where you can step through switch statements when the break; is omitted and retained, and a for loop with the use of break and continue. The output is quite verbose.

C++ Control Flow Lab

See how switch, break, and continue redirect program flow.

Input value
matches case 5
int x = 5;
switch (x) {
case 1: cout << "test1";
case 5: cout << "test5";
default: cout << "none";
}
Speed
case matchcase bodybreak
Trace
Press Start or Step to begin…
Output
(nothing printed yet)

Without break, execution falls through from one matched case into every case below it — including default. Try x = 1 to see it cascade through every branch. This is occasionally useful, but almost always a bug.

I suppose in this discussion I should also begrudgingly mention goto. In 99% of the times you consider the use of goto there is an alternative solution. The goto keyword was added to the C++ language because it was present in C (and was of appeal to Basic programmers, where it was necessary). The use of goto makes programs difficult to follow and difficult to debug, but can be used correctly in limited circumstances. Remember that example above of the break quitting the loop before the database connection is closed; Well one possible solution to that problem is to use goto. For example we could use:

int main() {
recordsExist = true;
while ( recordsExist ) {
// open database connection
// retrieve record
if ( record invalid ) {
recordsExist = false;
goto endloop; // skip remaining statements in loop
}
// more statements that operate on a valid record
endloop:
// properly close database connection
}
}

Now, I am certain that there is another solution to this issue (such as a single else), but this is just an example of when you might use a goto. Distant goto calls are not acceptable as it would be impossible to follow the code, locating the destination in a large section of code.

This operator precedence table is placed at the end of the chapter because it is not important to memorise it. In fact, relying on obscure precedence rules can make your code less clear, as other developers (or your future self) may not have the table memorised.

The most important takeaway is that parentheses () have the highest precedence (Level 1). You should use them to explicitly state your intent and make your code easier to read. For example, instead of writing:

x = 3 + 4 * 5; // relies on * being higher than +

Writing the following is much clearer:

x = 3 + (4 * 5); // explicit intent

Best Practices:

  • When in doubt, use parentheses. It costs nothing in terms of performance and significantly improves readability.
  • Avoid complex “one-liners” that rely on multiple precedence levels. Break them into smaller, named variables if the logic becomes hard to follow.
  • Pay special attention to bitwise operators. Bitwise operators (&, |, ^) often have lower precedence than comparison operators (==, !=). Always wrap bitwise expressions in parentheses when comparing results, e.g., if ((status & mask) == expected).

Table 2. C++ Operator Precedence Table

PrecedenceOperatorDescriptionExampleAssociativity
1()Grouping operator(3+a)*2;left to right
1[]Array Accessa[0]=10;left to right
1->Pointer Member Accessp->balance=0;left to right
1.Object Member Accessaccount.balance=0;left to right
1::Scoping operator::value = 1;left to right
1++Post Increment operatorx++;left to right
1--Post Decrement operatorx--;left to right
2!Logical Negationif(!running)right to left
2~Bitwise Complementx=~x;right to left
2++Pre Increment operator++x;right to left
2--Pre Decrement operator--x;right to left
2-Unary minusa=-b;right to left
2+Unary plusa=+b;right to left
2*Dereferencea=*ptr;right to left
2&Address ofptr=&a[0];right to left
2(type)Cast to a typex=static_cast<int>(23.6);right to left
2sizeofSize in bytesx=sizeof(X);right to left
3->*Member Pointer Selectorptr->*p=1;left to right
3.*Member Pointer Selectoraccount.*p=1;left to right
4*Multiplicationa=b*c;left to right
4/Divisiona=b/c;left to right
4%Modulusremainder=b%c;left to right
5+Additiona=b+c;left to right
5-Subtractiona=b-c;left to right
6<<Bitwise Left Shifta=b<<1;left to right
6>>Bitwise Right Shifta=b>>1;left to right
7<Less than comparisonif(a<b)left to right
7<=Less than Equals comparisonif(a<=b)left to right
7>Greater than comparisonif(a>b)left to right
7>=Greater than Equals comparisonif(a>=b)left to right
8a==bEquals Toif(a==b)left to right
8!=Not Equals Toif(a!=b)left to right
9&Bitwise ANDa=a&1left to right
10^Bitwise Exclusive ORa=a^1left to right
11|Bitwise ORa=a|1left to right
12&&Logical ANDif(a&&b)left to right
13||Logical ORif(a||b)left to right
14?:Ternary Conditionx=(a<b)?a:b;right to left
15=Assignment Operatorx=5;right to left
15+=Increment and Assignx+=5;right to left
15-=Decrement and Assignx-=5;right to left
15*=Multiply and Assignx*=5;right to left
15/=Divide and Assignx/=5;right to left
15%=Modulo and Assignx%=2;right to left
15&=Bitwise AND and Assignx&=1;right to left
15^=Bitwise XOR and Assignx^=1;right to left
15|=Bitwise OR and Assignx|=1;right to left
15<<=Bitwise Shift Left and Assignx<<=2;right to left
15>>=Bitwise Shift Right and Assignx>>=2;right to left
16,Sequential Evaluation Operatorfor (int i=0, j=0; i<10; i++, j++)left to right

The compiler can differentiate this operator from the cout << operator based on the types used.

Knowledge Check

During C++ stack unwinding triggered by an exception, what happens to local objects in the scope being exited?

Code Order
C++

Structure a Standard Try-Catch Block

Code Cloze
C++

Safe Vector Access and Error Handling

Knowledge Check

Why is it best practice to use parentheses when combining bitwise operations with comparison operators (e.g., '(status & mask) == expected')?

Code Order
C++

Organize a Structured Switch Statement

Code Cloze
C++

Modern Efficient Iteration