Il presente articolo è stato tradotto automaticamente.

Windows con C++

C++ e l'API di Windows

Kenny Kerr

Kenny KerrL'API di Windows rappresenta una sfida per gli sviluppatori C++. Le varie librerie che compongono l'API sono, per la maggior parte, esposti come funzioni di tipo c e maniglie o interfacce COM-stile. Nessuna di queste è molto conveniente per lavorare con e richiede qualche livello di incapsulamento o riferimento indiretto.

La sfida per gli sviluppatori C++ è quello di determinare il livello di incapsulamento. Gli sviluppatori che è cresciuto con librerie come MFC e ATL può essere incline a avvolgere tutto come classi e funzioni membro, perché questo è il modello esibito dalle librerie C++ hai invocata per così tanto tempo. Altri sviluppatori possono beffe di qualsiasi sorta di incapsulamento e basta usare le funzioni crude, interfacce e maniglie direttamente. Probabilmente questi altri sviluppatori non sono realmente gli sviluppatori C++, ma gli sviluppatori semplicemente c con problemi di identità. Credo che ci sia una via di mezzo più naturale per lo sviluppatore C++ contemporaneo.

Come ho riavviato il mio colonna qui a msdn Magazine, vi mostrerò come utilizzare C + + 0x, o C++ 2011 come esso sarà probabilmente nominato, insieme con l'API Windows per sollevare l'arte di sviluppo di software Windows nativo di secoli bui. Per i prossimi mesi ho intenzione di porterà attraverso un tour esteso dell'API Windows Thread Pool. Proseguire lungo e scoprirete come scrivere applicazioni incredibilmente scalabile senza bisogno di fantasia nuove lingue e complicato o costoso Runtime. Tutto ciò di cui avrete bisogno è il compilatore Visual C++ eccellente, l'API di Windows e un desiderio di dominare il vostro mestiere.

Come con tutti i buoni progetti, alcune basi sono necessario scendere un buon inizio. Come, allora, ho intenzione di "avvolgere" l'API di Windows? Piuttosto che impantanarsi ogni colonna successiva con questi dettagli, ho intenzione di compitare mio approccio consigliato in questa colonna e semplicemente costruire su questo andare avanti. Lascio la questione delle interfacce COM-stile per il momento, che non essere necessario per la prossime alcune colonne.

L'API di Windows è costituito da molte librerie che espongono un insieme di funzioni di tipo c e uno o più puntatori opachi, chiamati maniglie. Queste maniglie solitamente rappresentano una risorsa di sistema o libreria. Le funzioni sono fornite di creare, manipolare e rilasciare le risorse utilizzando le maniglie. Ad esempio, la funzione CreateEvent crea un oggetto evento, restituendo un handle all'oggetto dell'evento. Per rilasciare l'handle e dire al sistema che stai utilizzando l'evento oggetto, semplicemente passare l'handle alla funzione CloseHandle. Se non ci sono altri maniglie eccezionale all'oggetto stesso evento, il sistema sarà distruggerla:

auto h = CreateEvent( ...
);
CloseHandle(h);

Nuovo per C++

Se siete nuovi al 2011 C++, vorrei far presente che la parola chiave auto indica al compilatore di dedurre il tipo di variabile dall'espressione di inizializzazione. Questo è utile quando non si conosce il tipo di un'espressione, come spesso accade in metaprogrammazione, o quando si desidera solo per salvare alcune combinazioni di tasti.

Ma è quasi mai necessario scrivere codice come questo. Indubbiamente, il singolo offre funzionalità C++ più prezioso è quello della classe. I modelli sono cool, la Standard Template Library (STL) è magico, ma senza la classe nient'altro in C++ ha un senso. La classe è ciò che rende i programmi C++ succinta e affidabile. Non sto parlando di funzioni virtuali ed ereditarietà e altre caratteristiche di fantasia. Appena sto parlando di un costruttore e un distruttore. Spesso questo è tutto quello che avete bisogno e indovinate cosa? Esso non costa nulla. In pratica, è necessario essere a conoscenza l'overhead imposto dalla gestione delle eccezioni, e sarò che rivolgo alla fine di questa colonna.

