In the previous sections, we learned about the exception types provided by the C++ standard library (such as <span>std::runtime_error</span>, <span>std::out_of_range</span>, etc.). However, in complex projects, using standard exceptions often fails to clearly and accurately describe specific errors encountered in the program. At this point, custom exceptions become a key technology for building robust, maintainable, and easily debuggable C++ applications.
Why Do We Need Custom Exceptions?
- Express specific domain errors: For example, in a banking application, you can define
<span>InsufficientFundsException</span>, which is much clearer than the generic<span>std::runtime_error("Withdrawal amount too large")</span>. - Carry rich error information: Custom exception classes can contain multiple data members to store various contextual information related to the error (such as error codes, operation IDs, timestamps, etc.).
- Fine-grained exception catching and handling:
<span>catch</span>blocks can precisely catch specific types of exceptions, allowing for more targeted error recovery logic. - Improve code readability and maintainability: The names of exception classes serve as documentation, clearly indicating potential issues.
How to Create Custom Exception Classes?
In C++, creating a custom exception class is straightforward: define a new class that inherits from a standard exception class (usually <span>std::exception</span> or one of its derived classes).
Basic Steps:
-
Include necessary header files:
#include <exception> // For std::exception #include <string> // For std::string -
Derive from
<span>std::exception</span>: Your new exception class should publicly inherit from<span>std::exception</span>or one of its derived classes (like<span>std::runtime_error</span>). Inheriting from<span>std::runtime_error</span>is often a better choice because it already has built-in capabilities for storing string information. -
Override the
<span>what()</span>method: The<span>std::exception</span>base class defines a virtual function<span>virtual const char* what() const noexcept;</span>. This function is intended to return a C-style string describing the exception. In your custom exception class, you must override this function to return your own error message.
Example 1: Inheriting from <span>std::exception</span>
#include <exception>
#include <string>
class MyCustomException : public std::exception {
private:
std::string message; // Used to store detailed error information
public:
// Constructor that receives error information
explicit MyCustomException(const std::string& msg) : message(msg) {}
// Override what() method to return stored error information
const char* what() const noexcept override {
return message.c_str();
}
};
- Usage example:
#include <iostream>
void riskyFunction(int value) {
if (value < 0) {
throw MyCustomException("Input value cannot be negative!");
}
// ... normal logic
}
int main() {
try {
riskyFunction(-5);
} catch (const MyCustomException& e) {
std::cerr << "Caught my custom exception: " << e.what() << std::endl;
} catch (const std::exception& e) {
// Catch other standard exceptions
std::cerr << "Caught standard exception: " << e.what() << std::endl;
}
return 0;
}
// Output: Caught my custom exception: Input value cannot be negative!
Example 2: A More Recommended Approach — Inheriting from <span>std::runtime_error</span>
<span>std::runtime_error</span> class internally maintains a string message and overrides the <span>what()</span> method. Directly inheriting it can make the code cleaner.
#include <stdexcept> // Include std::runtime_error
class FileOpenException : public std::runtime_error {
public:
// Constructor directly calls the base class std::runtime_error constructor
explicit FileOpenException(const std::string& filename)
: std::runtime_error("Failed to open file: " + filename) {}
};
- Usage example:
#include <fstream>
void openFile(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
throw FileOpenException(filename);
}
// ... file operations
}
int main() {
try {
openFile("nonexistent.txt");
} catch (const FileOpenException& e) {
std::cerr << e.what() << std::endl; // Output: Failed to open file: nonexistent.txt
}
return 0;
}
Creating More Complex Custom Exceptions
You can add any member data to the exception class that helps with debugging and error handling.
class DatabaseQueryException : public std::runtime_error {
private:
int errorCode;
std::string failedQuery;
public:
DatabaseQueryException(int code, const std::string& query, const std::string& message)
: std::runtime_error(message), errorCode(code), failedQuery(query) {}
int getErrorCode() const { return errorCode; }
const std::string& getFailedQuery() const { return failedQuery; }
};
// Usage
try {
// ... execute database query
throw DatabaseQueryException(1064, "SELECT * FROM non_existent_table", "Syntax error in SQL query");
} catch (const DatabaseQueryException& e) {
std::cerr << "DB Error (" << e.getErrorCode() << "): " << e.what() << "\n";
std::cerr << "Failed query: " << e.getFailedQuery() << std::endl;
}
// Output:
// DB Error (1064): Syntax error in SQL query
// Failed query: SELECT * FROM non_existent_table
Best Practices
- Prefer inheriting from
<span>std::runtime_error</span>: It saves you the trouble of managing string messages yourself and is the most commonly used and convenient base class for creating custom exceptions. - Catch by reference: Always catch exceptions by
<span>const</span>reference (like<span>catch (const MyException& e)</span>) to avoid unnecessary object slicing and copying. - Maintain
<span>noexcept</span>specification: When overriding the<span>what()</span>method, keep its<span>noexcept</span>specification, as it should not throw exceptions. - Design a clear exception hierarchy: For large projects, you can create a base exception class that inherits from
<span>std::exception</span>, and then let other more specific exception classes inherit from it, forming a clear hierarchy. - Provide meaningful error messages: Include enough context in the exception messages to facilitate quick problem identification.
Conclusion
Custom exceptions are a powerful and flexible feature of the C++ exception handling mechanism. By deriving from standard exception classes, you can create semantically clear, information-rich, and highly targeted error types, greatly enhancing the quality and debuggability of your code. Remember, a good exception class serves as excellent documentation.