Gestion des erreurs dans COM (Prise en main de Win32 et C++)
COM utilise les valeurs HRESULT pour indiquer le succès ou l’échec d’un appel de méthode ou de fonction. Divers en-têtes SDK définissent diverses constantes HRESULT. Un ensemble commun de codes système est défini dans WinError.h. Le tableau suivant montre certains de ces codes de retour système.
Constant | 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 | NULL a été passé incorrectement pour une valeur de pointeur. |
E_UNEXPECTED | 0x8000FFFF | Condition inattendue. |
S_OK | 0x0 | Réussite. |
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 toutes deux des codes de succès. Probablement 99 % des méthodes COM retournent S_OK lorsqu’elles réussissent ; mais ne laissez pas ce fait vous induire en erreur. Une méthode peut retourner d’autres codes de succès, alors testez toujours les erreurs en utilisant la macro SUCCEEDED ou FAILED. Le code d’exemple suivant montre la mauvaise manière et la bonne manière de tester le succès 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 succès S_FALSE mérite d’être mentionné. Certaines méthodes utilisent S_FALSE pour signifier, en gros, une condition négative qui n’est pas un échec. Cela peut également indiquer une « 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 seconde fois depuis le même thread. Si vous devez différencier S_OK et S_FALSE dans votre code, vous devez tester la valeur directement, mais utilisez toujours FAILED ou SUCCEEDED pour gérer les cas restants, comme montré dans le code d’exemple 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 Windows donne souvent une liste de codes d’erreur spécifiques qu’une méthode peut retourner. Cependant, vous ne devez pas considérer ces listes comme définitives. Une méthode peut toujours retourner une valeur HRESULT qui n’est pas listée dans la documentation. Encore une fois, 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 pour gérer les erreurs COM de manière structurée. Chaque modèle a des avantages et des inconvénients. Dans une certaine mesure, le choix est une question de préférences. Si vous travaillez sur un projet existant, il peut déjà avoir des directives de codage qui proscrivent un style particulier. Quel que soit le modèle que vous adoptez, un code robuste respectera les règles suivantes.
- Pour chaque méthode ou fonction qui retourne un HRESULT, vérifiez la valeur de retour avant de continuer.
- Libérez les ressources après les avoir utilisées.
- N’essayez pas d’accéder à des ressources invalides ou non initialisées, comme des pointeurs NULL.
- N’essayez pas d’utiliser une ressource après l’avoir libérée.
Avec ces règles en tête, voici quatre modèles pour gérer les erreurs.
if imbriqué
Après chaque appel qui retourne un HRESULT, utilisez une instruction if pour tester le succès. Ensuite, placez l’appel de méthode suivant dans la portée de l’instruction if. D’autres instructions if peuvent être imbriquées aussi profondément que nécessaire. Les exemples de code précédents dans ce module ont tous utilisé ce modèle, mais le voici à 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 portée minimale. Par exemple, pItem n’est pas déclaré jusqu’à ce qu’il soit 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 profonde, pItem et pFileOpen sont tous deux connus pour être valides.
- Il est clair quand libérer les pointeurs d’interface et autres ressources. Vous libérez une ressource à la fin de l’instruction if qui suit immédiatement l’appel qui a acquis la ressource.
Inconvénients
- Certaines personnes trouvent les imbrications profondes difficiles à lire.
- La gestion des erreurs est mélangée avec d’autres instructions de branchement et de boucle. Cela peut rendre la logique globale du programme plus difficile à suivre.
If en cascade
Après chaque appel de méthode, utilisez une instruction if pour tester le succès. 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’à la fin de la fonction.
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 les ressources à la toute fin de la fonction. Si une erreur se produit, certains pointeurs peuvent être invalides lorsque la fonction se termine. Appeler Release sur un pointeur invalide fera planter le programme (ou pire), donc vous devez initialiser tous les pointeurs à NULL et vérifier s’ils sont NULL avant de les libérer. Cet exemple utilise la fonction SafeRelease
; les pointeurs intelligents sont également un bon choix.
Si vous utilisez ce modèle, vous devez faire attention avec les constructions de boucle. À l’intérieur d’une boucle, sortez de la boucle si un appel échoue.
Avantages
- Ce modèle crée moins d’imbriquement que le modèle « if imbriqués ».
- Le flux de contrôle global est plus facile à voir.
- Les ressources sont libérées à un point du 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.
- Parce que le flux de contrôle continue à travers la fonction après un échec, vous devez être prudent dans tout le corps de la fonction de ne pas accéder à des ressources invalides.
- Les erreurs à l’intérieur d’une boucle nécessitent un cas spécial.
Saut en cas d’échec
Après chaque appel de méthode, testez l’échec (pas le succès). En cas d’échec, sautez à 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 point du code après une vérification FAILED, si vous n’avez pas sauté à l’étiquette, il est garanti que tous les appels précédents ont réussi.
- Les ressources sont libérées à un endroit 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. (Cependant, il convient de noter que cet usage de goto est hautement structuré ; le code ne saute jamais en dehors de l’appel de fonction actuel).
- Les instructions goto ignorent les initialisations.
Tentative en cas d’échec
Plutôt que de sauter à une étiquette, vous pouvez lancer une exception lorsqu’une méthode échoue. Cela peut produire un style plus idiomatique de C++ si vous avez l’habitude d’écrire du code sécurisé contre les exceptions.
#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.
}
}
Remarquez que cet exemple utilise la classe CComPtr pour gérer les pointeurs d’interface. Généralement, si votre code lance des exceptions, vous devez suivre le modèle RAII (Resource Acquisition is Initialization). C’est-à-dire que 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 lancée, le destructeur est garanti d’être invoqué. Sinon, votre programme pourrait fuir des ressources.
Avantages
- Compatible avec le code existant qui utilise la gestion des exceptions.
- Compatible avec les bibliothèques C++ qui lancent des exceptions, comme la bibliothèque standard de templates (STL).
Inconvénients
- Nécessite des objets C++ pour gérer des ressources telles que la mémoire ou les handles de fichiers.
- Nécessite une bonne compréhension de la manière d’écrire du code sécurisé contre les exceptions.
Next