Condividi tramite


Gestione degli errori in COM (Introduzione a Win32 e C++)

COM usa valori HRESULT per indicare l'esito positivo o negativo di una chiamata al metodo o alla funzione. Varie intestazioni SDK definiscono varie costanti HRESULT . Un set comune di codici a livello di sistema è definito in WinError.h. La tabella seguente illustra alcuni di questi codici restituiti a livello di sistema.

Costante Valore numerico Descrizione
E_ACCESSDENIED 0x80070005 Accesso negato.
E_FAIL 0x80004005 Errore non specificato.
E_INVALIDARG 0x80070057 Valore di parametro non valido.
E_OUTOFMEMORY 0x8007000E Memoria esaurita.
E_POINTER 0x80004003 NULL è stato passato in modo non corretto per un valore del puntatore.
E_UNEXPECTED 0x8000FFFF Condizione imprevista.
S_OK 0x0 Esito positivo.
S_FALSE 0x1 Esito positivo.

 

Tutte le costanti con il prefisso "E_" sono codici di errore. Le costanti S_OK e S_FALSE sono entrambi codici di esito positivo. Probabilmente il 99% dei metodi COM restituisce S_OK quando riescono, ma non lasciare che questo fatto ti inganni. Un metodo potrebbe restituire altri codici di esito positivo, quindi testare sempre la ricerca di errori usando la macro SUCCEEDED o FAILED. Il codice di esempio seguente mostra il modo errato e il modo corretto per testare l'esito positivo di una chiamata di funzione.

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

Il codice di successo S_FALSE merita menzione. Alcuni metodi usano S_FALSE per indicare, approssimativamente, una condizione negativa che non è un errore. Può anche indicare un "no-op", ovvero il metodo ha avuto esito positivo, ma non ha avuto alcun effetto. Ad esempio, la funzione CoInitializeEx restituisce S_FALSE se la si chiama una seconda volta dallo stesso thread. Se è necessario distinguere tra S_OK e S_FALSE nel codice, è necessario testare direttamente il valore, ma usare comunque FAILED o SUCCEEDED per gestire i casi rimanenti, come illustrato nel codice di esempio seguente.

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

Alcuni valori HRESULT sono specifici di una particolare funzionalità o sottosistema di Windows. Ad esempio, l'API grafica Direct2D definisce il codice di errore D2DERR_UNSUPPORTED_PIXEL_FORMAT, il che significa che il programma ha usato un formato pixel non supportato. La documentazione di Windows fornisce spesso un elenco di codici di errore specifici che un metodo potrebbe restituire. Tuttavia, non è consigliabile considerare questi elenchi come definitivi. Un metodo può sempre restituire un valore HRESULT non elencato nella documentazione. Anche in questo caso, utilizzare le macro SUCCEEDED e FAILED. Se si verifica un codice di errore specifico, includere anche un caso predefinito.

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

Modelli per la gestione degli errori

Questa sezione esamina alcuni modelli per la gestione degli errori COM in modo strutturato. Ogni modello presenta vantaggi e svantaggi. In qualche misura, la scelta è una questione di gusto. Se si lavora su un progetto esistente, potrebbe avere già linee guida per la codifica che proscrivono uno stile specifico. Indipendentemente dal modello adottato, il codice affidabile obbedirà alle regole seguenti.

  • Per ogni metodo o funzione che restituisce un HRESULT, controllare il valore restituito prima di procedere.
  • Rilasciare le risorse dopo l'uso.
  • Non tentare di accedere a risorse non valide o non inizializzate, ad esempio puntatori NULL .
  • Non provare a usare una risorsa dopo il rilascio.

Tenendo presente queste regole, ecco quattro modelli per la gestione degli errori.

If annidati

Dopo ogni chiamata che restituisce un HRESULT, usare un'istruzione if per verificare l'esito positivo. Inserire quindi la chiamata al metodo successivo nell'ambito dell'istruzione if . Altre istruzioni if possono essere annidate in modo approfondito in base alle esigenze. Gli esempi di codice precedenti in questo modulo hanno usato tutti questo modello, ma di seguito è riportato di nuovo:

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

Vantaggi

  • Le variabili possono essere dichiarate con ambito minimo. Ad esempio, pItem non viene dichiarato finché non viene usato.
  • All'interno di ogni istruzione if , alcuni invarianti sono true: tutte le chiamate precedenti hanno avuto esito positivo e tutte le risorse acquisite sono ancora valide. Nell'esempio precedente, quando il programma raggiunge l'istruzione if più interna, pItem e pFileOpen sono noti come validi.
  • È chiaro quando rilasciare puntatori di interfaccia e altre risorse. Si rilascia una risorsa alla fine dell'istruzione if che segue immediatamente la chiamata che ha acquisito la risorsa.

