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

Table of contents

Exceptions

Are exceptions really worth it ?

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.

Handling exceptions

The try... catch block

When a code is likely to trigger an exception, we surround it with a try {...} expression. The catches 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)

The 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

Memory handling

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;
}

The noexcept syntactical element

C++ 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.

The noexcept qualifier

The 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.

The noexcept operator

The 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;
}
				
Hervé Frezza-Buet,