Compartilhar via


Tratamento de erros no COM (Introdução ao Win32 e C++)

O COM usa valores HRESULT para indicar o êxito ou a falha de uma chamada de método ou função. Vários cabeçalhos do SDK definem várias constantes HRESULT. Um conjunto comum de códigos em todo o sistema é definido em WinError.h. A tabela a seguir mostra alguns desses códigos de retorno em todo o sistema.

Constante Valor numérico Descrição
E_ACCESSDENIED 0x80070005 Acesso negado
E_FAIL 0x80004005 Erro não especificado.
E_INVALIDARG 0x80070057 Valor de parâmetro inválido.
E_OUTOFMEMORY 0x8007000E Sem memória.
E_POINTER 0x80004003 NULL foi passado incorretamente para um valor de ponteiro.
E_UNEXPECTED 0x8000FFFF Condição inesperada.
S_OK 0x0 Êxito.
S_FALSE 0x1 Êxito.

 

Todas as constantes com o prefixo "E_" são códigos de erro. As constantes S_OK e S_FALSE são ambas códigos de sucesso. Provavelmente 99% dos métodos COM retornam S_OK quando são bem-sucedidos; mas não deixe que esse fato o engane. Um método pode retornar outros códigos de êxito, portanto, sempre teste se há erros usando a macro SUCCEEDED ou FAILED. O código de exemplo a seguir mostra a maneira errada e a maneira certa de testar o sucesso de uma chamada de função.

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

O código de sucesso S_FALSE merece menção. Alguns métodos usam S_FALSE para significar, grosso modo, uma condição negativa que não é uma falha. Também pode indicar um não operacional (no-op), o método foi bem-sucedido, mas não teve efeito. Por exemplo, a função CoInitializeEx retorna S_FALSE se você chamá-la uma segunda vez do mesmo thread. Se você precisar diferenciar entre S_OK e S_FALSE em seu código, deverá testar o valor diretamente, mas ainda usar FAILED ou SUCCEEDED para lidar com os casos restantes, conforme mostrado no exemplo de código a seguir.

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

Alguns valores HRESULT são específicos para um determinado recurso ou subsistema do Windows. Por exemplo, a API de elementos gráficos Direct2D define o código de erro D2DERR_UNSUPPORTED_PIXEL_FORMAT, o que significa que o programa usou um formato de pixel sem suporte. A documentação do Windows geralmente fornece uma lista de códigos de erro específicos que um método pode retornar. No entanto, você não deve considerar essas listas como definitivas. Um método sempre pode retornar um valor HRESULT que não está listado na documentação. Novamente, use as macros SUCCEEDED e FAILED . Se você testar um código de erro específico, inclua também um caso padrão.

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

Padrões para tratamento de erros

Esta seção examina alguns padrões para lidar com erros COM de maneira estruturada. Cada padrão traz vantagens e desvantagens. Até certo ponto, a escolha é uma questão de gosto. Se você trabalhar em um projeto existente, talvez ele já tenha diretrizes de codificação que determinem um estilo específico. Independentemente do padrão que você adotar, o código robusto obedecerá às seguintes regras.

  • Para cada método ou função que retorna um HRESULT, verifique o valor de devolução antes de continuar.
  • Libere recursos depois que eles forem usados.
  • Não tente acessar recursos inválidos ou não inicializados, como ponteiros NULL.
  • Não tente usar um recurso depois de liberá-lo.

Com essas regras em mente, aqui estão quatro padrões para tratamento de erros.

Ifs aninhados

Após cada chamada que retorna um HRESULT, use uma instrução if para testar o êxito. Em seguida, coloque a próxima chamada de método dentro do escopo da instrução if. Mais instruções if podem ser aninhadas tão profundamente quanto necessário. Todos os exemplos de código anteriores neste módulo usaram esse padrão, mas aqui está novamente:

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