Per domare le API di Windows e renderlo accessibile agli sviluppatori C++ moderni, una classe che incapsula un handle è necessario. Sì, libreria C++ preferito potrebbe già avere un wrapper di maniglia, ma è stato progettato da terra per C++ 2011? Potete attendibilmente conservare queste maniglie in un contenitore STL e passarli intorno il programma senza perdere traccia di chi li possiede?

La classe C++ è l'astrazione ideale per maniglie. Si noti che non ho detto "oggetti". Ricorda che la maniglia è rappresentante dell'oggetto all'interno del tuo programma e più spesso non è l'oggetto stesso. Il manico è che cosa ha bisogno la pastorizia — non l'oggetto. A volte può essere conveniente avere un rapporto uno a uno tra un oggetto Windows API e una classe C++, ma che è una questione separata.

Anche se le maniglie sono in genere opache, ci sono ancora diversi tipi di maniglie e, spesso, sottile semantico differenze che necessitano di un modello di classe per avvolgere adeguatamente maniglie in modo generale. I parametri dei modelli sono necessari per specificare il tipo di handle e le caratteristiche specifiche o tratti della maniglia.

In C++, una classe di tratti è comunemente utilizzata per fornire informazioni su un determinato tipo. In questo modo posso scrivere un singola classe modello per maniglie e fornire classi di tratti differenti per i diversi tipi di maniglie nell'API di Windows. Classe di tratti di un handle deve anche definire come un handle viene rilasciato in modo che il modello di classe handle può automaticamente rilasciarlo se necessario. Come tale, qui è una classe di tratti per maniglie di evento:

struct handle_traits
{
  static HANDLE invalid() throw()
  {
    return nullptr;
  }

  static void close(HANDLE value) throw()
  {
    CloseHandle(value);
  }
};

Perché molte librerie nell'API di Windows condividono questi semantica, può essere utilizzati per più solo gli oggetti evento. Come potete vedere, la classe di tratti consiste solo di funzioni membro statico. Il risultato è che il compilatore può facilmente inline viene introdotto il codice e nessun sovraccarico, fornendo nel contempo una grande quantità di flessibilità per metaprogrammazione.

La funzione non valida restituisce il valore di un handle non valido. Questo è di solito un nullptr, una nuova parola chiave nel 2011 C++ che rappresenta un valore di puntatore null. A differenza dei tradizionali alternative, nullptr è fortemente tipizzato modo che funziona bene con modelli e overload di funzioni. Ci sono casi in cui è definito un handle non valido come qualcosa di diverso da nullptr, così l'inclusione della funzione non valida nella classe tratti esiste per questo. La funzione stretta incapsula il meccanismo mediante il quale l'handle viene chiusa o rilasciato.

Dato il contorno della classe tratti, posso andare avanti e iniziare a definire il modello di classe handle, come mostrato Figura 1.

Figura 1 il modello di classe Handle

template <typename Type, typename Traits>
class unique_handle
{
  unique_handle(unique_handle const &);
  unique_handle & operator=(unique_handle const &);

  void close() throw()
  {
    if (*this)
    {
      Traits::close(m_value);
    }
  }

  Type m_value;

public:

  explicit unique_handle(Type value = Traits::invalid()) throw() :
    m_value(value)
  {
  }

