Partilhar via


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

COM usa valores HRESULT de para indicar o sucesso ou falha de uma chamada de método ou função. Vários cabeçalhos SDK definem várias constantes HRESULT. Um conjunto comum de códigos de 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 Sucesso.
S_FALSE 0x1 Sucesso.

 

Todas as constantes com o prefixo "E_" são códigos de erro. As constantes S_OK e S_FALSE são ambos códigos de sucesso. Provavelmente, 99% dos métodos COM retornam S_OK quando são bem-sucedidos; mas não deixe que este facto o induza em erro. Um método pode retornar outros códigos de sucesso, portanto, sempre teste erros usando o SUCCESSFUL ou FAILED macro. 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 ser mencionado. Alguns métodos usam S_FALSE para significar, grosso modo, uma condição negativa que não é uma falha. Também pode indicar um "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 precisar diferenciar entre S_OK e S_FALSE no seu código, deve testar o valor diretamente, mas ainda usar FALHADO ou BEM-SUCEDIDO para lidar com os casos restantes, conforme mostrado no código de exemplo 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 de HRESULT são específicos para um determinado recurso ou subsistema do Windows. Por exemplo, a API de gráficos Direct2D define o código de erro D2DERR_UNSUPPORTED_PIXEL_FORMAT, o que significa que o programa usou um formato de pixel não suportado. A documentação do Windows geralmente fornece uma lista de códigos de erro específicos que um método pode retornar. No entanto, não deve considerar estas listas definitivas. Um método sempre pode retornar um HRESULT valor que não está listado na documentação. Novamente, use o SUCCESSFUL e FAILED macros. 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 analisa alguns padrões para lidar com erros COM de forma estruturada. Cada padrão tem vantagens e desvantagens. Até certo ponto, a escolha é uma questão de gosto. Se trabalhas num projeto existente, ele já pode ter diretrizes de codificação que definem um determinado estilo. Independentemente do padrão adotado, o código robusto obedecerá às seguintes regras.

  • Para cada método ou função que retorna um HRESULT, verifique o valor de retorno antes de prosseguir.
  • Libere recursos depois que eles forem usados.
  • Não tente aceder a 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 lidar com erros.

Ifs aninhados

Após cada chamada que retorna um HRESULT, use uma condição if para verificar o sucesso. Em seguida, coloque a próxima chamada de método dentro do escopo da instrução "if" se. Mais se declarações puderem ser aninhadas tão profundamente quanto necessário. 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 utilizado.
  • Dentro de cada se instrução, certas invariantes 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 mais interna, tanto pItem quanto pFileOpen são considerados válidos.
  • Fica claro quando deve liberar ponteiros de interface e outros recursos. Você liberta um recurso no final de se na instrução que segue imediatamente a chamada que adquiriu o recurso.

Desvantagens

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

Declarações 'if' 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 se bloquear. Mas, em vez de aninhar mais instruções se, coloque cada teste subsequente BEM-SUCEDIDO após o bloco anterior se. Se algum método falhar, todos os testes BEM-SUCEDIDOS restantes simplesmente falham até que o fim da função seja atingido.

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 podem ser inválidos quando a função for encerrada. Chamar Release num ponteiro inválido irá provocar uma falha no programa (ou pior), por isso deveis inicializar todos os ponteiros para NULL e verificar se eles estão NULL antes de os libertar. Este exemplo usa a função SafeRelease; Ponteiros inteligentes também são uma boa escolha.

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

Vantagens

  • Esse padrão cria menos aninhamento do que os "if aninhados".
  • 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 faz várias verificações de erro desnecessárias, em vez de sair da função imediatamente.
  • Como o fluxo de controle continua através da função após uma falha, você deve ter cuidado em todo o corpo da função para não acessar recursos inválidos.
  • Erros dentro de um loop exigem um caso especial.

Saltar em Fail

Após cada chamada de método, teste se ocorreu uma falha (e não sucesso). Em caso de falha, salte para uma etiqueta perto do final da função. Após o rótulo, mas antes de sair da função, liberte 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 cada ponto do código após uma verificação de FALHA, se não tiver saltado para a etiqueta, é garantido que todas as chamadas anteriores tiveram sucesso.
  • Os recursos são liberados em um único lugar 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 no seu código. (No entanto, deve-se notar que esse uso de goto é altamente estruturado; o código nunca salta para fora da chamada de função atual.)
  • As instruções goto ignoram os inicializadores.

Jogar no Fail

Em vez de saltar para um rótulo, você pode lançar uma exceção quando um método falhar. Isso pode produzir um estilo mais idiomática de C++ se você estiver acostumado a escrever código seguro para 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 (Resource Acquisition is Initialization). Ou seja, todo recurso deve ser gerenciado por um objeto cujo destruidor garante que o recurso seja liberado corretamente. Se uma exceção for lançada, o destrutor tem a garantia de ser invocado. Caso contrário, seu programa pode vazar recursos.

Vantagens

  • Compatível com código existente que usa tratamento de exceções.
  • Compatível com bibliotecas C++ que lançam exceções, como a STL (Standard Template Library).

Desvantagens

  • Requer objetos C++ para gerenciar recursos como memória ou identificadores de arquivo.
  • Requer um bom entendimento de como escrever código seguro contra exceções.

Seguinte

Módulo 3. Gráficos do Windows