Condividi tramite


Il presente articolo è stato tradotto automaticamente.

Windows con C++

Nuova versione dei puntatori intelligenti COM

Kenny Kerr

Kenny KerrDopo la seconda venuta di COM, altrimenti noto come il Runtime di Windows, la necessità di un puntatore intelligente efficiente e affidabile per le interfacce COM è più importante che mai. Ma ciò che rende per un buon puntatore intelligente COM? Il modello di classe ATL CComPtr è stato il de facto puntatore intelligente COM per quello che si sente come decenni. Windows SDK per Windows 8 ha introdotto il modello di classe ComPtr come parte di Windows Runtime C++ Template Library (WRL), che alcuni salutato come un rimontaggio moderno per il CComPtr ATL. In un primo momento, ho pensato anche questo era un buon passo avanti, ma dopo un sacco di esperienza usando ComPtr WRL, sono arrivato alla conclusione che dovrebbe essere evitato. Perche '? Continua a leggere.

Così che cosa dovrebbe essere fatto? Dovremmo tornare alla ATL? Affatto, ma forse è il momento di applicare alcuni C++ moderno offerti da Visual C++ 2015 alla progettazione di un nuovo puntatore intelligente per le interfacce COM. Nella Connect (); 2015 Visual Studio & Microsoft Azure, numero speciale, ho mostrato come fare la maggior parte del Visual C++ 2015 facilmente implementare IUnknown e IInspectable utilizzando il modello di classe implementa. Ora ho intenzione di mostrare come utilizzare più di Visual C++ 2015 per implementare un nuovo modello di classe ComPtr.

Puntatori intelligenti sono notoriamente difficili da scrivere, ma grazie al C + + 11, è quasi non difficile come era una volta. Parte del motivo per cui questo ha a che fare con tutti i trucchi intelligenti gli sviluppatori ideati per aggirare la mancanza di espressività nel linguaggio C++ e librerie standard, al fine di rendere i propri oggetti agiscono come puntatori incorporati pur restando efficiente e corretto. In particolare, i riferimenti rvalue andare un lungo cammino verso rendendo la vita molto più facile per noi sviluppatori di libreria. Un'altra parte è semplicemente senno di poi, vedendo come esistenti progetta cavata. E, naturalmente, c'è il dilemma di ogni sviluppatore: mostrando moderazione e non cercando di confezionare ogni caratteristica immaginabile in una particolare astrazione.

Base al massimo livello, un puntatore intelligente COM deve fornire la gestione delle risorse per il puntatore a interfaccia COM sottostante. Questo implica che il puntatore intelligente sarà un modello di classe e memorizzare un puntatore a interfaccia del tipo desiderato. Tecnicamente, esso in realtà non ha bisogno di memorizzare un puntatore a interfaccia di tipo particolare, ma invece potrebbe memorizzare solo un puntatore a interfaccia IUnknown, ma poi il puntatore intelligente avrebbe dovuto contare su un static_cast ogni volta che il puntatore intelligente è dereferenziato. Questo può essere utile e concettualmente pericoloso, ma ne parlerò in un articolo futuro. Per ora, comincerò con un modello di classe base per memorizzare un puntatore fortemente tipizzato:

template <typename Interface>
class ComPtr
{
public:
  ComPtr() noexcept = default;
private:
  Interface * m_ptr = nullptr;
};

Da molto tempo agli sviluppatori C++ potrebbero chiedere all'inizio che cosa si tratta, ma è probabile che i più attivi sviluppatori C++ non saranno troppo sorpresi. La variabile membro m_ptr si basa su una nuova funzione che permette ai membri non statici dati venga inizializzato dove sono state dichiarate. Questo riduce drasticamente il rischio di dimenticare accidentalmente per inizializzare le variabili membro come costruttori vengono aggiunti e modificati nel tempo. Qualsiasi inizializzazione esplicitamente fornito da un particolare costruttore ha la precedenza su questa inizializzazione sul posto, ma per la maggior parte questo significa che costruttori non devono preoccuparsi sull'impostazione di tali variabili membro che sarebbe altrimenti hanno iniziato con valori imprevedibili.

Dato l'interfaccia puntatore è ora assicurato per essere inizializzato, posso contare anche su un'altra nuova caratteristica di richiedere esplicitamente una definizione di default delle funzioni membro speciali. Nell'esempio precedente, sto chiedendo la definizione di default del costruttore predefinito — un costruttore predefinito di default, se volete. Don' t sparare al Messaggero. Ancora, la capacità di default o eliminare le funzioni membro speciali oltre alla possibilità di inizializzare le variabili membro nel punto di dichiarazione sono tra i miei preferiti funzionalità offerte da Visual C++ 2015. È le piccole cose che contano.

