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 /EHsde ,
  • 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