License : Creative Commons Attribution 4.0 International (CC BY-NC-SA 4.0)
Copyright :
Hervé Frezza-Buet,
CentraleSupelec
Last modified : April 19, 2024 10:22
Link to the source : exceptions.md
This is a rather complex issue. Indeed, without exception mechanism, the program flow follows its course peacefully, even in case of an error. In this case, the function that detects the error ends with an error code, which the caller must test to see if the function call was successful.
Testing the error codes is not mandatory (the compilation is fine if you don’t do it). Moreover, if it is done, the code is peppered with tests at each function call… From the point of view of readability and reliability of the code we recommend to use exceptions. Indeed, the management of memory cleaning during an exception is well done. An exception can for example occur during the construction of an object, during its destruction, in a context where pointers are allocated in the heap and where they have not been desalted when the exception occurs… In more demanding contexts (performance, embedded code whose size is constrained), the overhead induced by the implementation of an exception mechanism by the compiler can be problematic. Even if C++ exceptions sometimes get a bad press in terms of performance, they should not be rejected. Indeed, the implementation of these mechanisms has evolved since then, and some exception compilation strategies only impact performance when the exception is triggered, which is rare in general. In short, in these contexts, it is advisable to look twice before depriving oneself for performance reasons of the comfort and reliability offered by the use of exceptions.
try... catch
blockWhen a code is likely to trigger an exception, we surround it with a try {...}
expression. The catch
es that immediately follow are exception handling functions (handler). An exception that is not handled is passed on to higher levels of execution (enclosing blocks, higher calls, etc.) until it is caught. If it is not, the program ends with an error.
#include <iostream>
struct A {};
struct B : public A {};
struct C : public A {};
void f(int i) {
switch(i) {
case 0 : throw A();
case 1 : throw B();
case 2 : throw C();
case 3 : throw 12.345;
case 4 : throw std::string("Yep !");
default : break;
}
}
int main(int argc, char* argv[]) {
for(unsigned int i=0; i < 100; ++i) {
try {
std::cout << "i = " << i << std::endl;
f(i);
}
catch(const B&) {
std::cout << "B caught." << std::endl;
}
catch(const A&) {
std::cout << "A caught." << std::endl;
}
catch(const std::string& s) {
std::cout << "\"" << s << "\" caught, but not really handled."
<< std::endl;
throw; // This re-trows the exception.
}
catch(...) {
std::cout << "Something caught." << std::endl;
}
}
return 0;
}
// Output :
// i = 0
// A caught.
// i = 1
// B caught.
// i = 2
// A caught.
// i = 3
// Something caught.
// i = 4
// "Yep !" caught, but not really handled.
// terminate called after throwing an instance of 'std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >'
// Abandon (core dumped)
try... catch
functional block#include <iostream>
#include <stdexcept>
int f(int i) try {
if(i > 2) throw std::overflow_error(std::to_string(i) + " > 2");
return i*i;
}
catch(std::exception& e) {
std::cout << "Exception caught : " << e.what() << std::endl;
return -1;
}
int main(int argc, char* argv[]) {
for(unsigned int i=0; i < 5; ++i) {
int k = 0;
k = f(i);
std::cout << i << '*' << i << " = " << k << std::endl;
}
return 0;
}
// Output:
// 0*0 = 0
// 1*1 = 1
// 2*2 = 4
// Exception caught : 3 > 2
// 3*3 = -1
// Exception caught : 4 > 2
// 4*4 = -1
The stack is cleaned when an exception is thrown… smart pointers benefit from this !
#include <iostream>
#include <vector>
#include <array>
#include <memory>
#include <num.hpp>
#define SIZE 3
auto range_ptr() {
num* res = new num[SIZE];
int i = 0;
auto it = res;
auto end = res + SIZE;
while(it != end) *(it++) = i++; return res;
}
auto range_vec() {
std::vector<num> res(SIZE);
int i = 0;
auto it = res.begin();
auto end = res.end();
while(it != end) *(it++) = i++;
return res;
}
auto range_smart() {
auto res = std::make_unique<std::array<num, SIZE>>();
int i = 0;
auto it = res->begin();
auto end = res->end();
while(it != end) *(it++) = i++;
return res;
}
void f() {throw std::string("ouch !");}
void g() {
fun_scope;
auto ptr = range_ptr();
auto vec = range_vec();
auto smart = range_smart();
___;
for(int i = 0; i < SIZE; ++i) {
if(i > 0) f();
std::cout << scope_indent << '(' << ptr[i] << ", " << vec[i] << ", " << (*smart)[i] << ')' << std::endl;
}
// Unreached code... ptr is a memory leak.
delete [] ptr;
}
int main(int argc, char* argv[]) {
fun_scope;
for(int i = 0; i < 2; ++i)
try {g();}
catch(...) {/* "Do nothing" handler. */}
return 0;
}
noexcept
syntactical elementC++ can optimize the code (i.e. not installing some exception checking) if is is told, by the programmer, that some function will not throw exception. The compiler then trusts the programmer and do not write extra exception handling stuff. The noexcept
qualifier tells that, in a similar way than the const
qualifiers notifies the compiler about constness. At compiling time, in case of templates, mainly, some code can be generated. The noexcept
status of some function may depend on the noexcept
status of the elements in it… Determining the noexcept
status when some code is generated is done by the noexcept
operator. So you will have to write code involving either the noexcept
qualifier, either the noexcept
operator… and often both ! It leads to expressions as noexcept(noexcept(...))
that sounds funny.
noexcept
qualifierThe noexcept
qualifier tells the compiler to base its compiling strategy considering that it is guaranted that no exceptions will be thrown by the function. If a function may throw an exception, write noexcept(false)
, it is the default value if nothing about noexcept is specified.
If you are sure that the function will not throw exception, write noexcept(true)
. Usually, programmers write only noexcept
, which means noexcept(true)
.
#include <iostream>
#include <string>
// The macro STATUS (true or false) has to be set at compiling time.
// g++ -std=c++20 -o test thisfile.cpp -DSTATUS=true
// g++ -std=c++20 -o test thisfile.cpp -DSTATUS=false
void lier_if_status_is_true() noexcept(STATUS) {
throw std::string("got you !");
}
// This function catches all exceptions. It could have been tagged
// with noexcept as well.
void catcher() {
try {
lier_if_status_is_true();
}
catch(...) {
std::cout << "Exception caught" << std::endl;
}
}
int main(int argc, char* argv[]) {
catcher();
return 0;
}
// Output when STATUS is true:
// terminate called after throwing an instance of 'std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >'
// Abandon (core dumped)
// and we had a warning at compiling time.
// Output when STATUS is false:
// Exception caught
Here, the status depends, at compiling time, to the value of the macro. Next section shows how to compute such a value, at compiling time as well.
noexcept
operatorThe noexcept(expr)
operator takes a syntactic expression expr
, at compiling time, and tells wether it will throw an exception, as far as the compiler knows. So noexcept(expr)
is either true
(if expr
is guarantied to not throw an exception) or false
(otherwise).
// Inspired from http://en.cppreference.com/w/cpp/language/noexcept
#include <iostream>
#include <vector>
#include <iomanip>
void may_throw () ;
void wont_throw() noexcept;
struct A {
void foo(int i) noexcept ;
void bar(int i) ;
A() = default;
A(const A&) = default;
A(A&&) noexcept(false);
};
// std::declval<A>() represents a value of type A in a unevaluated context (noexcept argument here).
int main() {
A a;
std::cout << std::boolalpha << std::left
<< "may_throw() = noexcept(" << std::setw(5) << noexcept(may_throw()) << ')' << std::endl // Output: may_throw() = noexcept(false)
<< "wont_throw() = noexcept(" << std::setw(5) << noexcept(wont_throw()) << ')' << std::endl // Output: wont_throw() = noexcept(true )
<< "a::foo() = noexcept(" << std::setw(5) << noexcept(std::declval<A>().foo(3)) << ')' << std::endl // Output: a::foo() = noexcept(true )
<< "a::bar() = noexcept(" << std::setw(5) << noexcept(std::declval<A>().bar(3)) << ')' << std::endl // Output: a::bar() = noexcept(false)
<< "A default = noexcept(" << std::setw(5) << noexcept(A()) << ')' << std::endl // Output: A default = noexcept(true )
<< "A cp = noexcept(" << std::setw(5) << noexcept(A(a)) << ')' << std::endl // Output: A cp = noexcept(true )
<< "A rvalue cp = noexcept(" << std::setw(5) << noexcept(A(std::declval<A>())) << ')' << std::endl; // Output: A rvalue cp = noexcept(false)
return 0;
}
Let us introduce a bit of templates to see how this is used in practical situations.
#include <iostream>
#include <iomanip>
int f(int x) noexcept(true) {return x*x;}
double f(double x) noexcept(false) {return 2*x / (x - 2.0);}
template<typename T> // We will use T as int or double... templates are introduced later.
T g(T x) noexcept(noexcept(f(std::declval<T>()))) {
return f(x) * (x + 1);
}
// g<int>(2) returns the int 12
// g<double>(3.0) returns 24.0
// The <type> can be omitted of the compiler can guess it from the arguments...
// g(2) <=> g<int>(2)
// g(2.0) <=> g<double>(2.0)
int main(int argc, char* argv[]) {
std::cout << std::boolalpha << noexcept(g(1)) << ", " << noexcept(g(1.0)) << std::endl;
// Output : true, false
return 0;
}