Il servizio più importante che deve offrire un puntatore intelligente COM è quella di schermatura lo sviluppatore dai pericoli del modello intrusivo di conteggio dei riferimenti COM. Io in realtà come l'approccio COM al conteggio dei riferimenti, ma voglio una libreria a prendersi cura di esso per me. Questo superfici in diversi luoghi sottile in tutto il modello di classe ComPtr, ma forse la più ovvia è quando un chiamante dereferenzia il puntatore intelligente. Non voglio un chiamante per scrivere qualcosa di simile a ciò che segue, accidentalmente o altrimenti:

ComPtr<IHen> hen;
hen->AddRef();

La possibilità di chiamare le funzioni virtuali AddRef o rilascio dovrebbe essere esclusivamente sotto la competenza del puntatore intelligente. Naturalmente, il puntatore intelligente deve ancora permettono i metodi restanti chiamata attraverso un'operazione di tale risoluzione dei riferimenti. Normalmente, di dereferenziare un puntatore intelligente operatore potrebbe essere simile a questo:

Interface * operator->() const noexcept
{
  return m_ptr;
}

Che funziona per i puntatori a interfaccia COM e non è necessario per un'asserzione perché una violazione di accesso è più istruttiva. Ma questa implementazione permetterà ancora un chiamante di chiamare AddRef e Release. La soluzione è semplicemente per restituire un tipo che vieta AddRef e Release venga chiamato. Un piccolo modello di classe è utile:

template <typename Interface>
class RemoveAddRefRelease : public Interface
{
  ULONG __stdcall AddRef();
  ULONG __stdcall Release();
};

Il modello di classe RemoveAddRefRelease eredita tutti i metodi su un argomento di template, ma dichiara AddRef e Release privata affinché un chiamante non può accidentalmente fare riferimento a tali metodi. Il puntatore intelligente di dereferenziare operatore può semplicemente utilizzare static_cast per proteggere il puntatore restituito:

RemoveAddRefRelease<Interface> * operator->() const noexcept
{
  return static_cast<RemoveAddRefRelease<Interface> *>(m_ptr);
}

Questo è solo un esempio dove mio ComPtr devia dall'approccio WRL. WRL opta per rendere tutti i privati di metodi di IUnknown, tra cui QueryInterface, e non vedo alcuna ragione per limitare i chiamanti in quel modo. Vuol dire che WRL inevitabilmente deve fornire alternative per questo servizio essenziale e che conduce alla complessità e confusione per i chiamanti.

Perché il mio ComPtr prende decisamente il comando di conteggio dei riferimenti, essa era meglio farlo correttamente. Bene, inizierò con un paio di funzioni di supporto privato cominciando con uno per AddRef:

void InternalAddRef() const noexcept
{
  if (m_ptr)
  {
    m_ptr->AddRef();
  }
}

Questo non è tutto così eccitante, ma ci sono una varietà di funzioni che richiedono un riferimento da prendere in modo condizionale e in questo modo che ogni volta fare la cosa giusta. La corrispondente funzione di supporto per il rilascio è un po' più sottile:

void InternalRelease() noexcept
{
  Interface * temp = m_ptr;
  if (temp)
  {
    m_ptr = nullptr;
    temp->Release();
  }
}

Perché il temporaneo? Beh, si consideri la più intuitiva ma non corretta implementazione che rispecchia più o meno quello che ho fatto (giustamente) all'interno della funzione InternalAddRef:

if (m_ptr)
{
  m_ptr->Release(); // BUG!
  m_ptr = nullptr;
}

Il problema qui è che chiamando il metodo potrebbe partì da una catena di eventi che potrebbe vedere l'oggetto viene rilasciato una seconda volta di rilascio. Questo secondo viaggio attraverso InternalRelease vuoi ancora trovare un puntatore a interfaccia non null e tentare di rilasciarlo nuovamente. Questo è certamente uno scenario comune, ma il lavoro dello sviluppatore libreria è di considerare tali cose. L'implementazione originale che coinvolge un temporaneo evita questo doppio rilascio staccando prima il puntatore a interfaccia dal puntatore intelligente e solo chiamando Release. Guardando attraverso gli annali della storia, sembra come se Jim Springfield fu il primo a prendere questo fastidioso bug in ATL. Comunque, con queste due funzioni di supporto in mano, posso cominciare a implementare alcune funzioni membro speciali che contribuiscono a rendere l'oggetto risultante agire e sentire come un oggetto incorporato. Il costruttore di copia è un semplice esempio.

