3.3 C++ Exceptions and Operators

Exceptions in C++
Section titled “Exceptions in C++”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.
What is an Exception?
Section titled “What is an Exception?”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_rangeusing 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:
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, unlikeoperator[]. If the index is invalid, it throws astd::out_of_rangeexception.- The
tryblock 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 viae.what(). - A
catch (...)block can act as a fallback for unexpected exceptions. - After handling, program execution continues normally without crashing.
Modern Practices
Section titled “Modern Practices”- 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
noexceptkeyword 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.
Virtual Lab: C++ Exceptions
Section titled “Virtual Lab: C++ Exceptions”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.
C++ Expressions and Operations Reference
Section titled “C++ Expressions and Operations Reference”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.
C++ variable names
Section titled “C++ variable names”- 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.)
Character Constants
Section titled “Character Constants”Table 1 Character Constants Table
newline \n | horizontal tab \t | vertical tab \v |
backspace \b | carriage return \r | form feed \f |
bell \a | backslash \\ | question mark \? |
single quote \' | double quote \" | null character \0 |
Operators
Section titled “Operators”- 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:
Size of int is 4 bytes Size of the array p is 20 bytesConditional 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.
Note on Assignment statements
Section titled “Note on Assignment statements”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.
Bitwise Operations
Section titled “Bitwise Operations”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.
Why Bitwise Operations Are Useful
Section titled “Why Bitwise Operations Are Useful”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.
Modern Best Practice: std::byte
Section titled “Modern Best Practice: std::byte”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.
The Bitwise Operators
Section titled “The Bitwise Operators”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)Bitwise Shift Operators
Section titled “Bitwise Shift Operators”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)Virtual Lab: Bitwise Operations
Section titled “Virtual Lab: Bitwise Operations”Lab: Bitwise Operations on 8-bit unsigned int (uint8_t)
Click bits on A and B, choose an operation, and the result updates live.
Control Statements:
Section titled “Control Statements:”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)
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.
Range-based for loop
Section titled “Range-based for loop”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}Virtual Lab: Looping Mechanisms
Section titled “Virtual Lab: Looping Mechanisms”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] << " ";}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:
Loop Number:0 Loop Number:1 Loop Number:2 Loop Number:3 Loop Number:4 Loop Number:6When 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.
Virtual Lab: Control Flow
Section titled “Virtual Lab: Control Flow”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.
int x = 5;switch (x) { case 1: cout << "test1"; case 5: cout << "test5"; default: cout << "none";}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.
Precedence Reference:
Section titled “Precedence Reference:”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 intentBest 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
| Precedence | Operator | Description | Example | Associativity |
|---|---|---|---|---|
| 1 | () | Grouping operator | (3+a)*2; | left to right |
| 1 | [] | Array Access | a[0]=10; | left to right |
| 1 | -> | Pointer Member Access | p->balance=0; | left to right |
| 1 | . | Object Member Access | account.balance=0; | left to right |
| 1 | :: | Scoping operator | ::value = 1; | left to right |
| 1 | ++ | Post Increment operator | x++; | left to right |
| 1 | -- | Post Decrement operator | x--; | left to right |
| 2 | ! | Logical Negation | if(!running) | right to left |
| 2 | ~ | Bitwise Complement | x=~x; | right to left |
| 2 | ++ | Pre Increment operator | ++x; | right to left |
| 2 | -- | Pre Decrement operator | --x; | right to left |
| 2 | - | Unary minus | a=-b; | right to left |
| 2 | + | Unary plus | a=+b; | right to left |
| 2 | * | Dereference | a=*ptr; | right to left |
| 2 | & | Address of | ptr=&a[0]; | right to left |
| 2 | (type) | Cast to a type | x=static_cast<int>(23.6); | right to left |
| 2 | sizeof | Size in bytes | x=sizeof(X); | right to left |
| 3 | ->* | Member Pointer Selector | ptr->*p=1; | left to right |
| 3 | .* | Member Pointer Selector | account.*p=1; | left to right |
| 4 | * | Multiplication | a=b*c; | left to right |
| 4 | / | Division | a=b/c; | left to right |
| 4 | % | Modulus | remainder=b%c; | left to right |
| 5 | + | Addition | a=b+c; | left to right |
| 5 | - | Subtraction | a=b-c; | left to right |
| 6 | << | Bitwise Left Shift | a=b<<1; | left to right |
| 6 | >> | Bitwise Right Shift | a=b>>1; | left to right |
| 7 | < | Less than comparison | if(a<b) | left to right |
| 7 | <= | Less than Equals comparison | if(a<=b) | left to right |
| 7 | > | Greater than comparison | if(a>b) | left to right |
| 7 | >= | Greater than Equals comparison | if(a>=b) | left to right |
| 8 | a==b | Equals To | if(a==b) | left to right |
| 8 | != | Not Equals To | if(a!=b) | left to right |
| 9 | & | Bitwise AND | a=a&1 | left to right |
| 10 | ^ | Bitwise Exclusive OR | a=a^1 | left to right |
| 11 | | | Bitwise OR | a=a|1 | left to right |
| 12 | && | Logical AND | if(a&&b) | left to right |
| 13 | || | Logical OR | if(a||b) | left to right |
| 14 | ?: | Ternary Condition | x=(a<b)?a:b; | right to left |
| 15 | = | Assignment Operator | x=5; | right to left |
| 15 | += | Increment and Assign | x+=5; | right to left |
| 15 | -= | Decrement and Assign | x-=5; | right to left |
| 15 | *= | Multiply and Assign | x*=5; | right to left |
| 15 | /= | Divide and Assign | x/=5; | right to left |
| 15 | %= | Modulo and Assign | x%=2; | right to left |
| 15 | &= | Bitwise AND and Assign | x&=1; | right to left |
| 15 | ^= | Bitwise XOR and Assign | x^=1; | right to left |
| 15 | |= | Bitwise OR and Assign | x|=1; | right to left |
| 15 | <<= | Bitwise Shift Left and Assign | x<<=2; | right to left |
| 15 | >>= | Bitwise Shift Right and Assign | x>>=2; | right to left |
| 16 | , | Sequential Evaluation Operator | for (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
Section titled “Knowledge Check”During C++ stack unwinding triggered by an exception, what happens to local objects in the scope being exited?
Structure a Standard Try-Catch Block
Safe Vector Access and Error Handling
Why is it best practice to use parentheses when combining bitwise operations with comparison operators (e.g., '(status & mask) == expected')?
Organize a Structured Switch Statement
Modern Efficient Iteration
© 2026 Derek Molloy, Dublin City University. All rights reserved.