Windows com C++

Ponteiros inteligentes COM revistos

Kenny Kerr

Kenny KerrApós o ressurgimento de COM, de outra forma conhecido como Windows Runtime, a necessidade de um ponteiro inteligente eficiente e confiável para interfaces COM é mais importante do que nunca. Mas o que possibilita um bom ponteiro inteligente COM? O modelo da classe CComPtr da ATL tem sido o ponteiro inteligente COM ao que parecem ser décadas. O Windows SDK para Windows 8 incorporou o modelo da classe ComPtr como parte da biblioteca de modelos de C++ do Windows Runtime (WRL), que alguns aclamam como a substituição moderna da CComPtr da ATL. Primeiro, também pensava que era um bom passo adiante, mas após muita experiência usando ComPtr da WRL, cheguei à conclusão de que devia ser evitada. Por que? Continue lendo.

O que deve ser feito? Devemos retornar à ATL? De modo algum, mas também seja o momento de aplicar parte do Modern C++ oferecido pelo Visual C++ 2015 ao design de um novo ponteiro inteligente para interfaces COM. Em Connect(); Visual Studio 2015 & Problema especial do Microsoft Azure, mostrei como aproveitar ao máximo o Visual C++ 2015 para implementar facilmente IUnknown e IInspectable usando o modelo da classe Implements. Agora vou mostrar como usar mais do Visual C++ 2015 para implementar um novo modelo de classe ComPtr.

Os ponteiros inteligentes são notoriamente difíceis de escrever, mas graças ao C++ 11, não é tão difícil como era. Um dos motivos para isso tem a ver com todos os truques inteligentes que os desenvolvedores de bibliotecas inventaram para solucionar a falta de expressividade na linguagem C++ e bibliotecas padrão, para fazer com que seus próprios objetos atuem como ponteiros internos permanecendo corretos e eficientes. Em particular, as referências rvalue estão a um longo caminho de facilitar muito mais a vida a nós desenvolvedores de bibliotecas. Outra parte é simplesmente uma visão retrospectiva, vendo como os designs existentes se deram. E, evidentemente, há o dilema de todo o desenvolvedor: mostrar comedimento e não tentar englobar todos os recursos concebíveis em uma abstração particular.

Ao nível mais básico, um ponteiro inteligente COM deve fornecer gerenciamento de recursos para o ponteiro de interface COM subjacente. Isso implica que o ponteiro inteligente será um modelo de classe e armazene um ponteiro de interface do tipo desejado. Tecnicamente, não precisa de fato de armazenar um ponteiro de interface de um tipo específico, mas ao invés somente de armazenar um ponteiro de interface IUnknown, mas o ponteiro inteligente precisa contar com static_cast sempre que o ponteiro inteligente for desreferenciado. Isso pode ser útil e conceitualmente perigoso, mas falarei sobre isso em uma coluna futura. Por enquanto, vou começar com um modelo de classe básico para armazenar um ponteiro fortemente tipado:

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

Há muito tempo os desenvolvedores de C++ podiam se questionar do que tudo isto se trata, mas as chances são que os desenvolvedores de C++ mais ativos não fiquem muito surpresos. A variável de membro m_ptr se baseia em um novo recurso incrível que permite que os membros de dados não estáticos sejam inicializados quando são declarados. Isto reduz dramaticamente o risco de se esquecer acidentalmente de inicializar as variáveis de membro quando os construtores são adicionados e mudados ao longo do tempo. Qualquer inicialização fornecida explicitamente por um construtor particular tem precedência sobre esta inicialização local, mas geralmente isso significa que os construtores não precisam se preocupar sobre como definir essas variáveis de membro que teriam, caso contrário, iniciado com valores imprevisíveis.

Dado que já é garantido que o ponteiro de interface será inicializado, também posso contar com outro novo recurso para solicitar explicitamente uma definição padrão de funções de membro especial. No exemplo anterior, estou solicitando a definição padrão do construtor padrão – um construtor padrão padrão, se desejar. Não mate o mensageiro. Ainda assim, a habilidade de tornar como padrão ou excluir funções de membro especial junto com a habilidade de inicializar variáveis de membro no ponto de declaração estão entre meus recursos favoritos oferecidos pelo Visual C++ 2015. São as pequenas coisas que interessam.

