Поделиться через


Обработка ошибок в COM (начало работы с Win32 и C++)

COM использует значения HRESULT , чтобы указать успешность или сбой вызова метода или функции. Различные заголовки ПАКЕТА SDK определяют различные константы HRESULT . Общий набор системных кодов определен в WinError.h. В следующей таблице показаны некоторые из этих системных кодов возврата.

Константа Числовое значение Description
E_ACCESSDENIED 0x80070005 Доступ запрещен.
E_FAIL 0x80004005 Не указано сообщение об ошибке.
E_INVALIDARG 0x80070057 Недопустимое значение параметра .
E_OUTOFMEMORY 0x8007000E Нехватка памяти.
E_POINTER 0x80004003 Значение NULL было передано неправильно для значения указателя.
E_UNEXPECTED 0x8000FFFF Неожиданное условие.
S_OK 0x0 Успех.
S_FALSE 0x1 Успех.

 

Все константы с префиксом "E_" являются кодами ошибок. Константы S_OK и S_FALSE являются кодами успешности. Вероятно, 99% методов COM возвращаются S_OK , когда они успешно, но не позволяйте этому факту обмануть вас. Метод может возвращать другие коды успешности, поэтому всегда тестировать ошибки с помощью макроса SUCCESSED или FAILED. В следующем примере кода показан неправильный способ и правильный способ проверки успешного вызова функции.

// 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"); 
}

Код успеха S_FALSE заслуживает упоминания. Некоторые методы используют S_FALSE , чтобы означать, примерно, отрицательное условие, которое не является сбоем. Он также может указывать на "no-op", метод успешно выполнен, но не имел никакого эффекта. Например, функция CoInitializeEx возвращает S_FALSE при вызове его второй раз из одного потока. Если необходимо различать S_OK и S_FALSE в коде, необходимо протестировать значение напрямую, но по-прежнему использовать FAILED или SUCCEEDED для обработки оставшихся случаев, как показано в следующем примере кода.

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

Некоторые значения HRESULT относятся к определенной функции или подсистеме Windows. Например, API графики Direct2D определяет код ошибки D2DERR_UNSUPPORTED_PIXEL_FORMAT, что означает, что программа использовала неподдерживаемый формат пикселей. Документация По Windows часто содержит список определенных кодов ошибок, возвращаемых методом. Однако эти списки не следует рассматривать как окончательные. Метод всегда может возвращать значение HRESULT , которое не указано в документации. Опять же, используйте макросы SUCCEEDED и FAILED. Если вы тестируете определенный код ошибки, включите также вариант по умолчанию.

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

Шаблоны обработки ошибок

В этом разделе рассматриваются некоторые шаблоны обработки ошибок COM в структурированном режиме. Каждый шаблон имеет преимущества и недостатки. В некоторой степени выбор является вопросом вкуса. Если вы работаете над существующим проектом, возможно, у него уже есть рекомендации по написанию кода, которые запрещают определенный стиль. Независимо от того, какой шаблон вы используете, надежный код будет соблюдать следующие правила.

  • Для каждого метода или функции, возвращающего HRESULT, проверьте возвращаемое значение перед продолжением.
  • Выпуск ресурсов после их использования.
  • Не пытайтесь получить доступ к недопустимым или неинициализированным ресурсам, таким как указатели NULL .
  • Не пытайтесь использовать ресурс после его выпуска.

Учитывая эти правила, ниже приведены четыре шаблона обработки ошибок.

Вложенные ifs

После каждого вызова, возвращающего HRESULT, используйте инструкцию if для проверки успешности. Затем поместите следующий вызов метода в область действия инструкции if . Больше , если операторы могут быть вложены так глубоко, как это необходимо. Предыдущие примеры кода в этом модуле использовали все эти шаблоны, но здесь снова:

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;
}

