Guide pratique pour effectuer une interface entre du code exceptionnel et non exceptionnel
Cet article explique comment implémenter une gestion cohérente des exceptions dans le code C++ et comment traduire des exceptions vers et depuis des codes d’erreur aux limites d’exception.
Parfois, le code C++ doit s’interfacer avec du code qui n’utilise pas d’exceptions (code non exceptionnel). Une telle interface est appelée limite d’exception. Par exemple, vous pouvez appeler la fonction Win32 CreateFile
dans votre programme C++. CreateFile
ne lève pas d’exceptions. Au lieu de cela, il définit des codes d’erreur qui peuvent être récupérés par la GetLastError
fonction. Si votre programme C++ n’est pas trivial, vous préférez probablement avoir une stratégie cohérente de gestion des erreurs basée sur des exceptions. Et vous ne souhaitez probablement pas abandonner les exceptions simplement parce que vous interfacez avec du code non exceptionnel. Vous ne souhaitez pas non plus mélanger les stratégies d’erreur basées sur des exceptions et non basées sur des exceptions dans votre code C++.
Appeler des fonctions non exceptionnelles à partir de C++
Lorsque vous appelez une fonction non exceptionnelle depuis C++, l'idée est d'encapsuler cette fonction dans une fonction C++ qui détecte les erreurs et lève éventuellement une exception. Lorsque vous concevez une telle fonction wrapper, déterminez d’abord le type de garantie d’exception à fournir : noexcept, strong ou basic. Ensuite, créez la fonction de manière à ce que toutes les ressources, par exemple, les handles de fichiers, soient correctement libérées si une exception est levée. En règle générale, cela signifie que vous utilisez des pointeurs intelligents ou des gestionnaires de ressources similaires pour posséder les ressources. Pour plus d’informations sur les considérations relatives à la conception, consultez Guide pratique pour la conception pour la sécurité des exceptions.
Exemple
L'exemple suivant illustre les fonctions C++ qui utilisent les fonctions Win32 CreateFile
et ReadFile
en interne pour ouvrir et lire deux fichiers. La classe File
est un wrapper RAII (Resource Acquisition Is Initialization) pour les handles de fichiers. Son constructeur détecte une condition « fichier introuvable » et lève une exception pour propager l’erreur dans la pile des appels de l’exécutable C++ (dans cet exemple, la main()
fonction). Si une exception est levée après qu’un objet File
est entièrement construit, le destructeur appelle automatiquement CloseHandle
pour libérer le handle de fichiers. (Si vous préférez, vous pouvez utiliser la classe ATL (Active Template Library) CHandle
à cet effet ou une unique_ptr
fonction de suppression personnalisée.) Les fonctions qui appellent les API Win32 et CRT détectent les erreurs, puis lèvent des exceptions C++ à l’aide de la fonction définie ThrowLastErrorIf
localement, qui utilise à son tour la Win32Exception
classe dérivée de la runtime_error
classe. Toutes les fonctions de cet exemple fournissent une garantie d’exception forte : si une exception est levée à tout moment dans ces fonctions, aucune ressource n’est divulguée et aucun état du programme n’est modifié.
// compile with: /EHsc
#include <Windows.h>
#include <stdlib.h>
#include <vector>
#include <iostream>
#include <string>
#include <limits>
#include <stdexcept>
using namespace std;
string FormatErrorMessage(DWORD error, const string& msg)
{
static const int BUFFERLENGTH = 1024;
vector<char> buf(BUFFERLENGTH);
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM, 0, error, 0, buf.data(),
BUFFERLENGTH - 1, 0);
return string(buf.data()) + " (" + msg + ")";
}
class Win32Exception : public runtime_error
{
private:
DWORD m_error;
public:
Win32Exception(DWORD error, const string& msg)
: runtime_error(FormatErrorMessage(error, msg)), m_error(error) { }
DWORD GetErrorCode() const { return m_error; }
};
void ThrowLastErrorIf(bool expression, const string& msg)
{
if (expression) {
throw Win32Exception(GetLastError(), msg);
}
}
class File
{
private:
HANDLE m_handle;
// Declared but not defined, to avoid double closing.
File& operator=(const File&);
File(const File&);
public:
explicit File(const string& filename)
{
m_handle = CreateFileA(filename.c_str(), GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, nullptr);
ThrowLastErrorIf(m_handle == INVALID_HANDLE_VALUE,
"CreateFile call failed on file named " + filename);
}
~File() { CloseHandle(m_handle); }
HANDLE GetHandle() { return m_handle; }
};
size_t GetFileSizeSafe(const string& filename)
{
File fobj(filename);
LARGE_INTEGER filesize;
BOOL result = GetFileSizeEx(fobj.GetHandle(), &filesize);
ThrowLastErrorIf(result == FALSE, "GetFileSizeEx failed: " + filename);
if (filesize.QuadPart < (numeric_limits<size_t>::max)()) {
return filesize.QuadPart;
} else {
throw;
}
}
vector<char> ReadFileVector(const string& filename)
{
File fobj(filename);
size_t filesize = GetFileSizeSafe(filename);
DWORD bytesRead = 0;
vector<char> readbuffer(filesize);
BOOL result = ReadFile(fobj.GetHandle(), readbuffer.data(), readbuffer.size(),
&bytesRead, nullptr);
ThrowLastErrorIf(result == FALSE, "ReadFile failed: " + filename);
cout << filename << " file size: " << filesize << ", bytesRead: "
<< bytesRead << endl;
return readbuffer;
}
bool IsFileDiff(const string& filename1, const string& filename2)
{
return ReadFileVector(filename1) != ReadFileVector(filename2);
}
#include <iomanip>
int main ( int argc, char* argv[] )
{
string filename1("file1.txt");
string filename2("file2.txt");
try
{
if(argc > 2) {
filename1 = argv[1];
filename2 = argv[2];
}
cout << "Using file names " << filename1 << " and " << filename2 << endl;
if (IsFileDiff(filename1, filename2)) {
cout << "+++ Files are different." << endl;
} else {
cout<< "=== Files match." << endl;
}
}
catch(const Win32Exception& e)
{
ios state(nullptr);
state.copyfmt(cout);
cout << e.what() << endl;
cout << "Error code: 0x" << hex << uppercase << setw(8) << setfill('0')
<< e.GetErrorCode() << endl;
cout.copyfmt(state); // restore previous formatting
}
}
Appeler du code exceptionnel à partir d’un code non exceptionnel
Fonctions C++ déclarées comme extern "C"
pouvant être appelées par les programmes C. Les serveurs COM C++ peuvent être consommés par du code écrit dans un nombre quelconque de langages différents. Lorsque vous implémentez des fonctions publiques prenant en charge les exceptions en C++ pour qu'elles soient appelées par du code non exceptionnel, la fonction C++ ne doit pas autoriser les exceptions à se propager à l'appelant. Ces appelants n’ont aucun moyen d’intercepter ou de gérer des exceptions C++. Le programme peut arrêter, fuiter des ressources ou provoquer un comportement non défini.
Nous recommandons à votre extern "C"
fonction C++ d’intercepter spécifiquement chaque exception qu’elle sait gérer et, le cas échéant, de convertir l’exception en code d’erreur que l’appelant comprend. Si toutes les exceptions potentielles ne sont pas connues, la fonction C++ doit avoir un bloc catch(...)
comme dernier gestionnaire. Dans ce cas, il est préférable de signaler une erreur irrécupérable à l’appelant, car votre programme peut être dans un état inconnu et irrécupérable.
L’exemple suivant montre une fonction qui suppose que toute exception susceptible d’être levée est un Win32Exception
ou un type d’exception dérivé de std::exception
. La fonction intercepte toute exception de ces types et propage les informations d'erreur sous la forme d'un code d'erreur Win32 à l'appelant.
BOOL DiffFiles2(const string& file1, const string& file2)
{
try
{
File f1(file1);
File f2(file2);
if (IsTextFileDiff(f1, f2))
{
SetLastError(MY_APPLICATION_ERROR_FILE_MISMATCH);
return FALSE;
}
return TRUE;
}
catch(Win32Exception& e)
{
SetLastError(e.GetErrorCode());
}
catch(std::exception& e)
{
SetLastError(MY_APPLICATION_GENERAL_ERROR);
}
return FALSE;
}
Lorsque vous convertissez des exceptions en codes d’erreur, il existe un problème potentiel : les codes d’erreur ne contiennent souvent pas la richesse des informations qu’une exception peut stocker. Pour résoudre ce problème, vous pouvez fournir un catch
bloc pour chaque type d’exception spécifique qui peut être levée et effectuer la journalisation pour enregistrer les détails de l’exception avant qu’elle ne soit convertie en code d’erreur. Cette approche peut créer du code répétitif si plusieurs fonctions utilisent tous le même ensemble de catch
blocs. Une bonne façon d’éviter la répétition du code consiste à refactoriser ces blocs en une fonction utilitaire privée qui implémente les try
et blocs et catch
accepte un objet de fonction appelé dans le try
bloc. Dans chaque fonction publique, passez le code à la fonction utilitaire en tant qu'expression lambda.
template<typename Func>
bool Win32ExceptionBoundary(Func&& f)
{
try
{
return f();
}
catch(Win32Exception& e)
{
SetLastError(e.GetErrorCode());
}
catch(const std::exception& e)
{
SetLastError(MY_APPLICATION_GENERAL_ERROR);
}
return false;
}
L’exemple suivant montre comment écrire l’expression lambda qui définit le foncteur. Une expression lambda est souvent plus facile à lire en ligne que le code qui appelle un objet de fonction nommé.
bool DiffFiles3(const string& file1, const string& file2)
{
return Win32ExceptionBoundary([&]() -> bool
{
File f1(file1);
File f2(file2);
if (IsTextFileDiff(f1, f2))
{
SetLastError(MY_APPLICATION_ERROR_FILE_MISMATCH);
return false;
}
return true;
});
}
Pour plus d’informations sur les expressions lambda, voir Expressions lambda.
Appeler du code exceptionnel par le biais d’un code non exceptionnel à partir d’un code exceptionnel
Il est possible, mais pas recommandé, de lever des exceptions dans le code sans connaissance des exceptions. Par exemple, votre programme C++ peut appeler une bibliothèque qui utilise des fonctions de rappel que vous fournissez. Dans certaines circonstances, vous pouvez lever des exceptions des fonctions de rappel dans le code non exceptionnel que votre appelant d’origine peut gérer. Toutefois, les circonstances dans lesquelles les exceptions peuvent fonctionner correctement sont strictes. Vous devez compiler le code de la bibliothèque de manière à conserver la sémantique de déroulement de la pile. Le code sans connaissance de l’exception ne peut rien faire qui peut intercepter l’exception C++. En outre, le code de bibliothèque entre l’appelant et le rappel ne peut pas allouer de ressources locales. Par exemple, le code qui ne prend pas en charge les exceptions ne peut pas avoir de locaux qui pointent vers la mémoire du tas allouée. Ces ressources sont divulguées lorsque la pile est déwound.
Ces exigences doivent être remplies pour lever des exceptions dans le code non prenant en charge les exceptions :
- Vous pouvez générer l’intégralité du chemin de code dans le code non prenant en charge les exceptions à l’aide
/EHs
de , - Il n’y a pas de ressources allouées localement qui peuvent être divulguées lorsque la pile est déwound,
- Le code n’a
__except
pas de gestionnaires d’exceptions structurées qui interceptent toutes les exceptions.
Étant donné que la levée d’exceptions sur un code non exceptionnel est sujette à des erreurs et peut entraîner des problèmes de débogage difficiles, nous ne le recommandons pas.
Voir aussi
Meilleures pratiques C++ modernes pour la gestion des exceptions et des erreurs
Guide pratique pour la conception pour la sécurité des exceptions
Commentaires
https://aka.ms/ContentUserFeedback.
Bientôt disponible : Tout au long de 2024, nous allons supprimer progressivement GitHub Issues comme mécanisme de commentaires pour le contenu et le remplacer par un nouveau système de commentaires. Pour plus d’informations, consultezEnvoyer et afficher des commentaires pour