O serviço mais importante que um ponteiro inteligente COM deve oferecer é proteger o desenvolvedor dos perigos do modelo de contagem de referências COM intrusivo. Na verdade, gosto da abordagem COM em termos de contagem de referências, mas quero que uma biblioteca trate disso por mim. Isto chega em diversos locais sutis em todo o modelo de classe ComPtr, mas talvez o mais óbvio é quando um chamador desreferencia o ponteiro inteligente. Não quero que o chamador escreva algo como se segue, acidentalmente ou de outro modo:

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

A habilidade de invocar as funções virtuais AddRef ou Release deve ser exclusivamente no escopo do ponteiro inteligente. Claro, o ponteiro inteligente deve ainda permitir que os métodos restantes sejam invocados por meio de uma operação de desreferência. Normalmente, o operador de desreferência de um ponteiro inteligente pode ter este aspecto:

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

Isso funciona com ponteiros de interface COM e não há necessidade de uma asserção, pois uma violação de acesso é mais instrutiva. Mas esta implementação continuará a permitir que um chamador invoque AddRef e Release. A solução é simplesmente retornar um tipo que proíba a chamada de AddRef e Release. Um pequeno modelo de classe é útil:

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

O modelo de classe RemoveAddRefRelease herda todos os métodos do argumento de modelo, mas declara AddRef e Release particulares para que um chamador não possa se referir acidentalmente a esses métodos. O operador de desreferência do ponteiro inteligente pode simplesmente usar static_cast para proteger o ponteiro de interface devolvido:

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

Isto é somente um exemplo onde meu ComPtr se desvia da abordagem WRL. A WRL opta por tornar todos os métodos de IUnknown particulares, incluindo QueryInterface, e não vejo qualquer motivo para restringir os chamadores desse modo. Significa que a WRL deve fornecer inevitavelmente alternativas para este serviço essencial e que levem a uma complexidade adicional e confusão para os chamadores.

Como meu ComPtr decididamente usa o comando de contagem de referências, era melhor fazê-lo corretamente. Bem, vou começar com um par de funções auxiliares particulares começando com uma para AddRef:

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

Isso não é tão emocionante, mas existe uma variedade de funções que exigem que uma referência seja tomada condicionalmente e isto garantirá que faço sempre a coisa certa. A função auxiliar correspondente para Release é um pouco mais sutil:

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

Por que a temporária? Bem, considero a implementação mais intuitiva, mas incorreta que espelha aproximadamente o que fiz (corretamente) na função InternalAddRef:

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

O problema aqui é que invocar o método Release pode desencadear eventos que veriam o objeto sendo liberado uma segunda vez. Esta segunda viagem por InternalRelease iria encontrar de novo um ponteiro de interface não nulo e tentar executar Release de novo. Isto é com certeza um cenário incomum, mas o trabalho do desenvolvedor de biblioteca é considerar essas coisas. A implementação original envolvendo um temporário evita este Release duplo primeiro desanexando o ponteiro de interface do ponteiro inteligente e somente então invocando Release. Observando os anais da história, parece como se Jim Springfield tivesse sido o primeiro a detectar este bug irritante na ATL. Mesmo assim, com essas duas funções auxiliares em mãos, eu posso começar a implementar algumas das funções de membro especial que ajudam a fazer o objeto resultante agir e sentir como um objeto interno. O construtor de cópia é um exemplo simples.

Diferente dos ponteiros inteligentes que fornecem propriedade exclusiva, a construção da cópia deve ser permitida aos ponteiros inteligentes COM. Tome cuidado para evitar cópias a todo o custo, mas se o chamador quiser mesmo uma cópia, então uma cópia é o que terá. Eis um construtor de cópia simples:

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

Isto cuida o caso óbvio de construção da cópia. Copia o ponteiro de interface antes de invocar o auxiliar InternalAddRef. Se o deixasse ali, copiar um ComPtr seria muito parecido com um ponteiro interno, mas não na totalidade. Eu poderia, por exemplo, criar uma cópia como esta:

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

Isso espelha o que posso fazer com ponteiros brutos:

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

Mas ponteiros brutos também permitem isto:

IUnknown * unknown = hen;

Com meu construtor de cópia simples, não estou autorizado a fazer o mesmo com ComPtr:

ComPtr<IUnknown> unknown = hen;