Достоинства

  • Переменные можно объявлять с минимальной областью. Например, pItem не объявляется до его использования.
  • В каждом операторе if некоторые инварианты имеют значение true: все предыдущие вызовы успешно выполнены, и все приобретенные ресурсы по-прежнему действительны. В предыдущем примере, когда программа достигает самого внутреннего оператора if , как известно, pItem и pFileOpen являются допустимыми.
  • Ясно, когда следует освободить указатели интерфейса и другие ресурсы. Вы освобождаете ресурс в конце инструкции if , которая немедленно следует вызову, который приобрел ресурс.

Недостатки

  • Некоторые люди находят глубокое вложение трудно читать.
  • Обработка ошибок смешана с другими операторами ветвления и циклирования. Это может сделать общую логику программы более сложной для выполнения.

Каскадное значение ifs

После каждого вызова метода используйте инструкцию if для проверки успешности. Если метод выполнен успешно, поместите следующий вызов метода внутри блока if . Но вместо дальнейшего вложения операторов, поместите каждый последующий тест SUCCEEDED после предыдущего блока. Если какой-либо метод завершается ошибкой, все остальные тесты SUCCEEDED просто завершаются ошибкой до достижения нижней части функции.

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;
}

В этом шаблоне вы освобождаете ресурсы в самом конце функции. Если возникает ошибка, некоторые указатели могут быть недопустимыми при выходе функции. Вызов выпуска в недопустимом указателе приведет к сбою программы (или хуже), поэтому необходимо инициализировать все указатели на NULL и проверить, имеют ли они значение NULL перед их освобождением. В этом примере используется SafeRelease функция; умные указатели также являются хорошим выбором.

Если вы используете этот шаблон, необходимо быть осторожным с конструкциями цикла. В цикле разорвать цикл, если любой вызов завершается ошибкой.

Достоинства

  • Этот шаблон создает меньше вложения, чем шаблон "вложенный ifs".
  • Общий поток управления проще видеть.
  • Ресурсы выпускаются в один момент кода.

Недостатки

  • Все переменные должны быть объявлены и инициализированы в верхней части функции.
  • Если вызов завершается сбоем, функция выполняет несколько ненужных проверок ошибок, а не немедленного выхода из функции.
  • Так как поток управления продолжается через функцию после сбоя, необходимо быть осторожным во всем тексте функции, чтобы не получить доступ к недопустимым ресурсам.
  • Ошибки внутри цикла требуют специального случая.

Переход к сбою

После каждого вызова метода проверьте неудачу (не успешно). При сбое перейдите к метки в нижней части функции. После метки, но перед выходом из функции выпустите ресурсы.

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;
}

Достоинства

  • Общий поток управления легко увидеть.
  • На каждом этапе кода после проверки FAILED , если вы не перейдете к метке, гарантируется, что все предыдущие вызовы выполнены успешно.
  • Ресурсы выпускаются в одном месте в коде.

Недостатки

  • Все переменные должны быть объявлены и инициализированы в верхней части функции.
  • Некоторые программисты не любят использовать goto в коде. (Однако следует отметить, что это использование goto очень структурировано; код никогда не переходит за пределы текущего вызова функции.)
  • Операторы goto пропускают инициализаторы.

Создание при сбое

Вместо перехода к метки можно создать исключение при сбое метода. Это может создать более идиоматический стиль C++, если вы используете для написания кода, безопасного для исключений.

#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.
    }
}

Обратите внимание, что в этом примере для управления указателями интерфейса используется класс CComPtr . Как правило, если код создает исключения, следует следовать шаблону RAII (приобретение ресурсов — инициализация). То есть каждый ресурс должен управляться объектом, деструктор которого гарантирует правильность выпуска ресурса. Если создается исключение, деструктор гарантированно вызывается. В противном случае программа может утечка ресурсов.

Достоинства

  • Совместим с существующим кодом, использующим обработку исключений.
  • Совместима с библиотеками C++, которые вызывают исключения, такие как стандартная библиотека шаблонов (STL).

Недостатки

  • Требуется, чтобы объекты C++ управляли ресурсами, такими как дескриптор памяти или файла.
  • Требуется хорошее представление о том, как писать безопасный для исключений код.

Следующий

Модуль 3. Графические элементы Windows