  ~unique_handle() throw()
  {
    close();
  }

Ho chiamato e unique_handle perché è simile nello spirito per il modello di classe unique_ptr standard. Molte librerie anche utilizzano tipi di handle identici e semantica, quindi ha senso per fornire un typedef per più comunemente utilizzato caso, chiamato semplicemente handle:

typedef unique_handle<HANDLE, handle_traits> handle;

Ora posso creare un oggetto event e "gestire" come segue:

handle h(CreateEvent( ...
));

Hai dichiarato il costruttore di copia e copiare l'operatore di assegnazione come privato e sinistra li implementate. Ciò impedisce il compilatore di generarle automaticamente, come sono raramente appropriati per maniglie. L'API di Windows permette di alcuni tipi di maniglie per essere copiato, ma questo è un concetto molto diverso dalla semantica copia C++.

Un parametro di valore del costruttore si basa sulla classe tratti per fornire un valore predefinito. Il distruttore chiama privato vicino funzione membro, che a sua volta si basa sulla classe tratti di chiudere l'handle, se necessario. In questo modo ho un handle pila-friendly e le eccezioni.

Ma non sto ancora fatto. La funzione membro stretta si basa sulla presenza di una conversione booleana per determinare se l'handle deve essere chiuso. Benché C++ 2011 introduce funzioni di conversione esplicita, questa non è ancora disponibile in Visual C++, quindi utilizzare un approccio comune alla conversione booleano per evitare le conversioni implicite temuti che il compilatore altrimenti consente:

private:

  struct boolean_struct { int member; };
  typedef int boolean_struct::* boolean_type;

  bool operator==(unique_handle const &);
  bool operator!=(unique_handle const &);

public:

