Gestion des erreurs dans COM (Prise en main de Win32 et C++)

COM utilise des valeurs HRESULT pour indiquer la réussite ou l’échec d’un appel de méthode ou de fonction. Différents en-têtes du SDK définissent différentes constantes HRESULT . Un ensemble commun de codes à l’échelle du système est défini dans WinError.h. Le tableau suivant présente certains de ces codes de retour à l’échelle du système.

Constante Valeur numérique Description
E_ACCESSDENIED 0x80070005 Accès refusé.
E_FAIL 0x80004005 Erreur non spécifiée.
E_INVALIDARG 0x80070057 Valeur de paramètre non valide.
E_OUTOFMEMORY 0x8007000E Mémoire insuffisante.
E_POINTER 0x80004003 La valeur NULL a été transmise de manière incorrecte pour une valeur de pointeur.
E_UNEXPECTED 0x8000FFFF Condition inattendue.
S_OK 0x0 Opération réussie.
S_FALSE 0x1 Opération réussie.

 

Toutes les constantes avec le préfixe « E_ » sont des codes d’erreur. Les constantes S_OK et S_FALSE sont des codes de réussite. Probablement 99 % des méthodes COM retournent S_OK quand elles réussissent ; mais ne laissez pas ce fait vous induire en erreur. Une méthode peut retourner d’autres codes de réussite. Par conséquent, testez toujours les erreurs à l’aide de la macro SUCCEEDED ou FAILED . L’exemple de code suivant montre la mauvaise façon et la bonne façon de tester la réussite d’un appel de fonction.

// Wrong.
HRESULT hr = SomeFunction();
if (hr != S_OK)
{
    printf("Error!\n"); // Bad. hr might be another success code.
}

// Right.
HRESULT hr = SomeFunction();
if (FAILED(hr))
{
    printf("Error!\n"); 
}

Le code de réussite S_FALSE mérite mention. Certaines méthodes utilisent S_FALSE pour signifier, à peu près, une condition négative qui n’est pas un échec. Elle peut également indiquer un « no-op » (la méthode a réussi, mais n’a eu aucun effet). Par exemple, la fonction CoInitializeEx retourne S_FALSE si vous l’appelez une deuxième fois à partir du même thread. Si vous devez faire la différence entre S_OK et S_FALSE dans votre code, vous devez tester la valeur directement, tout en utilisant FAILED ou SUCCEEDED pour gérer les cas restants, comme indiqué dans l’exemple de code suivant.

if (hr == S_FALSE)
{
    // Handle special case.
}
else if (SUCCEEDED(hr))
{
    // Handle general success case.
}
else
{
    // Handle errors.
    printf("Error!\n"); 
}

Certaines valeurs HRESULT sont spécifiques à une fonctionnalité ou un sous-système particulier de Windows. Par exemple, l’API graphique Direct2D définit le code d’erreur D2DERR_UNSUPPORTED_PIXEL_FORMAT, ce qui signifie que le programme a utilisé un format de pixel non pris en charge. La documentation MSDN fournit souvent une liste de codes d’erreur spécifiques qu’une méthode peut retourner. Toutefois, vous ne devez pas considérer ces listes comme définitives. Une méthode peut toujours retourner une valeur HRESULT qui n’est pas répertoriée dans la documentation. Là encore, utilisez les macros SUCCEEDED et FAILED . Si vous testez un code d’erreur spécifique, incluez également un cas par défaut.

if (hr == D2DERR_UNSUPPORTED_PIXEL_FORMAT)
{
    // Handle the specific case of an unsupported pixel format.
}
else if (FAILED(hr))
{
    // Handle other errors.
}

Modèles de gestion des erreurs

