C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
96: std::expected for Error Handling
For years, we've been caught in a tug-of-war between two extremes of error handling in C++. On one side, you have exceptions, which are powerful but can make the control flow feel like a magic trick—suddenly you're jumping three levels up the call stack. On the other, you have the "C-style" approach: returning a magic number like -1 or a bool to indicate failure. I've spent way too many hours debugging production crashes because someone forgot to check a return code, or because a -1 was actually a valid piece of data.
The Frustration of the Silent Null
When std::optional arrived, it felt like a breath of fresh air. It explicitly told the caller, "This function might not return a value." But std::optional has a glaring weakness: it's silent. It tells you that something failed, but it never tells you why. Consider a function that parses a configuration value from a file. If it returns std::nullopt, is the file missing? Is the key misspelled? Is the value the wrong data type?
// The "I don't know what went wrong" approach
std::optional<int> parse_port(const std::string& input) {
try {
return std::stoi(input);
} catch (...) {
return std::nullopt; // We lost all the context of the error here
}
}
In a small tool, this is fine. In a complex system, this is a nightmare. You end up adding logging statements everywhere just to figure out which nullopt triggered the failure. You're essentially guessing at the state of the system.
Explicit Intent with std::expected
This is where std::expected (introduced in C++23) changes the game. Think of it as a specialized union: it contains either the expected value or an error object. It gives you the semantic clarity of an optional, but it carries the payload of an exception without the overhead of stack unwinding.
If we rewrite that port parser, we can define a specific error type—maybe an enum class—to tell the caller exactly what happened. Now, the function signature itself becomes a piece of documentation.
enum class ParseError {
InvalidFormat,
OutOfRange,
EmptyInput
};
std::expected<int, ParseError> parse_port(const std::string& input) {
if (input.empty()) {
return std::unexpected(ParseError::EmptyInput);
}
try {
return std::stoi(input);
} catch (const std::invalid_argument&) {
return std::unexpected(ParseError::InvalidFormat);
} catch (const std::out_of_range&) {
return std::unexpected(ParseError::OutOfRange);
}
}
Notice the use of std::unexpected. It's a wrapper that tells the compiler, "I'm not returning the value; I'm returning the error." It prevents the compiler from accidentally confusing an error integer with a success integer.
Balancing Verbosity and Safety
Now, you might ask: "Why not just use exceptions?" I usually tell my teammates that exceptions are for exceptional circumstances—things that shouldn't happen in a healthy system, like running out of memory or a lost database connection. But a user typing a letter into a port number field isn't exceptional; it's expected. Using std::expected for these cases keeps your "happy path" clean while forcing the caller to acknowledge the error explicitly.
The trade-off is a bit more verbosity at the call site. You can't just call the function and forget about the result; you have to check it. But in my experience, that extra line of code is a cheap price to pay for not waking up at 3 AM to a crash dump that just says "Value was null".
auto result = parse_port("8080");
if (result) {
std::cout << "Port is: " << *result << "\n";
} else {
switch (result.error()) {
case ParseError::InvalidFormat: std::cerr << "Not a number!\n"; break;
case ParseError::OutOfRange: std::cerr << "Number too big!\n"; break;
case ParseError::EmptyInput: std::cerr << "Input was empty!\n"; break;
}
}
By returning the error in-band, you're treating error handling as a first-class part of your API design rather than a side-channel mechanism. It makes the code predictable, testable, and significantly more robust.
📋 Practical Task
Implement a Robust User Permission Validator
You are building a security module for a system. You need to write a function validate_permission that checks if a user has a specific access level. Instead of returning a simple boolean, use std::expected to provide detailed feedback on why a request was denied.
Requirements:
- Define an enum
PermissionErrorwith the following values:UserNotFound,InsufficientLevel, andAccountLocked. - Create a function
std::expected<bool, PermissionError> validate_permission(int userId, int requiredLevel). - Simulate a database lookup:
- If
userIdis negative, returnUserNotFound. - If
userIdis 0, returnAccountLocked. - If the
userIdis positive, simulate a user level (e.g., ifuserId % 2 == 0, they are level 10; otherwise, level 1). If their level is less thanrequiredLevel, returnInsufficientLevel. - Otherwise, return
true.
- If
- In your
mainfunction, call this validator with three different cases (one for each error and one success) and print a user-friendly message based on the specificPermissionErrorreturned.
There are no comments for now.