Vantagens

  • As variáveis podem ser declaradas com escopo mínimo. Por exemplo, pItem não é declarado até que seja usado.
  • Dentro de cada instrução if , certas invariáveis são verdadeiras: todas as chamadas anteriores foram bem-sucedidas e todos os recursos adquiridos ainda são válidos. No exemplo anterior, quando o programa atinge a instrução if ambas pItem e pFileOpen são reconhecidamente válidas.
  • Fica evidente quando liberar ponteiros de interface e outros recursos. Você libera um recurso no final da instrução if que segue imediatamente a chamada que adquiriu o recurso.

Desvantagens

  • Algumas pessoas acham o aninhamento profundo difícil de ler.
  • O tratamento de erros é misturado com outras instruções de ramificação e loop. Isso pode tornar a lógica geral do programa mais difícil de seguir.

Ifs em cascata

Após cada chamada de método, use uma instrução if para testar o sucesso. Se o método for bem-sucedido, coloque a próxima chamada de método dentro do bloco if. Mas, em vez de aninhar mais instruções if coloque cada teste SUCCEEDED após o bloco if. Se algum método falhar, todos os testes SUCCEEDED restantes simplesmente falharão até que a parte inferior da função seja atingida.

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

Nesse padrão, você libera recursos no final da função. Se ocorrer um erro, alguns ponteiros poderão ser inválidos quando a função for encerrada. Chamar Release em um ponteiro inválido travará o programa (ou pior), portanto, você deve inicializar todos os ponteiros para NULL e verificar se eles são NULL antes de liberá-los. Este exemplo usa a função SafeRelease ponteiros inteligentes também são uma boa escolha.

Se você usar esse padrão, deverá ter cuidado com as construções de loop. Dentro de um loop, interrompa o loop se alguma chamada falhar.

Vantagens

  • Esse padrão cria menos aninhamento do que o padrão "ifs aninhado".
  • O fluxo de controle geral é mais fácil de ver.
  • Os recursos são liberados em um ponto do código.

Desvantagens

  • Todas as variáveis devem ser declaradas e inicializadas na parte superior da função.
  • Se uma chamada falhar, a função fará várias verificações de erros desnecessárias, em vez de sair da função imediatamente.
  • Como o fluxo de controle continua pela função após uma falha, é preciso ter cuidado para não acessar recursos inválidos em todo o corpo da função.
  • Erros dentro de um loop exigem um caso especial.

Pule em falha

Após cada chamada de método, teste a falha (não o sucesso). Em caso de falha, pule para um rótulo próximo à parte inferior da função. Após o rótulo, mas antes de sair da função, libere os recursos.

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

Vantagens

  • O fluxo de controle geral é fácil de ver.
  • Em todos os pontos do código após uma verificação FAILED , se você não tiver pulado para o rótulo, é garantido que todas as chamadas anteriores foram bem-sucedidas.
  • Os recursos são liberados em um local no código.

Desvantagens

  • Todas as variáveis devem ser declaradas e inicializadas na parte superior da função.
  • Alguns programadores não gostam de usar goto em seu código. (No entanto, deve-se notar que esse uso de goto é altamente estruturado e o código nunca sai da chamada de função atual.)
  • Instruções goto ignoram inicializadores.

Lance a falha

Em vez de pular para um rótulo, você pode lançar uma exceção quando um método falhar. Isso pode produzir um estilo mais idiomático de C++ se você estiver acostumado a escrever códigos à prova de exceções.

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

Observe que este exemplo usa a classe CComPtr para gerenciar ponteiros de interface. Geralmente, se o código gerar exceções, você deverá seguir o padrão RAII (Aquisição de recursos é inicialização). Ou seja, cada recurso deve ser gerenciado por um objeto cujo destruidor garante que o recurso seja liberado corretamente. Se uma exceção for lançada, o destruidor terá a garantia de ser invocado. Caso contrário, seu programa pode vazar recursos.

Vantagens

  • Compatível com o código existente que usa tratamento de exceção.
  • Compatível com bibliotecas C++ que lançam exceções, como a STL (Biblioteca de Modelos Padrão).

Desvantagens

  • Requer objetos C++ para gerenciar recursos como memória ou identificadores de arquivo.
  • Requer uma boa compreensão de como escrever código à prova de exceções.

Próximo

Módulo 3. Windows Graphics