8. Exceptions

Chapitre 8 : exceptions #

📝 À NOTER

Absence de RAII / exception safety #

Ce chapitre se concentre sur le mécanisme des exceptions en C++. Pour être complètement sûr, il faut le coupler à RAII (Resource Acquisition Is Initialization) qui sera vu dans un autre chapitre.

std::exception_ptr, std::current_exception, std::rethrow_exception, std::nested_exception #

Utiles pour du multithreading, stocker des exceptions, wrapper des exceptions, etc. Ils sont hors scope pour ce cours.

Diagramme de classes #

Dans le namespace std, la classe de base pour les exceptions est exception :

PlantUML diagram

Slides #

Version imprimable (faire CTRL+P)

Exemples #

1242.2_08.01_ErrorCodesReturnValue

main.cpp

#include <print>
// Error handling using return codes — the traditional approach before exceptions.
// The return value of the function is used both for the result AND for error signaling,
// which makes the code harder to read and maintain.

#define ERROR_CODE -1

static int oneOverN(int n, double &result)
{
  if (n == 0)
  {
    return ERROR_CODE;
  }
  else
  {
    result = 1.0 / n;
    return 0;
  }
}

static double compute(int k)
{
  double result;
  if (oneOverN(k, result) == ERROR_CODE)
  {
    // Ambiguous error signaling: ERROR_CODE could be a valid result
    return ERROR_CODE;
  }
  else
  {
    return result * result;
  }
}

int main()
{
  // Problem: ERROR_CODE could be a valid result — ambiguous error signaling
  auto result = compute(0);
  if (result == ERROR_CODE)
  {
    std::println("ERROR");
  }
  else
  {
    std::println("{}", result);
  }

  return 0;
}
1242.2_08.02_BasicExceptionHandling

main.cpp

#include <print>
// Same logic as 08.01 but using exceptions instead of return codes.
// The exception propagates automatically through the call stack — no need
// for each intermediate function to check and forward error codes.

static double oneOverN(int n)
{
    if (n == 0)
    {
        throw n;
    }
    return 1.0 / n;
}

static double compute(int k)
{
    auto result = oneOverN(k);
    return result * result;
}

int main()
{
    try
    {
        auto result = compute(2);
        std::println("{}", result);
    }
    catch (...)
    {
        std::println("ERROR");
    }

    return 0;
}
1242.2_08.03_ExceptionTypeMatching

main.cpp

#include <print>
// Catch handlers with value parameters do NOT perform implicit type conversions.
// A thrown double will NOT be caught by a catch(float) handler.

int main()
{
    int dividend = 10;
    int divisor = 0;

    try
    {
        if (divisor == 0)
        {
            throw 0.0; // throws a double
            // try also: throw "division by zero!";  // const char*
            // try also: throw 0.0f;                 // float
        }
        std::println("Quotient: {}", dividend / divisor);
    }
    catch (float f)
    {
        // Will NOT catch the double — no implicit conversion in catch handlers
        std::println("Caught float: {}", f);
    }
    catch (const char* msg)
    {
        std::println("Caught message: {}", msg);
    }
    catch (...)
    {
        // The double ends up here because no exact-type handler matched
        std::println("Caught unknown exception (catch-all)");
    }

    std::println("Done");

    return 0;
}
1242.2_08.04_ExceptionRethrowing

main.cpp

#include <print>
// An exception can be caught, partially handled, and re-thrown with a bare `throw`.
// This allows multiple levels of the call stack to react to the same exception.

void f()
{
    try
    {
        int n = 2;
        throw n;
    }
    catch (int x)
    {
        std::println("Exception caught in f(): {}", x);
        throw; // re-throw the same exception to the caller
    }
}

int main()
{
    try
    {
        f();
    }
    catch (int i)
    {
        std::println("Exception caught in main() (int): {}", i);
    }
    catch (double d)
    {
        std::println("Exception caught in main() (double): {}", d);
    }

    return 0;
}
1242.2_08.05_NoexceptSpecification

main.cpp