Cette section examine certains modèles de gestion des erreurs COM de manière structurée. Chaque modèle présente des avantages et des inconvénients. Dans une certaine mesure, le choix est une question de goût. Si vous travaillez sur un projet existant, il a peut-être déjà des instructions de codage qui interdisent un style particulier. Quel que soit le modèle que vous adoptez, un code robuste obéit aux règles suivantes.

  • Pour chaque méthode ou fonction qui retourne un HRESULT, case activée la valeur renvoyée avant de continuer.
  • Libérez les ressources une fois qu’elles sont utilisées.
  • N’essayez pas d’accéder à des ressources non valides ou non initialisées, telles que des pointeurs NULL .
  • N’essayez pas d’utiliser une ressource après l’avoir mise en production.

Avec ces règles à l’esprit, voici quatre modèles de gestion des erreurs.

Ifs imbriqués

Après chaque appel qui retourne un HRESULT, utilisez une instruction if pour tester la réussite. Ensuite, placez l’appel de méthode suivant dans l’étendue de l’instruction if . Plus si les instructions peuvent être imbriquées aussi profondément que nécessaire. Les exemples de code précédents de ce module ont tous utilisé ce modèle, mais ici, il est à nouveau :

HRESULT ShowDialog()
{
    IFileOpenDialog *pFileOpen;

    HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
    if (SUCCEEDED(hr))
    {
        hr = pFileOpen->Show(NULL);
        if (SUCCEEDED(hr))
        {
            IShellItem *pItem;
            hr = pFileOpen->GetResult(&pItem);
            if (SUCCEEDED(hr))
            {
                // Use pItem (not shown). 
                pItem->Release();
            }
        }
        pFileOpen->Release();
    }
    return hr;
}

Avantages

  • Les variables peuvent être déclarées avec une étendue minimale. Par exemple, pItem n’est pas déclaré tant qu’il n’est pas utilisé.
  • Dans chaque instruction if , certains invariants sont vrais : tous les appels précédents ont réussi et toutes les ressources acquises sont toujours valides. Dans l’exemple précédent, lorsque le programme atteint l’instruction if la plus interne, pItem et pFileOpen sont connus pour être valides.
  • Il est clair quand libérer des pointeurs d’interface et d’autres ressources. Vous relâchez une ressource à la fin de l’instruction if qui suit immédiatement l’appel qui a acquis la ressource.

Inconvénients

  • Certaines personnes trouvent l’imbrication profonde difficile à lire.
  • La gestion des erreurs est mixte avec d’autres instructions de branchement et de bouclage. Cela peut rendre la logique globale du programme plus difficile à suivre.

Ifs en cascade

Après chaque appel de méthode, utilisez une instruction if pour tester la réussite. Si la méthode réussit, placez l’appel de méthode suivant à l’intérieur du bloc if . Mais au lieu d’imbriquer d’autres instructions if , placez chaque test SUCCEEDED suivant après le bloc if précédent. Si une méthode échoue, tous les tests SUCCEEDED restants échouent simplement jusqu’à ce que le bas de la fonction soit atteint.

HRESULT ShowDialog()
{
    IFileOpenDialog *pFileOpen = NULL;
    IShellItem *pItem = NULL;

    HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));

    if (SUCCEEDED(hr))
    {
        hr = pFileOpen->Show(NULL);
    }
    if (SUCCEEDED(hr))
    {
        hr = pFileOpen->GetResult(&pItem);
    }
    if (SUCCEEDED(hr))
    {
        // Use pItem (not shown).
    }

    // Clean up.
    SafeRelease(&pItem);
    SafeRelease(&pFileOpen);
    return hr;
}

Dans ce modèle, vous libérez des ressources à la toute fin de la fonction. Si une erreur se produit, certains pointeurs peuvent ne pas être valides lorsque la fonction se ferme. L’appel de Release sur un pointeur non valide bloque le programme (ou pire), vous devez donc initialiser tous les pointeurs vers NULL et case activée s’ils sont NULL avant de les libérer. Cet exemple utilise la SafeRelease fonction ; les pointeurs intelligents sont également un bon choix.