  operator boolean_type() const throw()
  {
    return Traits::invalid() != m_value ?
&boolean_struct::member : nullptr;
  }

Ciò significa che ora posso semplicemente testare se ho un handle valido, ma senza consentire conversioni pericolose passare inosservato:

unique_handle<SOCKET, socket_traits> socket;
unique_handle<HANDLE, handle_traits> event;

if (socket && event) {} // Are both valid?
if (!event) {} // Is event invalid?
int i = socket; // Compiler error!
if (socket == event) {} // Compiler error!

Utilizzando il più ovvio bool operator sarebbe hai permesso quegli ultimi due errori passare inosservato. Questo, tuttavia, consente una presa da confrontare con un altro — quindi la necessità di sia esplicitamente implementare gli operatori di uguaglianza o dichiararli come privato e lasciarli implementate.

Il modo in cui che un unique_handle possiede che un handle è analogo al modo in cui la unique_ptr standard classe modello possiede un oggetto e gestisce tale oggetto tramite un puntatore. Quindi ha senso per fornire il get familiare, reset e rilasciare le funzioni membro per gestire l'handle sottostante. La funzione get è facile:

Type get() const throw()
{
  return m_value;
}

La funzione di ripristino è un po' più lavoro, ma si basa su quello che ho già discusso:

bool reset(Type value = Traits::invalid()) throw()
{
  if (m_value != value)
  {
    close();
    m_value = value;
  }

  return *this;
}

Ho preso la libertà di cambiare la funzione reset leggermente dal modello fornito da unique_ptr restituendo un valore bool che indica l'oggetto è stato reimpostato con un handle valido o meno. Questo è utile con la gestione degli errori, a cui tornerò in un attimo. La funzione di rilascio dovrebbe ora essere ovvia:

Type release() throw()
{
  auto value = m_value;
  m_value = Traits::invalid();
  return value;
}

Copiare vs. Spostare

Il tocco finale è quello di considerare la copia contro mossa semantica. Perché io ho già vietato copia semantica per maniglie, ha senso per consentire la semantica di spostamento. Questo diventa essenziale se si desidera memorizzare le maniglie in contenitori STL. Questi contenitori hanno tradizionalmente fatto affidamento sulla semantica di copia, ma con l'introduzione del C++ 2011, spostare la semantica è supportati.

Senza entrare in una lunga descrizione dei riferimenti semantica e rvalue mossa, l'idea è quella di consentire il valore di un oggetto per passare da un oggetto a altro in un modo che è prevedibile per lo sviluppatore e coerente per compilatori e autori della biblioteca.

Prima di C++ 2011, gli sviluppatori ha dovuto ricorrere a tutti i tipi di trucchi complicati per evitare l'eccessiva predilezione che la lingua — e, per estensione STL — ha per la copia di oggetti. Il compilatore avrebbe spesso creare una copia di un oggetto, quindi immediatamente distruggere l'originale. Con mossa semantica, che lo sviluppatore può dichiarare che un oggetto non verrà utilizzato e il suo valore spostata altrove, spesso con il poco quanto uno swap di puntatore.

In alcuni casi lo sviluppatore deve essere espliciti e indicare questo; ma più spesso il compilatore può approfittare degli oggetti in movimento-consapevoli ed eseguire ottimizzazioni follemente efficiente che non furono mai possibile prima. La buona notizia è che la mossa semantica per le proprie classi di abilitazione è semplice. Proprio come la copia si basa su un costruttore di copia e un operatore di assegnazione copia, sposta semantica si basa su un costruttore mossa e un operatore di assegnazione di spostamento:

unique_handle(unique_handle && other) throw() :
  m_value(other.release())
{
}

unique_handle & operator=(unique_handle && other) throw()
{
  reset(other.release());
  return *this;
}

Il riferimento rvalue

2011 C++ introduce un nuovo tipo di riferimento, chiamato un riferimento rvalue. È dichiarato utilizzando & &; Questo è ciò che viene utilizzato nei membri unique_handle nel codice precedente. Anche se simili ai riferimenti di vecchi, ora chiamato lvalue i riferimenti, i nuovi riferimenti rvalue mostrano un po ' diverse regole quando si tratta di inizializzazione e sovraccarico di risoluzione. Per ora, vi lascio a quel (tornerò a questo argomento più tardi). Il principale vantaggio in questa fase di una maniglia con mossa semantica è che si possono correttamente ed efficacemente handle di archivio in contenitori STL.

Gestione degli errori

Questo è tutto per il modello di classe unique_handle. L'argomento finale questo mese — e preparare le colonne avanti — è la gestione degli errori. Noi potremmo discutere all'infinito sui pro e contro delle eccezioni contro i codici di errore, ma se si vuole abbracciare le librerie C++ standard ti basta abituarsi a eccezioni. Naturalmente, l'API di Windows utilizza codici di errore, quindi è necessario un compromesso.

Il mio approccio alla gestione degli errori è quello di fare il meno possibile e scrivere il codice di eccezioni ma evitare l'intercettazione di eccezioni. Se non ci sono gestori eccezioni, Windows genera automaticamente un report di errore che include un minidump dello schianto che è possibile eseguire il debug post-mortem. Generare eccezioni solo quando il runtime inaspettato errori si verificano e gestire tutto il resto con codici di errore. Quando viene generata un'eccezione, sai che è un bug nel codice oppure una catastrofe che ha colpito il computer.

L'esempio che mi piace dare è quello di accedere al Registro di Windows. Non riuscendo a scrivere un valore del Registro di sistema è di solito un sintomo di un problema più grande che sarà difficile da gestire in modo sensato nel vostro programma. Ciò dovrebbe comportare un'eccezione. Non riuscendo a leggere un valore dal Registro di sistema, tuttavia, dovrebbe essere anticipato e gestito con garbo. Questo non dovrebbe generare un'eccezione, ma restituiscono un valore bool o enum, che indica il valore se o perché non poteva essere letto.

L'API di Windows non è particolarmente coerenza con il suo errore di manipolazione; che è il risultato di un'API che si è evoluto nel corso di molti anni. Per la maggior parte, gli errori vengono restituiti come valori BOOL o HRESULT. Ci sono alcuni altri, che tendono a gestire in modo esplicito confrontando il valore restituito contro valori documentati.

Se decido di una chiamata di funzione specificata deve riuscire per il mio programma di continuare il funzionamento affidabile, utilizzare una delle funzioni elencate nella Figura 2 per controllare il valore restituito.

Figura 2 controllo valore restituito

inline void check_bool(BOOL result)
{
  if (!result)
  {
    throw check_failed(GetLastError());
  }
}

inline void check_bool(bool result)
{
  if (!result)
  {
    throw check_failed(GetLastError());
  }
}

inline void check_hr(HRESULT result)
{
  if (S_OK != result)
  {
    throw check_failed(result);
  }
}

template <typename T>
void check(T expected, T actual)
{
  if (expected != actual)
  {
    throw check_failed(0);
  }
}

Ci sono due cose degni di nota su queste funzioni. Il primo è che la funzione check_bool è sovraccarico che è inoltre possibile controllare la validità di un oggetto handle, che giustamente non consente la conversione implicita a BOOL. Il secondo è la funzione check_hr, che confronta in modo esplicito contro S_OK, piuttosto che utilizzare la macro SUCCEEDED più comune. Questo evita in silenzio ad accettare altri codici di dubbia successo come S_FALSE, che non è quasi mai che cosa si aspetta che lo sviluppatore.

Il mio primo tentativo di scrittura di queste controllare funzioni era una serie di overload. Ma come li ho usati in vari progetti, ho capito che l'API Windows definisce semplicemente troppi tipi di risultato e macro, affinché la creazione di un set di overload che funzionerebbe per tutti loro non è semplicemente possibile. Da qui i nomi delle funzioni decorato. Ho trovato alcuni casi dove Errori non erano essere catturati a causa di risoluzione dell'overload inaspettato. Il tipo check_failed gettato è abbastanza semplice:

struct check_failed
{
  explicit check_failed(long result) :
    error(result)
  {
  }