Embora IHen deva derivar, por fim, de IUnknown, ComPtr<IHen> não deriva de ComPtr<IUnknown> e o compilador considera-os tipos não relacionados. O que eu preciso é de um construtor que aja como um construtor de cópia lógico para outros objetos ComPtr logicamente relacionados – especificamente, qualquer ComPtr com um argumento de modelo que seja convertível no argumento de modelo do ComPtr construído. Aqui, a WRL conta com características de tipo, mas na verdade não é necessário. Somente preciso de um modelo de função para fornecer a possibilidade de conversão e simplesmente deixarei o compilador verificar se é, de fato, convertível:

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

É quando o outro ponteiro é usado para inicializar o ponteiro de interface do objeto que o compilador verifica se a cópia é verdadeiramente importante. Então irá compilar:

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

Mas isto não:

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

E é como deve ser. É claro que o compilador ainda considera os dois tipos muito diferentes, pelo que o modelo do construtor não tenha acesso à variável de membro particular do outro, salvo se os tornar amigos:

template <typename T>
friend class ComPtr;

Pode ficar tentado a remover parte do código redundante, pois IHen é convertível em IHen. Por que não somente remover o construtor de cópia real? O problema é que este segundo construtor não é considerado um construtor de cópia pelo compilador. Se omitir o construtor de cópia, o compilador assumirá que você tencionava removê-lo e opor-se a qualquer referência a esta função excluída. Avançando.

Com a construção de cópia tratada, é muito importante que ComPtr também forneça a construção de movimentos. Se um movimento for permissível em dado cenário, ComPtr deve permitir que o compilador opte por esse, pois irá evitar um obstáculo de referência, que é muito mais dispendioso em comparação a uma operação de movimento. Um construtor de movimento é ainda mais simples que o construtor de cópia, pois não é necessário invocar InternalAddRef:

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

Ele copia o ponteiro de interface antes de limpar ou redefinir o ponteiro em uma referência rvalue ou o objeto do qual está sendo movido. No entanto, nesse caso, o compilador não é tão exigente e é possível simplesmente evitar este construtor de movimento para uma versão genérica que suporta tipos convertíveis:

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

E isso encerra os construtores de ComPtr. O destruidor é previsivelmente simples:

~ComPtr() noexcept
{
  InternalRelease();
}

Já tratei das nuances de destruição no auxiliar InternalRelease, pelo que posso simplesmente reutilizar essa cortesia. Abordei a construção de cópia e movimento, mas os operadores de atribuição correspondentes também devem ser fornecidos para este ponteiro inteligente se parecer com um ponteiro real. Para suportar essas operações, vou adicionar outro par de funções auxiliares particulares. A primeira é para adquirir em segurança uma cópia de dado ponteiro de interface:

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

Supondo que os ponteiros de interface não são iguais (ou não são ambos ponteiros nulos), a função libera qualquer referência existente antes de fazer uma cópia do ponteiro e proteger uma referência para o novo ponteiro de interface. Dessa forma, posso facilmente invocar InternalCopy para assumir a propriedade de uma referência única para a dada interface mesmo se o ponteiro inteligente já tiver uma referência. Da mesma forma, o segundo auxiliar lida com o movimento em segurança de dado ponteiro de interface, junto com a contagem de referências que representa:

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

Embora InternalCopy suporte naturalmente tipos conversíveis, essa função é um modelo para fornecer essa capacidade para o modelo de classe. Caso contrário, InternalMove é basicamente o mesmo, mas logicamente move o ponteiro de interface em vez de adquirir uma referência adicional. Com isso fora do caminho, posso implementar os operadores de atribuição simplesmente. Primeiro, a atribuição de cópia, e com o construtor de cópia, deve fornecer a forma canônica:

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

Então, posso fornecer um modelo para tipos convertíveis:

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

Mas como o construtor de movimento, posso simplesmente fornecer uma versão genérica única de atribuição de movimento:

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

Enquanto a semântica de movimento seja superior à cópia em termos de ponteiros inteligentes contado por referência, os movimentos têm um custo e uma boa forma de evitar movimentos em alguns cenários-chave é fornecer semântica de permuta. Muitos tipos de contêiner irão favorecer operações de permuta a movimentos, o que pode evitar a construção de uma carga tremenda de objetos temporários. Implementar a funcionalidade de permuta para ComPtr é bastante direto:

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

Usaria o algoritmo de permuta padrão mas, pelo menos na implementação do Visual C++, o cabeçalho <utilitário> necessário também inclui indiretamente <stdio.h> e não quer realmente forçar os desenvolvedores a incluir tudo isso somente para permuta. Claro, para algoritmos genéricos encontrarem meu método Swap, também preciso fornecer uma função de permuta não-membro (minúsculas):

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