A differenza dei puntatori intelligenti che forniscono proprietà esclusiva costruzione copia dovrebbe essere consentito per puntatori intelligenti COM. Cura deve essere presa per evitare copie a tutti i costi, ma se un chiamante vuole davvero una copia, una copia è quello che ottiene. Qui è un costruttore di copia semplice:

ComPtr(ComPtr const & other) noexcept :
  m_ptr(other.m_ptr)
{
  InternalAddRef();
}

Questo si prende cura del caso evidente della costruzione di copia. Copia il puntatore a interfaccia prima di chiamare il supporto InternalAddRef. Se ho lasciato esso là, copiando un ComPtr sentirei principalmente come un puntatore incorporato, ma non del tutto così. Ho potuto, ad esempio, creare una copia simile a questa:

ComPtr<IHen> hen;
ComPtr<IHen> another = hen;

Questo rispecchia cosa posso fare con i puntatori non elaborati:

IHen * hen = nullptr;
IHen * another = hen;

Ma i puntatori non elaborati permettono anche questo:

IUnknown * unknown = hen;

Con il mio costruttore di copia semplice, non sono consentito per fare la stessa cosa con ComPtr:

ComPtr<IUnknown> unknown = hen;

Anche se IHen, infine, deve derivare da IUnknown, ComPtr<IHen> non deriva da ComPtr<IUnknown> e il compilatore considera loro tipi indipendenti. Quello che mi serve è un costruttore che agisce come un costruttore di copia logica per altro ComPtr oggetti logicamente correlati — in particolare, qualsiasi ComPtr con un argomento di modello convertibile argomento di modello di ComPtr costruito. Qui, WRL si basa su tratti di tipo, ma questo non è realmente necessario. Tutto quello che serve è un template di funzione, prevedere la possibilità di conversione e poi semplicemente lascio il compilatore verifica se è effettivamente convertibile:

template <typename T>
ComPtr(ComPtr<T> const & other) noexcept :
  m_ptr(other.m_ptr)
{
  InternalAddRef();
}

È quando l'altro puntatore viene utilizzato per inizializzare il puntatore a interfaccia oggetto che il compilatore verifica se la copia è in realtà significativa. Così questo compilerà:

ComPtr<IHen> hen;
ComPtr<IUnknown> unknown = hen;

Ma questo non farà:

ComPtr<IUnknown> unknown;
ComPtr<IHen> hen = unknown;

E questo è come dovrebbe essere. Naturalmente, il compilatore considera ancora i due molto diversi tipi, quindi il modello costruttore effettivamente non avrà accesso all'altra variabile membro privata, a meno che non li faccio gli amici:

template <typename T>
friend class ComPtr;

Si potrebbe essere tentati di rimuovere alcuni del codice ridondante perché IHen è convertibile IHen. Perché non basta rimuovere il costruttore di copia effettivi? Il problema è che questo secondo costruttore non è considerato un costruttore di copia dal compilatore. Se si omette il costruttore di copia, il compilatore presupporrà che intendevi per rimuoverlo e oggetto a qualsiasi riferimento a questa funzione eliminata. In avanti.

Con la costruzione di copia curato, è molto importante che ComPtr fornire anche costruzione mossa. Se una mossa è ammissibile in un determinato scenario, ComPtr dovrebbe consentire al compilatore di optare per quello come salverà un urto di riferimento, che è molto più costoso rispetto a un'operazione di spostamento. Un costruttore di spostamento è ancora più il costruttore di copia semplice, perché non non c'è alcun bisogno di chiamare InternalAddRef:

ComPtr(ComPtr && other) noexcept :
  m_ptr(other.m_ptr)
{
  other.m_ptr = nullptr;
}

Copia il puntatore a interfaccia prima di cancellare o reimpostare il puntatore in riferimento rvalue, o l'oggetto viene spostato da. In questo caso, tuttavia, il compilatore non è così esigente e si può semplicemente evitano questo costruttore di spostamento per una versione generica che supporta tipi convertibili:

template <typename T>
ComPtr(ComPtr<T> && other) noexcept :
  m_ptr(other.m_ptr)
{
  other.m_ptr = nullptr;
}

E che avvolge i costruttori ComPtr. Il distruttore è prevedibilmente semplice:

~ComPtr() noexcept
{
  InternalRelease();
}

