Chapitre 8 : exceptions #
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 :
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;
}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.
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” simaxValuevaut 100 par exemple - sinon, lever une exception qui envoie une instance de la classe
MyExceptionqui 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 smallSerie 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::vectorcontenant 10 vecteurs de typeVector, puis copiez le vecteurv1dans 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()
- 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.