  long error;
};

Potrei decorarla con tutti i tipi di caratteristiche di fantasia, come l'aggiunta del supporto per i messaggi di errore, ma qual è il punto? Incluso il valore di errore in modo che posso facilmente scegliere esso quando si esegue un'autopsia in un'applicazione si è schiantata. Oltre a ciò, è solo andare a mettersi in cammino.

Dato queste controlla funzioni, posso creare un oggetto event e segnalarlo, generare un'eccezione se qualcosa va storto:

handle h(CreateEvent( ...
));

check_bool(h);

check_bool(SetEvent(h.get()));

Gestione delle eccezioni

L'altra questione con preoccupazioni efficienza di gestione delle eccezioni. Ancora una volta, gli sviluppatori sono divisi, ma più spesso perché tengono alcuni presupposizione non basati in realtà.

Il costo di gestione delle eccezioni si pone in due aree. È la prima generazione di eccezioni. Questo tende a essere più lento rispetto all'utilizzo di codici di errore ed è uno dei motivi che si dovrebbero solo generare eccezioni quando si verifica un errore fatale. Se tutto va bene, non pagherai mai questo prezzo.

La seconda e più comune, causa di problemi di prestazioni ha a che fare con il sovraccarico di runtime di assicurare che sono chiamati i distruttori appropriati, nell'improbabile evento viene generata un'eccezione. Codice è necessario per tenere traccia di quali distruttori devono essere eseguite; Naturalmente, questo aumenta anche la dimensione dello stack, che nel codice di grandi basi può influire in modo significativo sulle prestazioni. Nota che pagare questo costo o meno effettivamente viene generata un'eccezione, minimizzando così questo è essenziale per garantire buone prestazioni.

Che significa assicurare che il compilatore ha una buona idea di quali funzioni potenzialmente può generare eccezioni. Se il compilatore può dimostrare che non ci sarà più eventuali eccezioni da determinate funzioni, è possibile ottimizzare il codice che genera per definire e gestire lo stack. Ecco perché ho decorato il modello di classe intera maniglia e tratti classe funzioni membro con la specifica eccezione. Sebbene obsoleta in C++ 2011, è un'importante ottimizzazione specifica della piattaforma.

Questo è tutto per questo mese. Ora avete uno degli ingredienti fondamentali per la scrittura di programmi affidabili utilizzando l'API di Windows. Unisciti a me il mese prossimo come comincio a esplorare l'API Windows Thread Pool.

Kenny Kerr è un progettista software con la passione per lo sviluppo di Windows nativi. Contattarlo al kennykerr.ca.