#include <type_traits>
#include <print>
#include <stdexcept>
// In C++17, dynamic exception specifications (throw(int, float, ...)) were removed.
// The only remaining specification is `noexcept`, which guarantees that a function
// will not throw. If a noexcept function does throw, std::terminate is called.

// Most standard library containers rely heavily on noexcept for performance optimizations.
// If a move constructor is noexcept, std::vector will use it instead of the copy constructor during resizing

// This function promises not to throw
void safeFunction() noexcept
{
  std::println("safeFunction: I promise not to throw");
}

// This function may throw (default behavior, same as noexcept(false))
void riskyFunction()
{
  throw std::runtime_error("something went wrong");
}

// noexcept can also be conditional
template <typename T>
void process(T value) noexcept(std::is_integral_v<T>)
{
  if constexpr (std::is_integral_v<T>)
  {
    std::println("Processing integral value: {} (noexcept)", value);
  }
  else
  {
    std::println("Processing non-integral value: {} (may throw)", value);
    throw std::runtime_error("non-integral error");
  }
}

int main()
{
  // noexcept can be queried at compile time
  std::println("safeFunction is noexcept: {}", noexcept(safeFunction()));
  std::println("riskyFunction is noexcept: {}", noexcept(riskyFunction()));
  std::println("process<int> is noexcept: {}", noexcept(process(42)));
  std::println("process<double> is noexcept: {}", noexcept(process(3.14)));

  safeFunction();

  try
  {
    riskyFunction();
  }
  catch (const std::exception &e)
  {
    std::println("Caught: {}", e.what());
  }

  // Safe: int is integral → noexcept(true)
  process(42);

  // This would throw → catch it
  try
  {
    process(3.14);
  }
  catch (const std::exception &e)
  {
    std::println("Caught from process<double>: {}", e.what());
  }

  // WARNING: calling safeFunction with a throw inside would call std::terminate!
  // Uncomment to test (program will abort):
  // auto dangerous = []() noexcept { throw std::runtime_error("oops"); };
  // dangerous(); // → std::terminate()

  return 0;
}

📝 À NOTER
Ă€ regarder uniquement après avoir fait l’exercice 1 de la sĂ©rie 8.2 (ExceptionVector).
1242.2_08.06_VectorBoundsException

main.cpp

#include <iostream>
#include <print>
#include <new>
// A simple Vector class that demonstrates both implicit exceptions (bad_alloc
// from new[]) and explicit exceptions (bounds checking with const char*).

class Vector
{
public:
  explicit Vector(int size) : m_size(size)
  {
    m_data = new double[m_size]; // may throw std::bad_alloc
  }

  ~Vector()
  {
    delete[] m_data;
    m_data = nullptr;
  }

  Vector(const Vector&) = delete;
  Vector& operator=(const Vector&) = delete;

  double &operator[](int i)
  {
    validateIndex(i);
    return m_data[i];
  }

private:
  void validateIndex(int i) const
  {
    if (i < 0 || i >= m_size)
    {
      // In real code, prefer throwing std::out_of_range
      throw "Index out of bounds!";
    }
  }

  double *m_data{nullptr};
  int m_size{0};
};

int main()
{
  int n = 0;
  int i = 0;
  double x = 0.0;

  std::print("Number of vector components: n = ");
  std::cin >> n;

  try
  {
    Vector v(n); // if n is too large → bad_alloc
    std::print("Index and value of a component: i, v[i] ? ");
    std::cin >> i >> x;
    v[i] = x; // if i is out of [0..n-1] → throws const char*
    std::println("v[{}] = {}", i, x);
  }
  catch (const char *message)
  {
    std::println("{}", message);
  }
  catch (const std::bad_alloc &)
  {
    std::println("Memory allocation failed");
  }
  catch (...)
  {
    std::println("Unknown error");
  }

  std::println("Done");

  return 0;
}

1242.2_08.07_CatchByReference

main.cpp

#include <print>
#include <exception>
// Catching exceptions by reference preserves polymorphism.
// Catching by value causes slicing — the derived part is lost.

