RoadMap notes 2 Flashcards
Exception handling, language concepts, STL, templates
Exception handling
a mechanism to handle errors, anomalies, or unexpected events that can occur during the runtime execution of a program. This allows the program to continue running or exit gracefully when encountering errors instead of crashing abruptly.
C++ provides a set of keywords and constructs for implementing exception handling:
try: Defines a block of code that should be monitored for exceptions.
catch: Specifies the type of exception to be caught and the block of code that shall be executed when that exception occurs.
throw: Throws an exception that will be caught and handled by the appropriate catch block.
noexcept: Specifies a function that doesn’t throw exceptions or terminates the program if an exception is thrown within its scope.
Exception handling example
int divide(int a, int b) {
if (b == 0) {
throw “Division by zero!”;
}
return a / b;
}
int main() {
int num1, num2;
std::cout << "Enter two numbers for division: "; std::cin >> num1 >> num2; try { int result = divide(num1, num2); std::cout << "The result is: " << result << std::endl; } catch (const char* msg) { std::cerr << "Error: " << msg << std::endl; }
define a function divide that throws an exception if b is zero. In the main function, we use a try block to call divide and output the result. If an exception is thrown, it is caught inside the catch block, which outputs an error message. This way, we can handle the error gracefully rather than letting the program crash when attempting to divide by zero.
Standard exceptions
include <stdexcept></stdexcept>
classes under the <stdexcept> library which can be used as the exception type for more specific error handling. Some of these classes include:</stdexcept>
std::exception: Base class for all standard exceptions.
std::logic_error: Represents errors which can be detected statically by the program.
std::runtime_error: Represents errors occurring during the execution of a program.
int divide(int a, int b) {
if (b == 0) {
throw std::runtime_error(“Division by zero!”);
}
return a / b;
}
int main() {
int num1, num2;
std::cout << "Enter two numbers for division: "; std::cin >> num1 >> num2; try { int result = divide(num1, num2); std::cout << "The result is: " << result << std::endl; } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; }
modified the divide function to throw a std::runtime_error instead of a simple string. The catch block now catches exceptions derived from std::exception and uses the member function what() to display the error message.
Exception handling:
try { … }
In the try block, you write the code that can possibly generate an exception. If an exception is encountered, the control is passed to the relevant catch block to handle the issue.
Example:
try {
// code that might throw an exception
}
Exception handling:
catch( … ) { … }
The catch block follows the try block and is responsible for handling the exceptions thrown by the try block. There can be multiple catch blocks to handle different types of exceptions.
Example:
catch (int e) {
// handle exception of type int
}
catch (char e) {
// handle exception of type char
}
catch (…) {
// handle any other exception
}
Exception handling:
throw …;
In case an error occurs within the try block, you can use the throw keyword to generate an exception of the specific type. This will then be caught and handled by the corresponding catch block.
Example:
try {
int num1 = 10, num2 = 0;
if (num2 == 0) {
throw “Division by zero not allowed!”;
} else {
int result = num1 / num2;
cout «_space;“Result: “ «_space;result «_space;endl;
}
}
catch (const char* e) {
cout «_space;“Error: “ «_space;e «_space;endl;
}
Exit Codes
also known as “return codes” or “status codes”, are numeric values that a program returns to the calling environment (usually the operating system) when it finishes execution. These codes are used to indicate the success or failure of a program’s execution.
0 is the standard exit code for a successful execution, while non-zero exit codes typically indicate errors or other exceptional situations. The actual meanings of non-zero exit codes can vary between different applications or systems.
In C++, you can return an exit code from the main function by using the return statement, or you can use the exit() function, which is part of the C++ Standard Library.
Examples:
return 0;
std::exit(1);
Access Violation
specific type of error that occurs when a program attempts to access an illegal memory location. In C++, access violations are most commonly caused by:
Dereferencing a null or invalid pointer.
Accessing an array out of bounds.
Reading or writing to memory freed by the user or the operating system.
It is crucial to identify access violations because they can lead to unpredictable behavior, application crashes, or corruption of data.
Dereferencing null or invalid pointer
int *p = nullptr;
int x = *p; // Access violation: trying to access null pointer’s content
Accessing an array out of bounds
int arr[5] = {1, 2, 3, 4, 5};
int y = arr[5]; // Access violation: index out of bounds (valid indices are 0-4)
Reading or writing to freed memory
int* p2 = new int[10];
delete[] p2;
p2[3] = 42; // Access violation: writing to memory that has been freed
Debugging Access Violations
Tools like debuggers, static analyzers, and profilers can help identify access violations in your code. For example:
Microsoft Visual Studio: Use the built-in debugger to identify the line of code responsible for the access violation error.
Valgrind: A popular Linux tool that detects memory leaks and access violations in your C++ programs.
AddressSanitizer: A runtime memory error detector for C++ that can detect out-of-bounds accesses, memory leaks, and use-after-free errors.
C-style casting
It is the syntax inherited from C, and it is done by simply putting the target data type in parentheses before the value to cast. Example:
int a = 10;
float b = (float)a; // C-style cast from int to float
static_cast
This is the most commonly used method for type casting in C++. It is performed at compile time, and you should use it when you have an explicit conversion between data types. Example:
int a = 10;
float b = static_cast<float>(a); // static_cast from int to float</float>
dynamic_cast
This method is specifically used for safely converting pointers and references between base and derived classes in a class hierarchy. Example:
class Base {};
class Derived : public Base {};
Base* base_ptr = new Derived();
Derived* derived_ptr = dynamic_cast<Derived>(base_ptr); // dynamic_cast from Base to Derived*
reinterpret_cast
This cast changes the type of a pointer, reference, or an integer value. It is also called a bitwise cast because it changes how the compiler interprets the underlying bits. Use reinterpret_cast only when you have a deep understanding of what you’re doing, as it does not guarantee that the resulting value will be meaningful. Example:
int* a = new int(42);
long b = reinterpret_cast<long>(a); // reinterpret_cast from int* to long</long>
const_cast
This casting method is used to remove the const qualifier from a variable. It is generally not recommended, but can be useful in certain situations where you have no control over the constness of a variable. Example:
const int a = 10;
int* ptr = const_cast<int>(&a); // const_cast from const int to int*
*ptr = 20; // Not recommended, use with caution
Static Cast
casting operators in C++ that allows you to convert between different data types, such as integer and float, or between pointer types. This type of cast performs a compile-time check and gives an error if there is no valid conversion possible between given types. static_cast is generally safer than C-style casts since it does not perform an unsafe reinterpretation of data and allows for better type checking.
Static cast syntax
static_cast<new_type>(expression)</new_type>
Convert between basic data types
int i = 42;
float f = static_cast<float>(i); // Converts integer i to float f</float>
Casting pointers of different object types in an inheritance hierarchy:
class Base { /* … / };
class Derived : public Base { / … */ };
Base *bPtr = new Derived;
Derived *dPtr = static_cast<Derived *>(bPtr); // Converts Base pointer bPtr to Derived pointer dPtr
Converting an integer to an enumeration
enum Color { RED, GREEN, BLUE };
int value = 1;
Color color = static_cast<Color>(value); // Converts integer value to corresponding Color enumeration</Color>
Keep in mind that static_cast should be used with caution when casting pointers between different object types.
If the original type of the pointer does not match the target type, the result of the cast can be incorrect or cause unexpected behavior.
const_cast
type of casting in C++ that allows you to remove or add constness to a variable. In other words, it enables you to modify a const or volatile object, or change a pointer or reference to a const or volatile type. This is useful in certain scenarios when you need to pass a const variable as an argument or when a function parameter requires a non-const type, but you want to make sure the variable remains constant throughout the code.
Keep in mind that using const_cast to modify a truly const variable can lead to undefined behavior, so it is best to use this feature only when absolutely necessary.
void modifyVariable(int* ptr) {
*ptr = 42;
}
int main() {
const int original_value = 10;
int* non_const_value_ptr = const_cast<int*>(&original_value);
std::cout «_space;“Original value: “ «_space;original_value «_space;std::endl;
modifyVariable(non_const_value_ptr); std::cout << "Modified value: " << *non_const_value_ptr << std::endl;
first create a const variable, original_value. Then we use const_cast to remove the constness of the variable and assign it to a non-const pointer, non_const_value_ptr. The modifyVariable function takes an int* as an argument and modifies the value pointed to by the pointer, which would not have been possible if we passed the original const int directly. Finally, we print the original_value and the *non_const_value_ptr, which shows that the value has been modified using const_cast.
Please note that this example comes with some risks, as it touches undefined behavior. */
dynamic_cast
type of casting operator in C++ that is used specifically for polymorphism. It safely converts pointers and references of a base class to its derived class and checks the validity of the conversion during runtime. If the conversion is not valid (i.e., the object is not of the target type), it returns a null pointer instead of producing undefined behavior. Therefore, dynamic_cast can prevent potential crashes and errors when using polymorphism.
dynamic_cast example
class BaseClass {
public:
virtual void display() {
std::cout «_space;“BaseClass” «_space;std::endl;
}
};
class DerivedClass : public BaseClass {
public:
void display() {
std::cout «_space;“DerivedClass” «_space;std::endl;
}
};
int main() {
BaseClass *basePtr = new DerivedClass(); // Upcasting
DerivedClass *derivedPtr;
derivedPtr = dynamic_cast<DerivedClass *>(basePtr); // Downcasting if (derivedPtr) { derivedPtr->display(); // Output: DerivedClass } else { std::cout << "Invalid type conversion."; } delete basePtr;
a pointer to a DerivedClass object is assigned to a BaseClass pointer (basePtr). Then, we attempt to downcast it back to a DerivedClass pointer using dynamic_cast. If the casting is successful, we can access the DerivedClass functionality through the new pointer (derivedPtr).
reinterpret_cast
type of casting in C++ that allows you to change the type of a pointer or an integer without altering the representation of the data. It is generally used when the conversion required is too low-level or not supported by other casting methods, such as static_cast.
Using reinterpret_cast should be handled with care, as it can lead to undefined behavior and severe problems if used incorrectly.
reinterpret_cast example
int num = 42;
int *num_ptr = #
// Disguise the integer pointer as a char pointer char *char_ptr = reinterpret_cast<char *>(num_ptr); for (size_t i = 0; i < sizeof(int); ++i) { // Print the individual bytes of the integer as characters std::cout << "Byte " << i << ": " << char_ptr[i] << std::endl; }
using reinterpret_cast to change the type of a pointer from int * to char *, effectively treating the integer as an array of characters and printing each byte.
Remember that when using reinterpret_cast, you should be cautious about dereferencing the converted pointers. The behavior can be unpredictable, and it can lead to issues, such as accessing memory regions that are not intended to be accessed. reinterpret_cast should be used sparingly and only when a low-level conversion is necessary.
ADL (Argument Dependent Lookup)
AKA Koenig Lookup is a mechanism in C++ that allows the compiler to search for the appropriate function to call based on the types of arguments provided. It is particularly helpful when using overloaded functions or operators in a namespace.
ADL allows the compiler to find functions in the same namespace as the arguments, even if the function is not declared at the point of use or within the namespace provided. This is especially useful when working with templates or generic programming.
Consider the following example using a namespace and overloaded operator«():
namespace MyNamespace {
class MyClass {
public:
int value;
};
std::ostream& operator<<(std::ostream& os, const MyClass& obj) { os << "MyClass: " << obj.value; return os; } }
int main() {
MyNamespace::MyClass obj;
obj.value = 42;
using std::cout;
// Required to use ‘cout’ without fully qualifying it.
cout «_space;obj «_space;std::endl;
when you call cout «_space;obj; in main(), ADL is used to find the correct operator«() in the MyNamespace namespace because the argument obj is of type MyNamespace::MyClass.