Io ho già preso cura delle sfumature di distruzione all'interno dell'InternalRelease helper, così qui posso riutilizzare semplicemente che bontà. Mi hai discusso copia e spostare la costruzione, ma i corrispondenti operatori di assegnazione devono inoltre essere forniti per questo puntatore intelligente sentire come un vero e proprio puntatore. Al fine di supportare le operazioni, ho intenzione di aggiungere un altro paio di funzioni di supporto privato. Il primo è per l'acquisizione di una copia di un puntatore a interfaccia dato in modo sicuro:

void InternalCopy(Interface * other) noexcept
{
  if (m_ptr != other)
  {
    InternalRelease();
    m_ptr = other;
    InternalAddRef();
  }
}

Supponendo che i puntatori a interfaccia non sono uguali (o non sia puntatori null), la funzione rilascia qualsiasi riferimento esistente prima di prendere una copia del puntatore e fissando un riferimento nuovo puntatore a interfaccia. In questo modo, posso facilmente chiamare InternalCopy per prendere possesso di un unico riferimento per l'interfaccia specificata anche se il puntatore intelligente contiene già un riferimento. Allo stesso modo, il supporto per la secondo si occupa di muoversi in modo sicuro un puntatore a interfaccia dato, insieme con il conteggio di riferimento che rappresenta:

template <typename T>
void InternalMove(ComPtr<T> & other) noexcept
{
  if (m_ptr != other.m_ptr)
  {
    InternalRelease();
    m_ptr = other.m_ptr;
    other.m_ptr = nullptr;
  }
}

Mentre InternalCopy naturalmente supporta tipi convertibili, questa funzione è un modello per fornire questa funzionalità per il modello di classe. Altrimenti, InternalMove è in gran parte lo stesso, ma logicamente si sposta il puntatore all'interfaccia, piuttosto che acquisire un riferimento aggiuntivo. Con quella di mezzo, è possibile implementare gli operatori di assegnazione molto semplicemente. Primo, l'assegnazione di copia, e come con il costruttore di copia, io devo fornire la forma canonica:

ComPtr & operator=(ComPtr const & other) noexcept
{
  InternalCopy(other.m_ptr);
  return *this;
}

Quindi posso fornire un modello per i tipi di Cabriolet:

template <typename T>
ComPtr & operator=(ComPtr<T> const & other) noexcept
{
  InternalCopy(other.m_ptr);
  return *this;
}

Ma come il costruttore di spostamento, posso semplicemente fornire un'unica versione generica della mossa di assegnazione:

template <typename T>
ComPtr & operator=(ComPtr<T> && other) noexcept
{
  InternalMove(other);
  return *this;
}

Mentre mossa semantica è spesso superiore a copiare quando si tratta di riferimento-contato puntatori intelligenti, mosse non sono senza costo, e un ottimo modo per evitare mosse in alcuni scenari chiave è quello di fornire la semantica di swap. Molti tipi di contenitore favorirà le operazioni di swap di mosse, che possono evitare la costruzione di un enorme carico di oggetti temporanei. Implementa la funzionalità di scambio per ComPtr è abbastanza semplice:

void Swap(ComPtr & other) noexcept
{
  Interface * temp = m_ptr;
  m_ptr = other.m_ptr;
  other.m_ptr = temp;
}

Vorrei utilizzare l'algoritmo di swap Standard ma, almeno in implementazione di Visual C++, la necessaria <utilità> intestazione comprende anche indirettamente <stdio. h> e davvero non voglio forzare gli sviluppatori in compreso tutto questo solo per lo swap. Naturalmente, per algoritmi generici trovare il mio metodo di Swap, ho bisogno di fornire anche una funzione di swap (minuscolo) di terzi:

template <typename Interface>
void swap(ComPtr<Interface> & left, 
  ComPtr<Interface> & right) noexcept
{
  left.Swap(right);
}

Finché questo è definito lo stesso spazio dei nomi come il modello di classe ComPtr, il compilatore felicemente permetterà algoritmi generici di fare uso dello swap.

Un'altra funzionalità interessante di C + + 11 è quello degli operatori di conversione espliciti. Storicamente, ci sono voluti alcuni hack disordinato per produrre un operatore booleano affidabile, esplicito per controllare se un puntatore intelligente era logicamente non null. Oggi, è semplice come questo:

explicit operator bool() const noexcept
{
  return nullptr != m_ptr;
}

E che si prende cura della special e membri praticamente speciali che rendono il mio puntatore intelligente si comportano tanto come un tipo incorporato con tanta assistenza come posso eventualmente fornire per aiutare il compilatore ottimizza qualsiasi sovraccarico via. Ciò che rimane è una selezione di aiutanti che sono necessarie per le applicazioni COM in molti casi. Questo è dove dovrebbe prestare attenzione per evitare di aggiungere troppe campane e fischietti. Ancora, ci sono una manciata di funzioni su cui si baserà quasi qualsiasi applicazione non banale o componente. In primo luogo, ha bisogno di un modo per rilasciare in modo esplicito il riferimento sottostante. Che è abbastanza facile:

void Reset() noexcept
{
  InternalRelease();
}

E poi ha bisogno di un modo per ottenere il puntatore sottostante, il chiamante è necessario passarlo come argomento a qualche altra funzione:

Interface * Get() const noexcept
{
  return m_ptr;
}

Potrei aver bisogno di staccare il riferimento, forse per restituirlo a un chiamante:

Interface * Detach() noexcept
{
  Interface * temp = m_ptr;
  m_ptr = nullptr;
  return temp;
}

Potrei aver bisogno di fare una copia di un puntatore esistente. Questo potrebbe essere un riferimento tenuto dal chiamante che vorrei tenere su a:

void Copy(Interface * other) noexcept
{
  InternalCopy(other);
}

O potrei avere un puntatore raw che possiede un riferimento alla sua destinazione che vorrei attaccare senza un riferimento aggiuntivo viene acquistato. Questo può essere utile anche per coalescenza riferimenti in rari casi:

void Attach(Interface * other) noexcept
{
  InternalRelease();
  m_ptr = other;
}

Il finale alcune funzioni svolgono un ruolo particolarmente critico, così che passerò qualche istante di più su di loro. COM metodi tradizionalmente restituiscono riferimenti come i parametri tramite un puntatore a un puntatore. È importante che qualsiasi puntatore intelligente COM fornisce un modo per catturare direttamente tali riferimenti. Per questo fornisco il metodo GetAddressOf:

Interface ** GetAddressOf() noexcept
{
  ASSERT(m_ptr == nullptr);
  return &m_ptr;
}

Ciò è ancora dove il mio ComPtr parte dall'implementazione WRL in modo sottile ma molto critico. Si noti che GetAddressOf asserisce che non contenere un riferimento prima di restituire il relativo indirizzo. Questo è di vitale importanza, altrimenti la funzione chiamata semplicemente sovrascriverà qualsiasi riferimento sono stato tenuto e che hai una perdita di riferimento. Senza l'asserzione, questi bug sono molto più difficili da rilevare. A altra estremità dello spettro è la capacità a portata di mano i riferimenti, sia dello stesso tipo o per altre interfacce che potrebbe implementare l'oggetto sottostante. Se si desidera un altro riferimento alla stessa interfaccia, posso evitare di chiamare QueryInterface e semplicemente restituire un riferimento aggiuntivo utilizzando la convenzione prescritta da COM:

void CopyTo(Interface ** other) const noexcept
{
  InternalAddRef();
  *other = m_ptr;
}

E si potrebbe utilizzare come segue:

hen.CopyTo(copy.GetAddressOf());

In caso contrario, QueryInterface stesso può essere impiegato con nessun ulteriore aiuto da ComPtr:

HRESULT hr = hen->QueryInterface(other.GetAddressOf());

Questo in realtà si basa su un modello di funzione fornito direttamente da IUnknown per evitare di dover fornire in modo esplicito GUID dell'interfaccia.

Infine, ci sono spesso casi in cui un'applicazione o un componente deve eseguire una query per un'interfaccia senza necessariamente passandogli il chiamante nella convenzione COM classica. In questi casi, ha più senso per restituire questo nuovo puntatore a interfaccia rimboccato comodamente all'interno di un altro ComPtr, come segue:

template <typename T>
ComPtr<T> As() const noexcept
{
  ComPtr<T> temp;
  m_ptr->QueryInterface(temp.GetAddressOf());
  return temp;
}

Quindi posso usare l'operatore esplicito bool semplicemente per verificare se la query è riuscita. Infine, ComPtr fornisce inoltre tutti gli operatori di confronto di terzi previsto per comodità e per sostenere i vari contenitori e algoritmi generici. Ancora una volta, questo aiuta solo a rendere il puntatore intelligente agire e sentire più come un puntatore incorporato, per tutto il tempo che fornisce i servizi essenziali per gestire le risorse e fornire i servizi necessari che apps COM correttamente e componenti si aspettano. Il modello di classe ComPtr è solo un altro esempio da Modern C++ per Windows Runtime (moderncpp.com).


Kenny Kerr è un programmatore di computer basato in Canada, così come un autore per Pluralsight e MVP Microsoft. Ha Blog a kennykerr.ca e si può seguirlo su Twitter a twitter.com/kennykerr.

Grazie al seguente Microsoft esperto tecnico per la revisione di questo articolo: James McNellis