Si vous utilisez ce modèle, vous devez être prudent avec les constructions de boucles. À l’intérieur d’une boucle, arrêtez la boucle en cas d’échec d’un appel.

Avantages

  • Ce modèle crée moins d’imbrication que le modèle « si imbriqué ».
  • Le flux de contrôle global est plus facile à voir.
  • Les ressources sont libérées à un point dans le code.

Inconvénients

  • Toutes les variables doivent être déclarées et initialisées en haut de la fonction.
  • Si un appel échoue, la fonction effectue plusieurs vérifications d’erreur inutiles, au lieu de quitter la fonction immédiatement.
  • Étant donné que le flux de contrôle continue à travers la fonction après une défaillance, vous devez faire attention dans tout le corps de la fonction à ne pas accéder aux ressources non valides.
  • Les erreurs à l’intérieur d’une boucle nécessitent un cas spécial.

Échec de l’opération Jump on

Après chaque appel de méthode, testez l’échec (pas la réussite). En cas d’échec, accédez à une étiquette près du bas de la fonction. Après l’étiquette, mais avant de quitter la fonction, libérez les ressources.

HRESULT ShowDialog()
{
    IFileOpenDialog *pFileOpen = NULL;
    IShellItem *pItem = NULL;

    HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFileOpen->Show(NULL);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFileOpen->GetResult(&pItem);
    if (FAILED(hr))
    {
        goto done;
    }

    // Use pItem (not shown).

done:
    // Clean up.
    SafeRelease(&pItem);
    SafeRelease(&pFileOpen);
    return hr;
}

Avantages

  • Le flux de contrôle global est facile à voir.
  • À chaque étape du code après un case activée FAILED, si vous n’avez pas atteint l’étiquette, il est garanti que tous les appels précédents ont réussi.
  • Les ressources sont libérées à un seul emplacement dans le code.

Inconvénients

  • Toutes les variables doivent être déclarées et initialisées en haut de la fonction.
  • Certains programmeurs n’aiment pas utiliser goto dans leur code. (Toutefois, il convient de noter que cette utilisation de goto est très structurée ; le code ne saute jamais en dehors de l’appel de fonction actuel.)
  • Les instructions goto ignorent les initialiseurs.

Levée en cas d’échec

Au lieu de passer à une étiquette, vous pouvez lever une exception en cas d’échec d’une méthode. Cela peut produire un style plus idiomatique de C++ si vous êtes habitué à écrire du code sans exception.

#include <comdef.h>  // Declares _com_error

inline void throw_if_fail(HRESULT hr)
{
    if (FAILED(hr))
    {
        throw _com_error(hr);
    }
}

void ShowDialog()
{
    try
    {
        CComPtr<IFileOpenDialog> pFileOpen;
        throw_if_fail(CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
            CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen)));

        throw_if_fail(pFileOpen->Show(NULL));

        CComPtr<IShellItem> pItem;
        throw_if_fail(pFileOpen->GetResult(&pItem));

        // Use pItem (not shown).
    }
    catch (_com_error err)
    {
        // Handle error.
    }
}

Notez que cet exemple utilise la classe CComPtr pour gérer les pointeurs d’interface. En règle générale, si votre code lève des exceptions, vous devez suivre le modèle RAII (Resource Acquisition is Initialization). Autrement dit, chaque ressource doit être gérée par un objet dont le destructeur garantit que la ressource est correctement libérée. Si une exception est levée, l’appel du destructeur est garanti. Dans le cas contraire, votre programme risque de fuiter des ressources.

Avantages

  • Compatible avec le code existant qui utilise la gestion des exceptions.
  • Compatible avec les bibliothèques C++ qui lèvent des exceptions, telles que stl (Standard Template Library).

Inconvénients

  • Nécessite des objets C++ pour gérer des ressources telles que des handles de mémoire ou de fichiers.
  • Nécessite une bonne compréhension de l’écriture de code sans exception.

Suivant

Module 3. Windows Graphics