Programming in C++

Exceptions

Gerald Senarclens de Grancy

Exceptions

Allow to pass errors back to the caller without returning error codes

They permit to decouple error handling from the regular control flow

Exceptions deal with anticipated, but uncommon cases, eg.:

  • Files that are not readable/ writable
  • A required server being unavailable
  • ...

If something should never happen, use assertions instead

Exceptions

If a function cannot reasonably deal with an error

  • It's caller has a chance to catch the error
  • It's caller’s caller has a chance to catch the error
  • ...
  • main(.) has a chance to catch the error
  • If an error is not caught, the program terminates (std::terminate()) with an exception error

Purpose of Exceptions

When recovering from an error is not straight forward in the current code unit

  • Exceptions allow splitting up error detection and error handling
  • Can avoid fatal errors (eg. std::exit(ERROR_CODE) is not user friendly)
  • Returning error codes might obscure a function's actual intent
    return does not work in constructors
  • Exceptions avoid having to constantly check for errors
  • They hence can facilitate understanding the intent of the code

Disadvantages of C++ Exceptions

Performance Overhead
Expensive in terms of runtime, especially when used frequently
Hard to Get Right
Exceptions make the control flow harder to evaluate
Exception safety requires both RAII and specific coding practices
Careless throwning or recovering from them may lead to leaks
Potential for Misuse
Use exceptions only for error handling!
Eg. invalid user input should not cause exceptions to be thrown

See eg. Google C++ Style Guide

Avoid unhandled exception at all costs

Syntax

Throwing an Exception

If a function cannot deal with a problem, it may use the throw statement

This is also commonly called "raising an exception"

throw std::runtime_error("Description of what went wrong.");

You may throw

  • numeric or string literals
  • instances of any class (possibly derived form std::exception)
  • any of the standard exceptions except std::exception
    (see cppreference for a list of available exceptions)

Catching an Exception

In a function directly or indirectly calling another function that uses throw

try {  // observe if any of the following statements throw an exception
  // Code that might throw an exception
} catch ( const MostSpecificException& e ) {
  // handle custom exception
} catch ( const LessSpecificException& e ) {
  // handle custom exception
} catch ( const std::exception& e ) {
  // handle all other standard exceptions
} catch ( ... ) {
  std::cerr << e.what() << std::endl;
  // Handle everything else (catch-all handler)
}

Defining C++ Exceptions

Deriving from std::exception ensures a consistent interface

class YourException : public std::exception {
public:
  const char* what() const noexcept {
    return "My exception occurred!";
  }
};

noexcept specifier

Functions are either non-throwing or potentially throwing

Functions can be defined as non-throwing using the noexcept specifier

void f() noexcept;
void f() noexcept {
  // ...
}

noexcept is not enforced at compile time

Related Guidelines

noexcept specifier

E.12
Use noexcept when exiting a function because of throw is impossible [...]
F.6
If your function must not throw, declare it noexcept
C.86
Make == symmetric with respect to operand types and noexcept

struct S {
  uint64_t id;
  double value;
};

bool operator==(const S& a, const S& b) noexcept {
  return a.name == b.name && a.number == b.number;
}

Example: main(.) Catching All Exceptions

#include <iostream>
void do_something() {
  // code, that deep in the call stack might throw an unexpected exception
  throw 1;
}

int main() {
  // imagine a local variable that needs to be cleaned up
  try {
    do_something();
  }
  catch(...) {
    std::cerr << "Abnormal termination\n";  // shouldn't happen
  }
  // now we can be certain that stack unwinding takes place
  return 0;
}

Visualize execution

To help your imagination ...

#include <iostream>

class C {
public:
  C() { std::cout << "creating a temporary file (just pretend)" << std::endl; }
  ~C() { std::cout << "removing a temporary file (just pretend)" << std::endl; }
};

void do_something() {
  // code, that deep in the call stack might throw an unexpected exception
  throw 1;
}

int main() {
  C c;
  try {
    do_something();
  }
  catch(...) {
    std::cerr << "Abnormal termination\n";  // shouldn't happen
  }
  // now we can be certain that stack unwinding takes place
  return 0;
}

Visualize execution

Instead, with uncaught exceptions (which should really be avoided) ...

#include <iostream>

class C {
public:
  C() { std::cout << "creating a temporary file (just pretend)" << std::endl; }
  ~C() { std::cout << "removing a temporary file (just pretend)" << std::endl; }
};

void do_something() {
  // code, that deep in the call stack might throw an unexpected exception
  throw 1;
}

int main() {
  C c;
  do_something();
  // orderly stack unwinding is not guaranteed (depends on implementation)
  return 0;
}

Visualize execution

Example: File I/O Error

#include <fstream>
#include <iostream>
#include <stdexcept>

void read_file(const std::string& filename) {
  std::ifstream file(filename);
  if (!file.is_open()) {
    throw std::runtime_error("Failed to open file: " + filename);
  }
  // ...
}

int main() {
  try {
    read_file("file_io_error.cpp"); // file might exist
    read_file("missing_file"); // exception is thrown
  } catch ( const std::runtime_error& e ) {
    std::cout << e.what() << std::endl;
  } catch ( ... ) {
    std::cout << "something terrible happened" << std::endl;
  }
  return 0;
}
Download file_io_error.cpp

Example: Working with Designated Exception

#include <iostream>
#include <sstream>
#include <string>

class InvalidCircleException : std::exception {
public:
  InvalidCircleException(double radius) : message_{create_message(radius)} {};
  const char* what() const noexcept override {
    return message_.c_str();
  }
private:
  std::string create_message(double radius) {
    std::stringstream message;
    message << "A circle must not have a negative radius (";
    message << radius << ")!";
    return message.str();
  }
  std::string message_;
};

class Circle {
public:
  Circle(double x, double y, double _radius) : x_{x}, y_{y} { radius(_radius); }
  double x() const { return x_; }
  double y() const { return y_; }
  double radius() const { return radius_; }
  void radius(double radius) {
    if (radius < 0) {
        throw InvalidCircleException(radius);
    }
    radius_ = radius;
  }
private:
  double x_{0.0};
  double y_{0.0};
  double radius_{0.0};
};

std::ostream& operator<<(std::ostream& out, const Circle& c) {
  out << "Circle(x=" << c.x() << ", y=" << c.y() << ", radius=" << c.radius();
  out << ")";
  return out;
}

int main() {
  try {
    Circle a{25, 15, 7};  // ok
    std::cout << a << std::endl;
    Circle invalid{25, 15, -7};  // throws
    std::cout << invalid << std::endl;
  } catch ( const InvalidCircleException& e ) {
    std::cout << e.what() << std::endl;
  } catch ( ... ) {
    std::cout << "something terrible happened" << std::endl;
  }
  return 0;
}
Download invalid_circle.cpp

Questions
and feedback...