C++ Throw
Introduction
Exception handling is a critical part of writing robust and reliable C++ programs. The throw
keyword is one of the three pillars of C++ exception handling, alongside try
and catch
. It allows you to signal that an exceptional situation has occurred that the normal flow of the program cannot handle.
In this tutorial, we'll explore:
- What the
throw
keyword does - How to use it properly
- When you should throw exceptions
- Best practices for throwing exceptions
Understanding the Throw Keyword
The throw
keyword in C++ is used to generate (or "raise") an exception. When a throw
statement is executed, program control is transferred to the nearest appropriate catch
block that can handle the exception.
Basic Syntax
The basic syntax of the throw
statement is:
throw expression;
Where expression
can be any type of value or object that represents the error that occurred.
Throwing Basic Types
You can throw values of any type in C++, including primitive types like integers, floating-point numbers, and characters.
Example: Throwing an Integer
#include <iostream>
double divide(double a, double b) {
if (b == 0) {
throw 0; // Throwing an integer to indicate division by zero
}
return a / b;
}
int main() {
try {
std::cout << "10 / 2 = " << divide(10, 2) << std::endl;
std::cout << "10 / 0 = " << divide(10, 0) << std::endl; // This will throw an exception
} catch (int e) {
std::cout << "Error: Division by zero!" << std::endl;
}
return 0;
}
Output:
10 / 2 = 5
Error: Division by zero!
In this example, when we try to divide by zero, the function divide
throws an integer value 0
. The catch
block in main
catches this integer exception and displays an error message.
Throwing C++ Standard Exceptions
The C++ Standard Library provides a hierarchy of exception classes that you can use. These classes are defined in the <stdexcept>
header and include:
std::exception
: Base class for all standard exceptionsstd::runtime_error
: Runtime errorsstd::logic_error
: Logic errors- And many more specialized exceptions
Example: Using Standard Exceptions
#include <iostream>
#include <stdexcept>
#include <string>
double divide(double a, double b) {
if (b == 0) {
throw std::runtime_error("Division by zero is not allowed");
}
return a / b;
}
int main() {
try {
std::cout << "10 / 2 = " << divide(10, 2) << std::endl;
std::cout << "10 / 0 = " << divide(10, 0) << std::endl; // This will throw an exception
} catch (const std::exception& e) {
std::cout << "Error: " << e.what() << std::endl;
}
return 0;
}
Output:
10 / 2 = 5
Error: Division by zero is not allowed
In this improved example, we throw a std::runtime_error
with a descriptive message. When caught, we can use the what()
method to get the error message.
Throwing Custom Exceptions
For more specific error situations, you might want to create your own exception classes that inherit from std::exception
.
Example: Custom Exception Class
#include <iostream>
#include <stdexcept>
#include <string>
// Custom exception class
class DivisionByZeroError : public std::runtime_error {
public:
DivisionByZeroError(const std::string& message)
: std::runtime_error(message) {}
};
double divide(double a, double b) {
if (b == 0) {
throw DivisionByZeroError("Cannot divide " + std::to_string(a) + " by zero");
}
return a / b;
}
int main() {
try {
std::cout << "10 / 2 = " << divide(10, 2) << std::endl;
std::cout << "10 / 0 = " << divide(10, 0) << std::endl; // This will throw our custom exception
} catch (const DivisionByZeroError& e) {
std::cout << "Division Error: " << e.what() << std::endl;
} catch (const std::exception& e) {
std::cout << "Generic Error: " << e.what() << std::endl;
}
return 0;
}
Output:
10 / 2 = 5
Division Error: Cannot divide 10.000000 by zero
This custom exception allows us to:
- Handle specific types of errors differently
- Include more context-specific information
- Maintain a clear hierarchy of error types
Re-throwing Exceptions
Sometimes you might catch an exception but then decide you can't fully handle it. In that case, you can re-throw the exception for outer catch
blocks to handle.
Example: Re-throwing an Exception
#include <iostream>
#include <stdexcept>
void handleException() {
try {
throw std::runtime_error("Original error");
} catch (const std::exception& e) {
std::cout << "First handler: " << e.what() << std::endl;
// Re-throw the same exception
throw;
}
}
int main() {
try {
handleException();
} catch (const std::exception& e) {
std::cout << "Second handler: " << e.what() << std::endl;
}
return 0;
}
Output:
First handler: Original error
Second handler: Original error
In this example, we catch the exception in handleException()
, do some processing, and then re-throw it to be caught again in main()
.
Throwing in a Class Hierarchy
When working with class hierarchies, you often want to throw specialized exceptions while catching more general ones.
Example: Exception Hierarchy
#include <iostream>
#include <stdexcept>
#include <vector>
void accessVector(const std::vector<int>& vec, size_t index) {
if (index >= vec.size()) {
throw std::out_of_range("Index " + std::to_string(index) +
" is out of range (size: " +
std::to_string(vec.size()) + ")");
}
std::cout << "Value at index " << index << ": " << vec[index] << std::endl;
}
int main() {
std::vector<int> numbers = {10, 20, 30, 40, 50};
try {
accessVector(numbers, 2); // This is fine
accessVector(numbers, 10); // This will throw an exception
} catch (const std::out_of_range& e) {
std::cout << "Out of range error: " << e.what() << std::endl;
} catch (const std::logic_error& e) {
std::cout << "Logic error: " << e.what() << std::endl;
} catch (const std::exception& e) {
std::cout << "Standard exception: " << e.what() << std::endl;
}
return 0;
}
Output:
Value at index 2: 30
Out of range error: Index 10 is out of range (size: 5)
This example shows how to throw a specific type of exception (std::out_of_range
) that is caught by a matching catch handler, even though there are other more general handlers available.
Real-World Application: Resource Management
Here's a practical example showing how to use exceptions for resource management in a file handling scenario:
#include <iostream>
#include <fstream>
#include <stdexcept>
#include <string>
class FileReader {
private:
std::ifstream file;
std::string filename;
public:
FileReader(const std::string& filename) : filename(filename) {
file.open(filename);
if (!file.is_open()) {
throw std::runtime_error("Failed to open file: " + filename);
}
}
~FileReader() {
if (file.is_open()) {
file.close();
}
}
std::string readLine() {
std::string line;
if (!std::getline(file, line) && !file.eof()) {
throw std::runtime_error("Error reading from file: " + filename);
}
return line;
}
bool hasMoreLines() {
return !file.eof();
}
};
int main() {
try {
// Try to open a file that may not exist
FileReader reader("config.txt");
std::cout << "File contents:" << std::endl;
while (reader.hasMoreLines()) {
std::cout << reader.readLine() << std::endl;
}
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
std::cerr << "Please check if the file exists and you have read permissions." << std::endl;
return 1;
}
return 0;
}
If the file doesn't exist or can't be opened, a runtime_error
exception is thrown and caught in main()
, allowing for graceful error handling.
Best Practices for Using throw
-
Be specific with exception types: Throw the most specific exception type that accurately represents the error.
-
Include detailed error messages: Your exception messages should provide enough information to understand what went wrong and potentially how to fix it.
-
Don't overuse exceptions: Use exceptions for exceptional conditions, not for normal control flow.
-
Throw by value, catch by reference: This is the recommended pattern to avoid object slicing.
-
Document the exceptions your functions might throw: Make it clear in your function documentation what exceptions might be thrown.
-
Consider exception safety: Make sure your code cleans up resources properly even when exceptions are thrown.
-
Avoid throwing exceptions in destructors: This can lead to program termination if an exception is already being handled.
Summary
The throw
keyword in C++ is a powerful tool for error handling that allows you to:
- Signal exceptional conditions in your code
- Transfer control to exception handlers
- Provide detailed information about what went wrong
- Create a hierarchy of error types for precise handling
By understanding how to use throw
properly, you can write more robust C++ programs that gracefully handle errors and unexpected situations.
Exercises
-
Create a function that validates a username and throws different types of exceptions based on various validation rules (e.g., too short, contains invalid characters, etc.).
-
Implement a simple stack class that throws exceptions for operations like pop on an empty stack.
-
Write a program that reads data from a CSV file and throws appropriate exceptions for different types of formatting errors.
-
Extend the FileReader example to handle different types of file errors with specific exception types.
-
Implement a function that performs a binary search on a sorted array and throws appropriate exceptions for invalid inputs.
Additional Resources
- C++ Reference: Exception Handling
- C++ Standard Library Exceptions
- "Effective C++" by Scott Meyers (Chapter on Exception Safety)
- "Exceptional C++" by Herb Sutter
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)