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

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

Константа Числовое значение Описание
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 при успешном выполнении. но не позволяйте этому факту вводить вас в заблуждение. Метод может возвращать другие коды успешного выполнения, поэтому всегда проверяйте наличие ошибок с помощью макросов SUCCEEDED или 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. Это означает, что программа использовала неподдерживаемый формат пикселей. В документации MSDN часто приводится список конкретных кодов ошибок, которые может возвращать метод. Однако не следует рассматривать эти списки как окончательные. Метод всегда может возвращать значение 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. Дополнительные операторы 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 . Но вместо вложений операторов if поместите каждый последующий тест SUCCEEDED после предыдущего блока if . Если какой-либо метод завершается сбоем, все оставшиеся тесты 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;
}

В этом шаблоне ресурсы освобождаются в самом конце функции. При возникновении ошибки некоторые указатели могут быть недопустимыми при выходе из функции. Вызов метода Release для недопустимого указателя приведет к сбою программы (или еще хуже), поэтому перед их освобождением необходимо инициализировать все указатели на 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