Desde que isto seja definido no mesmo namespace que o modelo de classe ComPtr, o compilador permitirá que os algoritmos genéricos usem a permuta sem problemas.

Outro recurso interessante do C++11 é os operadores de conversão explícitos. Historicamente, foram necessários alguns acessos ilegais complicados para produzir um operador booliano explícito e confiável para verificar se um ponteiro inteligente era logicamente não nulo. Atualmente, é tão simples quanto isto:

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

E isso trata dos membros especiais e praticamente especiais que fazem meu ponteiro inteligente se comportar como um tipo interno com tanta assistência quanto posso possivelmente fornecer para ajudar o compilador a otimizar qualquer sobrecarga. O que permanece é uma pequena seleção de auxiliares que são necessários para aplicativos COM em muitos casos. Isso é onde deve ser tido cuidado para evitar adicionar demasiadas características avançadas. Ainda assim, existem inúmeras funções nas quais quase qualquer componente ou aplicativo não trivial dependem. Primeiro, precisa de uma forma para liberar explicitamente a referência subjacente. Isso é bem fácil:

void Reset() noexcept
{
  InternalRelease();
}

E, em seguida, precisa de uma forma para chegar ao ponteiro subjacente, caso o chamador precise passá-lo como um argumento a alguma outra função:

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

Talvez precise desanexar a referência, talvez para devolvê-la a um chamador:

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

Talvez precise fazer uma cópia de um ponteiro existente. Isso pode ser uma referência mantida pelo chamador a qual gostaria de deter:

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

Ou posso ter um ponteiro bruto que possui uma referência para seu destino que gostaria de anexar sem uma referência adicional sendo adquirida. Isso também pode ser útil para referências de unificação em casos raros:

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

As funções de alguns finais desempenham um papel particularmente crucial, portanto vou gastar mais alguns momentos nelas. Os métodos COM devolvem tradicionalmente referências como parâmetros de saída por meio de um ponteiro para um ponteiro. É importante que qualquer ponteiro inteligente COM forneça uma forma de capturar diretamente essas referências. Para isso forneça o método GetAddressOf:

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

Isso é de novo onde meu ComPtr se descarta da implementação da WRL em uma forma sutil, mas muito importante. Observe que GetAddressOf afirma que ele não mantém uma referência antes de devolver seu endereço. Isso é extremamente importante, caso contrário, a função chamada simplesmente substituirá qualquer referência que possa ter sido mantida e você terá um vazamento de referências. Sem a asserção, esses bugs são muito difíceis de detectar. Na outra extremidade do espectro está a habilidade de entregar referências, do mesmo tipo ou para outras interfaces que o objeto subjacente possa implementar. Se for desejada outra referência para a mesma interface, posso evitar invocar QueryInterface e simples devolver uma referência adicional usando a convenção prescrita por COM:

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

E pode usá-la como se segue:

hen.CopyTo(copy.GetAddressOf());

Caso contrário, QueryInterface pode ser usado sem ajuda do ComPtr:

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

Isso realmente depende de um modelo de função fornecido diretamente pelo IUnknown para evitar ter que fornecer explicitamente o GUID da interface.

Por fim, geralmente há casos em que um aplicativo ou componente precisa consultar uma interface sem necessariamente retornar ao chamador na convenção COM clássica. Nesses casos, faz mais sentido retornar a esse novo ponteiro de interface guardado perfeitamente dentro de outro ComPtr, como se segue:

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

Posso simplesmente, em seguida, usar o bool do operador explícito para verificar se a consulta foi bem-sucedida. Por fim, ComPtr também fornece todos os operadores de comparação não-membro esperados para conveniência e para suportar a vários contêineres e algoritmos genéricos. De novo, isto somente ajuda a tornar o ponteiro inteligente mais como um ponteiro interno, tudo enquanto fornece os serviços essenciais para gerenciar devidamente o recurso e fornecer os serviços necessários que os aplicativos e componentes COM esperam. O modelo de classe ComPtr é somente outro exemplo do Modern C++ para o Windows Runtime (moderncpp.com).


Kenny Kerr é programador de computador, assim como autor da Pluralsight e Microsoft MVP que mora no Canadá. Ele mantém um blog em kennykerr.ca e pode ser seguido no Twitter em twitter.com/kennykerr.

Agradecemos ao seguinte especialista técnico da Microsoft pela revisão deste artigo: James McNellis