- Exception Handling
- C++ Standard Exceptions
- User defined exceptions
- Catching All Exceptions With Parameter Pack Expansion ...
- noexcept
exception handling has the following form:
try
{
some code
}
catch (Exception1 e)
{
some code
}
catch (Exception2 e)
{
some code
}
catch (...)
{
some code
}
-
std::bad_alloc
-
std::bad_cast
-
std::bad_exception
-
std::bad_typeid
-
std::logic_error
- std::domain_error: exception thrown when a mathematically invalid domain is used.
- std::invalid_argument
- std::length_error
- std::out_of_range
-
std::runtime_error: An exception that theoretically cannot be detected by reading the code.
- std::overflow_error
- std::underflow_error
- std::range_error
-
std::ios_base::failure
This exception is thrown by the allocation functions to when it failures to allocate memory.
try
{
while (true)
{
new int[100000000ul];
}
} catch (const std::bad_alloc& e)
{
std::cout << "Allocation failed: " << e.what() << '\n';
}
nothrow
is an empty class type and used as an argument for operator new
and operator new[]
to indicate that these functions shall not throw an exception on failure, but return a null pointer instead.
try
{
while (true)
{
new int[100000000ul]; // throwing overload
}
} catch (const std::bad_alloc& e)
{
std::cout << e.what() << '\n';
}
while (true)
{
int* p = new(std::nothrow) int[100000000ul]; // non-throwing overload
if (p == nullptr)
{
std::cout << "Allocation returned nullptr\n";
break;
}
}
This exception is thrown when a dynamic_cast
to a reference type fails, for instance when the types are not related by inheritance.
struct Foo { virtual ~Foo() {} };
struct Bar { virtual ~Bar() {} };
Bar b;
try
{
Foo& f = dynamic_cast<Foo&>(b);
} catch(const std::bad_cast& e)
{
std::cout << e.what() << '\n';
}
This exception is thrown when a typeid
operator is applied to a dereferenced null pointer value of a polymorphic type.
The type has to be polymorphic:
struct Foo
{
virtual void bar();
};
Foo* p = nullptr;
try
{
std::cout << typeid(*p).name() << '\n';
} catch(const std::bad_typeid& e) {
std::cout << e.what() << '\n';
}
This defines a type of object to be thrown as exception when there are a consequence of faulty logic in your program.
int amount, available;
amount=10;
available=9;
try
{
if(amount>available)
throw std::logic_error( "logic error" );
}
catch ( std::exception &e )
{
std::cerr << "Caught: " << e.what( ) << std::endl;
std::cerr << "Type: " << typeid( e ).name( ) << std::endl;
};
This defines a type of object to be thrown as exception in situations where the inputs are outside of the domain on which an operation is defined.
try
{
const double x = std::acos(2.0);
std::cout << x << '\n';
}
catch (std::domain_error& e)
{
std::cout << e.what() << '\n';
}
This defines a type of object to be thrown as exception when an argument value has not been accepted.
binary wrongly represented by char X
:
try
{
std::bitset<32> bitset(std::string("0101001X01010110000"));
}
catch (std::exception &err)
{
std::cerr<<"Caught "<<err.what()<<std::endl;
std::cerr<<"Type "<<typeid(err).name()<<std::endl;
}
This defines a type of object to be thrown as exception as a result of attempts to exceed implementation defined length limits for some object. For instance vector throws a length_error
if resized above max_size
try
{
std::vector<int> myvector;
myvector.resize(myvector.max_size()+1);
}
catch (const std::length_error& le)
{
std::cerr << "Length error: " << le.what() << '\n';
}
This defines a type of object to be thrown as exception as consequence of attempt to access elements out of defined range.
std::vector<int> myvector(10);
try
{
myvector.at(20)=100; // vector::at throws an out-of-range
}
catch (const std::out_of_range& oor)
{
std::cerr << "Out of Range error: " << oor.what() << '\n';
}
The only standard library components that throw std::overflow_error
are std::bitset::to_ulong
and std::bitset::to_ullong
.
try
{
std::bitset<100> bitset;
bitset[99] = 1;
bitset[0] = 1;
// to_ulong(), converts a bitset object to the integer that would generate the sequence of bits
unsigned long Test = bitset.to_ulong();
}
catch(std::exception &err)
{
std::cerr<<"Caught "<<err.what()<<std::endl;
std::cerr<<"Type "<<typeid(err).name()<<std::endl;
}
This defines a type of object to be thrown as exception where a result of a computation cannot be represented by the destination type.
The only standard library components that throw this exception are std::wstring_convert::from_bytes
and std::wstring_convert::to_bytes
.
try
{
throw std::range_error( "The range is in error!" );
}
catch (std::range_error &e)
{
std::cerr << "Caught: " << e.what( ) << std::endl;
std::cerr << "Type: " << typeid( e ).name( ) << std::endl;
}
Definition:
struct CustomException : public std::exception
{
const char * what () const throw ()
{
return "CustomException happened";
}
};
usage:
try
{
throw CustomException();
} catch(CustomException& e)
{
std::cout << "CustomException caught" << std::endl;
std::cout << e.what() << std::endl;
} catch(std::exception& e)
{
//Other errors
}
std::ifstream f("doesn't exist");
f.exceptions ( ifstream::badbit ); // No need to check failbit
try
{
f.exceptions(f.failbit);
}
catch (const std::ios_base::failure& e)
{
std::cout << "Caught an ios_base::failure.\n"
<< "Explanatory string: " << e.what() << '\n'
<< "Error code: " << e.code() << '\n';
}
Ref: 1
...
is a parameter pack and refers to zero or more template parameters. The ...
will catch all exception.
try
{
some code
}
catch (Exception e)
{
some code
}
catch (...)//... Parameter Pack Expansion, will catch any exception
{
some code
}
The noexcept
specification in C++ is used to indicate that a function is not expected to throw exceptions. This helps in optimizing the code, as the compiler can make certain optimizations knowing that no exceptions will be thrown from that function. However, if an exception is thrown from a noexcept
function, the program will call std::terminate
, resulting in a potential program crash.
-
Performance Critical Functions: In functions where performance is critical, and you're sure that no exceptions will be thrown, using
noexcept
can improve performance. -
Move Constructors and Move Assignments: It's generally good practice to mark move constructors and move assignments as
noexcept
. This is because many standard library implementations will only use move semantics if these operations are marked asnoexcept
. -
Functions Guaranteed Not to Throw: If you're certain that a function won't throw an exception (like simple getters or setters that don't do any complex operations), marking them as
noexcept
can be a good practice.
Imagine you're developing a real-time game engine where performance is critical. You have a function that updates the position of a game object based on its velocity and the elapsed time. This function is straightforward and doesn't involve operations that might throw exceptions (like memory allocation, file I/O, etc.).
class GameObject {
public:
// Other members...
// Update position - noexcept since it's a simple calculation
void updatePosition(float elapsedTime) noexcept {
position.x += velocity.x * elapsedTime;
position.y += velocity.y * elapsedTime;
// Other simple calculations...
}
// Other members...
};
// In the game loop
gameObject.updatePosition(elapsedTime);
In this scenario, using noexcept
for updatePosition
makes sense because:
- The function is simple and unlikely to throw exceptions.
- The function is likely called very frequently (every frame), so any performance improvement is beneficial.
- If an exception does occur here, it likely indicates a severe logic error or a bug that should terminate the program, which is the behavior
noexcept
enforces.
Marking move constructors and move assignments as noexcept
in C++ is considered good practice for several key reasons:
-
Optimizations in Standard Library Containers: Many standard library containers, like
std::vector
andstd::deque
, can perform certain optimizations if they know that move operations do not throw exceptions. For instance, when astd::vector
resizes, it may choose to move its elements to the new memory location instead of copying them, but only if the move operations arenoexcept
. This can lead to significant performance improvements. -
Strong Exception Safety Guarantee: By marking move operations as
noexcept
, you are ensuring that these operations won't throw exceptions, which aids in providing strong exception safety guarantees. This is particularly important in scenarios where maintaining system state consistency is crucial, and exceptions can lead to partial state changes or leaks. -
Better Resource Management and Safety: In the context of resource management (like memory, file handles, network connections), move semantics allow for efficient transfer of resources. When these operations are
noexcept
, it ensures that the resource transfer is safe and no exceptions will be thrown during the process, preventing resource leaks or undefined states. -
Improved Compiler Error Messages: If you mistakenly use a type in a context that requires a
noexcept
move operation and your type doesn't provide it, the compiler can give a clear and specific error message. This helps in catching potential issues at compile time. -
Compatibility with Move-Only Types: Some types in C++ are move-only (like
std::unique_ptr
). If your class holds move-only members and your move operations are notnoexcept
, this can lead to complications or even prevent your class from being used in certain standard library containers or algorithms. -
Semantical Clarity: Marking move operations as
noexcept
clearly communicates your intent to other developers that these operations are safe and won't throw exceptions. This enhances code readability and maintainability.
In summary, using noexcept
with move constructors and assignments improves performance and exception safety, ensures better resource management, improves code clarity, and ensures compatibility with certain types and containers in the C++ standard library.
This means if a function specified with noexcept it shouldn't throw exception (evaluation of its operand can propagate an exception).
The bodies of called functions are not examined to check if they actually throw exception or not, and noexcept
can yield false negatives.
In the case of exception std::sterminate
will be called
It tests if a function noexcept specification evaluate to true or false at compile time. noexcept(some compile time expression) and this returns a boolean
examples:
equals to noexcept(true),
this func
can not throw exception
void func1() noexcept
{
}
equals to not using noexcept, means this func2 can throw exception
void func2() noexcept (false)
{
}
void bar() noexcept
{
}
void baz() noexcept
{
throw std::range_error("range error");
}
void foo()
{
return;
}
now running the followings:
std::cout << std::boolalpha;
std::cout << noexcept(bar()) << '\n';
std::cout << noexcept(baz()) << '\n';
std::cout << noexcept(foo()) << '\n';
std::cout << noexcept(1 + 1) << '\n';
the output is:
true
true
false
true
- When using c++ functions in c
- when c++ standard requires us.
Almost every optimization in the compiler uses something called a "flow graph" of a function to reason about what is legal. A flow graph consists of what are generally called "blocks" of the function (areas of code that have a single entrance and a single exit) and edges between the blocks to indicate where flow can jump to. Noexcept alters the flow graph.
A noexcept specification on a function is merely a method for a programmer to inform the compiler whether or not a function should throw exceptions.
The compiler can use this information to enable certain optimizations on non-throwing functions as well as enable the noexcept operator, which can check at compile time if a particular expression is declared to throw any exceptions.
Declaring a function noexcept helps optimizers by reducing the number of alternative execution paths. It also speeds up the exit after failure.