class Exception1 : public std::exception
{
public:
    const char* what() const noexcept override
    {
        return "Exception1";
    }
};

class Exception2 : public std::exception
{
public:
    const char* what() const noexcept override
    {
        return "Exception2";
    }
};

int main()
{
    // Catch by REFERENCE: polymorphism works — calls Exception1::what()
    try
    {
        std::println("--- Catch by reference ---");
        throw Exception1();
    }
    catch (const std::exception& e)
    {
        std::println("Caught: {}", e.what()); // prints "Exception1"
    }

    // Catch by VALUE: slicing occurs — calls std::exception::what()
    try
    {
        std::println("--- Catch by value ---");
        throw Exception2();
    }
    catch (const std::exception e) // NOLINT: intentional catch by value
    {
        std::println("Caught: {}", e.what()); // prints "std::exception" (sliced!)
    }

    return 0;
}
1242.2_08.08_CustomExceptionClass

main.cpp

#include <print>
#include <format>
#include <exception>
#include <string>
// A custom exception class that inherits from std::exception.
// Uses __LINE__ to include the source line number in the error message.

class MyException : public std::exception
{
public:
    MyException(const char* message, int line)
        : m_msg(std::format("Error at line {}: {}", line, message))
    {
    }

    const char* what() const noexcept override
    {
        return m_msg.c_str();
    }

private:
    std::string m_msg;
};

int main()
{
    try
    {
        throw MyException("something went wrong", __LINE__);
    }
    catch (const std::exception& e)
    {
        std::println("{}", e.what());
    }

    return 0;
}
1242.2_08.09_IntegerExceptions

main.cpp

#include <print>

// WARNING: this is for pedagogical purposes only!
//  --> Throwing integral types is a bad practice.
//  --> Throw exceptions inheriting from std::exception
int main(int argc, char *argv[])
{
  if (argc < 2)
  {
    std::println("Usage: 1242.2_08.09_IntegralExceptionsVsClassesExceptions.exe <exception_type>");
    std::println("Exception types: char, short, int, long");

    return 1;
  }

  try
  {
    auto exceptionTypeStr = std::string_view(argv[1]);
    if (exceptionTypeStr == "char")
    {
      throw '0';
    }
    else if (exceptionTypeStr == "short")
    {
      throw static_cast<short>(1);
    }
    else if (exceptionTypeStr == "int")
    {
      throw 2;
    }
    else if (exceptionTypeStr == "long")
    {
      throw 3L;
    }
  }
  catch (char c)
  {
    std::println("Caught char exception: '{}'", c);
  }
  catch (short s)
  {
    std::println("Caught short exception: {}", s);
  }
  catch (int i)
  {
    std::println("Caught int exception: {}", i);
  }
  catch (long l)
  {
    std::println("Caught long exception: {}", l);
  }

  return 0;
}

Serie 8.1 #

Exercice 1 #

Le programme ci-dessous teste la validitĂ© d’une valeur saisie au clavier, qui doit ĂŞtre positive, impaire et comprise entre deux valeurs. Les erreurs sont transmises en levant des exceptions.

📝 À NOTER

Pour les besoins des exercices, il est demandĂ© d’utiliser des exceptions pour signaler des erreurs “communes”. Dans la pratique, il est important de faire la distinction entre les erreurs “communes” (ex: valeur saisie par l’utilisateur invalide) et les erreurs “exceptionnelles” (ex: Ă©chec d’une allocation mĂ©moire).

  • Les erreurs communes peuvent ĂŞtre gĂ©rĂ©es par des mĂ©canismes de contrĂ´le de flux
  • Les erreurs exceptionnelles doivent ĂŞtre gĂ©rĂ©es par des exceptions
int main()
{
  auto minValue = 10;
  auto maxValue = 100;
  auto value = 0;

  std::println("Enter a positive and odd value [{}-{}]: ", minValue, maxValue);
  std::cin >> value;
  
  isPositive(value);
  isOdd(value);
  isLessThan(value, maxValue);
  isGreaterThan(value, minValue);
  
  std::println("Correct value !\n");
  
  return 0;
}