Svantaggi

  • Alcune persone trovano un annidamento profondo difficile da leggere.
  • La gestione degli errori viene combinata con altre istruzioni di diramazione e ciclo. Ciò può rendere più difficile seguire la logica generale del programma.

Ifs a cascata

Dopo ogni chiamata al metodo, usare un'istruzione if per verificare l'esito positivo. Se il metodo ha esito positivo, inserire la chiamata al metodo successivo all'interno del blocco if . Invece di annidare ulteriormente le istruzioni if, posizionare ogni test SUCCEEDED successivo dopo il blocco if precedente. Se un metodo ha esito negativo, tutti i test SUCCEEDED rimanenti hanno semplicemente esito negativo fino a raggiungere la fine della funzione.

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

In questo modello si rilasciano le risorse alla fine della funzione. Se si verifica un errore, alcuni puntatori potrebbero non essere validi quando la funzione viene chiusa. La chiamata a Release su un puntatore non valido arresterà il programma (o peggio), quindi è necessario inizializzare tutti i puntatori a NULL e verificare se sono NULL prima di rilasciarli. In questo esempio viene usata la SafeRelease funzione . Anche i puntatori intelligenti sono una scelta ottimale.

Se si usa questo modello, è necessario prestare attenzione ai costrutti di ciclo. All'interno di un ciclo, interrompere il ciclo se una chiamata ha esito negativo.

Vantaggi

  • Questo modello crea meno annidamento del modello "ifs annidato".
  • Il flusso di controllo complessivo è più facile da vedere.
  • Le risorse vengono rilasciate in un punto del codice.

Svantaggi

  • Tutte le variabili devono essere dichiarate e inizializzate all'inizio della funzione.
  • Se una chiamata ha esito negativo, la funzione esegue più controlli di errore non necessario, anziché uscire immediatamente dalla funzione.
  • Poiché il flusso di controllo continua attraverso la funzione dopo un errore, è necessario prestare attenzione in tutto il corpo della funzione per non accedere a risorse non valide.
  • Gli errori all'interno di un ciclo richiedono un caso speciale.

Salto in caso di errore

Dopo ogni chiamata al metodo, verificare l'esito negativo (non esito positivo). In caso di errore, passare a un'etichetta nella parte inferiore della funzione. Dopo l'etichetta, ma prima di uscire dalla funzione, rilasciare le risorse.

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

Vantaggi

  • Il flusso di controllo complessivo è facile da vedere.
  • A ogni punto del codice dopo un controllo FAILED , se non si è passati all'etichetta, è garantito che tutte le chiamate precedenti abbiano avuto esito positivo.
  • Le risorse vengono rilasciate in un'unica posizione nel codice.

Svantaggi

  • Tutte le variabili devono essere dichiarate e inizializzate all'inizio della funzione.
  • Alcuni programmatori non amano usare goto nel codice. Si noti tuttavia che questo uso di goto è altamente strutturato. Il codice non passa mai all'esterno della chiamata di funzione corrente.
  • Le istruzioni goto ignorano gli inizializzatori.

Genera in caso di errore

Anziché passare a un'etichetta, è possibile generare un'eccezione quando un metodo ha esito negativo. Questo può produrre uno stile più idiotico di C++ se si usa per scrivere codice indipendente dall'eccezione.

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

Si noti che in questo esempio viene usata la classe CComPtr per gestire i puntatori di interfaccia. In genere, se il codice genera eccezioni, è necessario seguire il modello RAII (Acquisizione risorse è inizializzazione). Ovvero, ogni risorsa deve essere gestita da un oggetto il cui distruttore garantisce che la risorsa venga rilasciata correttamente. Se viene generata un'eccezione, viene garantito che venga richiamato il distruttore. In caso contrario, il programma potrebbe perdere risorse.

Vantaggi

  • Compatibile con il codice esistente che usa la gestione delle eccezioni.
  • Compatibile con le librerie C++ che generano eccezioni, ad esempio la libreria di modelli standard (STL).

Svantaggi

  • Richiede oggetti C++ per gestire risorse quali handle di memoria o file.
  • Richiede una buona comprensione di come scrivere codice indipendente da eccezioni.

Avanti

Modulo 3. Grafica Windows