Écrire les fonctions suivantes :

void isPositive(int value)

  • si la valeur est positive, afficher " - OK: It’s a positive value "
  • sinon, lever une exception qui envoie la valeur (value).

void isOdd(int value)

  • si la valeur est impaire, afficher " - OK: It’s an odd value"
  • sinon, lever une exception et envoyer la chaine de caractères: “The value is even”.

void isLessThan(int value, int maxValue)

  • si la valeur est plus petite que maxValue, afficher “- OK: It’s a value less than 100” si maxValue vaut 100 par exemple
  • sinon, lever une exception qui envoie une instance de la classe MyException qui hĂ©rite de la classe C++ exception (#include <exception>)
    • Ă©crire un constructeur de cette classe qui puisse recevoir le message d’erreur : “The value is too big”
    • redĂ©finir la mĂ©thode what() afin qu’elle affiche le message passĂ© au constructeur

void isGreaterThan(int value, int minValue)

  • idem que pour le cas prĂ©cĂ©dent

Dans le main, il s’agit de disposer des try et catch aux bons endroits.

Exemples #

Enter a positive and odd value [10, 100]: 11
- OK: It's a positive value
- OK: It's an odd value
- OK: It's a value less than 100
- OK: It's a value greater than 10
Correct value !
Enter a positive and odd value [10, 100]: 12
- OK: It's a positive value
Incorrect value ! ->  The value is even  
Enter a positive and odd value [10, 100]: 3
- OK: It's a positive value
- OK: It's an odd value
- OK: It's a value less than 100
Incorrect value ! ->  The value is too small

Serie 8.2 #

Exercice 1 #

Reprendre la classe Vector de la série 3.2 et y ajouter le traitement des exceptions susceptibles d’être levées.

Supposons la fonction main() suivante :

int main()
{
  const int SIZE = 2147483647;
  
  // A) Test de l'allocation dans le constructeur  Vector(int, int)
  Vector v1(SIZE);
  
  // B) Test de l'allocation dans l'opérateur d'assignement 
  for()   ...
  tabV[i] = v1;
    ...
    
  // C) Afficher la taille des vecteurs du tableau
  ...
  
  // D) Test de l'allocation dans le constructeur
  Vector v2(SIZE);

  // E) Test de l'allocation dans le constructeur par recopie
  Vector v3 = v1; 

  // F) Test de l'allocation dans l'opérateur d'affectation
  Vector v5;
  v5 = v1;

  // G) Test du constructeur Vector(int, double) avec une taille incorrecte
  Vector v6(-1, 0.0);

  // H) Test de l'accès hors limites à un vecteur
  v1[-1] = 5;

  // I) Test de l'accès hors limites à un std::vector
  // NOTE: std::vector::at() takes a size_t index
  // --> Passing -1 causes an implicit conversion to a very large unsigned value
  std::println("{}", tabV.at(-1).getSizeInBytes());

  return 0;
}

Des exceptions peuvent se produire dans les situations suivantes :

A) Lors de la création d’un objet de type Vector(int, double)

B) Lors de l’affectation d’un vecteur Ă  un autre vecteur :

  • construire un std::vector contenant 10 vecteurs de type Vector, puis copiez le vecteur v1 dans ce tableau afin de voir apparaitre une exception.

C) Afficher la taille des vecteurs du tableau

D) Lors de la création d’un objet de type Vector(int, double)

E) Lors de la création d’un objet de type Vector(const Vector&)

F) Lors de l’affectation d’un vecteur Ă  un autre vecteur

H) Lors d’un accès hors limite Ă  un vecteur

I) Lors d’un accès hors limite Ă  un std::vector avec l’opĂ©rateur at()

⚠️ ATTENTION
  • Il ne faut pas mettre l’ensemble du programme dans un try
  • En cas d’erreur d’allocation d’un Vecteur, il faut s’assurer celui-ci est remis en ordre et le programme main() doit continuer.