C++

Visual C++ 2015 trás o C++ moderno ao API do Windows

Kenny Kerr

Visual C++ 2015 é a culminação de um enorme esforço por parte da equipe do C++ para trazer o C++ moderno para a plataforma do Windows. Ao longo dos últimos lançamentos, o Visual C++ tem conquistado uma rica seleção de linguagem C++ moderna e recursos de biblioteca que, juntos, contribuem para um ambiente absolutamente incrível no qual cria aplicativos e componentes universais do Windows. O Visual C++ 2015 se baseia no progresso notável introduzido nestas versões anteriores e fornece um compilador maduro que suporta grande parte do C++ 11 e um subconjunto de C++ 2015. Você pode argumentar sobre o nível de integridade, mas eu acho que é justo dizer que o compilador suporta os recursos de linguagem mais importantes, permitindo que o C++ moderno inaugure uma nova era de desenvolvimento de biblioteca para Windows. E isso realmente é a chave. Enquanto o compilador suporta o desenvolvimento de bibliotecas eficientes e elegantes, os desenvolvedores podem obter a construção de grandes aplicativos e componentes.

Ao invés de lhe dar uma lista aborrecida de novos recursos ou fornecer um rápido tour de alto nível das capacidades, eu o levarei em vez através do desenvolvimento de algum código tradicionalmente complexo que é agora, francamente, bastante agradável para escrever, graças à maturidade do compilador do Visual C++. Vou lhe mostrar algo que é intrínseco ao Windows e no coração de praticamente todos os APIs atuais e futuros significativos.

É um pouco irônico que o C++ é finalmente moderno o suficiente para o COM. Sim, eu estou falando sobre o Component Object Model, que tem sido a base para grande parte do API do Windows há anos, e continua como a base para o Windows Runtime. Enquanto o COM está inegavelmente ligado ao C++ em termos de seu design original, emprestando muito do C++ em termos de convenções do binário e de semântica, ele nunca foi totalmente elegante. Partes do C++ que não foram considerados portáteis o suficiente, como o dynamic_cast, tiveram que ser evitados em favor de soluções portáteis que fizeram implementações C++ mais desafiadores para se desenvolver. Muitas soluções foram fornecidas ao longo dos anos para fazer o COM mais palatável para os desenvolvedores do C++. A extensão da linguagem C++/CX é talvez a mais ambiciosa até agora feita pela equipe do Visual C++. Ironicamente, esses esforços para melhorar o suporte C++ padrão não deixaram o C++/CX na poeira e realmente fazem uma extensão da linguagem redundante.

Para provar este ponto eu vou lhe mostrar como implementar IUnknown e IInspectable inteiramente no C++ moderno. Não há nada de moderno ou atraente sobre essas duas belezas. IUnknown continua a ser a abstração central para APIs de destaque, como o DirectX. E essas interfaces - com IInspectable decorrente do IUnknown - repousa no coração do Windows Runtime. Eu vou te mostrar como implementá-las sem quaisquer extensões de linguagem, tabelas de interface ou outras macros - apenas o C++ eficiente e elegante, com grande quantidade de informações sobre o tipo rich, que permite que o compilador e desenvolvedor tenham uma grande conversa sobre o que precisa ser construído.

O principal desafio é chegar a uma forma de descrever a lista de interfaces que a classe COM ou Windows Runtime tem a intenção de implementar, e fazê-la de uma forma que é conveniente para o desenvolvedor e acessível para o compilador. Especificamente, eu preciso fazer esta lista de tipos disponíveis de tal forma que o compilador possa interrogar e até mesmo enumerar as interfaces. Se eu puder retirar aquilo, eu poderia ser capaz de obter do compilador para gerar o código para o método IUnknown QueryInterface e, opcionalmente, o método IInspectable GetIids, também. Estes dois métodos que apresentam o maior desafio. Tradicionalmente, as únicas soluções envolveram extensões de linguagem, macros terríveis ou um monte de código difícil de manter.

Ambas as implementações de método requerem uma lista de interfaces que uma classe pretende implementar. A escolha natural para descrever tal lista de tipos é um modelo variádico:

template <typename ... Interfaces>
class __declspec(novtable) Implements : public Interfaces ...
{
};

O atributo estendido novtable__declspec mantém quaisquer construtores e destruidores de ter que inicializar o vfptr em tais classes abstratas, o que muitas vezes significa uma redução significativa no tamanho do código. O modelo de classe de Implementos inclui um pacote de parâmetro do modelo, tornando-se assim um modelo variádico. Um pacote de parâmetro é um parâmetro do modelo que aceita qualquer número de argumentos do modelo. O truque é que os pacotes de parâmetros são normalmente utilizados para permitir funções para aceitar qualquer número de argumentos, mas neste caso eu estou descrevendo um modelo em que os argumentos serão interrogados puramente no tempo de compilação. As interfaces nunca aparecerão em uma lista de parâmetros de função.

Uma utilização desses argumentos já está panejado para ser visto. O pacote de parâmetro se expande para formar a lista de classes de base pública. É claro, eu ainda sou responsável por realmente implementar essas funções virtuais, mas neste ponto eu posso descrever uma classe concreta que implementa qualquer número de interfaces:

class Hen : public Implements<IHen, IHen2>
{
};

Pelo fato do pacote do parâmetro ser expandido para designar a lista de classes de base, ele é equivalente ao que eu poderia ter escrito, como a seguir:

class Hen : public IHen, public IHen2
{
};

A beleza de estruturação do modelo de classe de Implementos desta forma é que agora posso inserir a implementação de vários códigos clichê para o modelo de classe de Implementos enquanto o desenvolvedor da classe Hen pode usar essa abstração discreta e ignora a magia por trás de tudo.

Até aqui tudo bem. Agora eu considerarei a implementação de IUnknown. Eu deveria ser capaz de implementá-lo totalmente dentro do modelo de classe de Implementos, devido ao tipo de informação que o compilador tem agora à sua disposição. O IUnknown oferece duas instalações que são tão essenciais para classes COM como o oxigênio e água são para os seres humanos. O primeiro e talvez mais simples dos dois é a contagem de referência e é o meio pelo qual os objetos do COM monitoram a sua vida útil. O COM prescreve uma forma de contagem de referência intrusiva em que cada objeto é responsável pelo gerenciamento de sua vida com base na consciência de como existem muitas referências pendentes. Isso está em contraste com um ponteiro inteligente da contagem de referência como o modelo de classe shared_ptr do C++ 11, onde o objeto não tem conhecimento de sua propriedade compartilhada. Você pode argumentar sobre os prós e contras das duas abordagens, mas, na prática, a abordagem do COM é muitas vezes mais eficiente e é apenas a forma que o COM funciona, então você tem que lidar com isso. No mínimo, você provavelmente concordará que é uma idéia horrível para embrulhar uma interface do COM dentro de um shared_ptr!

Começarei com a única sobrecarga de tempo de execução introduzida pelo modelo de classe de Implementos:

protected:
  unsigned long m_references = 1;
  Implements() noexcept = default;
  virtual ~Implements() noexcept
  {}

O construtor padronizado não é realmente a sobrecarga em si; ele simplesmente garante que o resultado do construtor - em que inicializará a contagem de referência - esteja protegido e não público. Tanto a contagem de referência como o destruidor virtual estão protegidos. Tornar a contagem de referência acessível para as classes derivadas permite a composição de classe mais complexa. A maioria das classes pode simplesmente ignorar isso, mas observo que eu estou inicializando a contagem de referência para um. Isso está em contraste com a sabedoria popular, que sugere que a contagem de referência deve, inicialmente, ser zero, porque nenhuma referência foi entregue ainda. Essa abordagem foi popularizada pelo ATL e, sem dúvida, influenciada pelo COM Essencial do Don Box, mas é bastante problemático, como um estudo do código-fonte ATL pode atestar bem. Partindo do pressuposto de que a propriedade da referência será imediatamente assumida por um chamador ou conectada a um ponteiro inteligente que fornece um processo de construção muito menos propenso a erros.

O destruidor virtual é uma tremenda conveniência em que permite que o modelo de classe de Implementos implemente a contagem de referência ao invés de forçar a própria classe concreta para fornecer a implementação. Outra opção seria usar o padrão de modelo curiosamente recorrente para evitar a função virtual. Normalmente eu prefiro essa abordagem, mas complicaria a abstração ligeiramente, e porque uma classe de COM por sua própria natureza tem uma vtable, não existe nenhuma razão convincente para evitar uma função virtual aqui. Com essas primitivas no lugar, torna-se uma questão simples de implementar tanto para o AddRef como para o Release dentro do modelo de classe de Implementos. Primeiro, o método AddRef pode simplesmente usar o InterlockedIncrement intrínseco para bater na contagem de referência:

virtual unsigned long __stdcall AddRef() noexcept override
{
  return InterlockedIncrement(&m_references);
}

Isso é em grande parte auto-explicativo. Não fique tentado a chegar a algum esquema complexo através do qual você pode condicionalmente substituir as funções intrínsecas de InterlockedIncrement e de InterlockedDecrement com os operadores de incremento e decréscimo do C++. O ATL tenta fazer isso em um grande custo de complexidade. Se a eficiência é a sua preocupação, em vez gastar seus esforços evitando chamadas falsas para AddRef e Release. Novamente, o C++ moderno vem para o resgate com o seu suporte para o movimento semântico e sua capacidade de mover a propriedade de referências sem uma colisão de referência. Agora, o método de Lançamento é apenas marginalmente mais complexo:

virtual unsigned long __stdcall Release() noexcept override
{
  unsigned long const remaining = InterlockedDecrement(&m_references);
  if (0 == remaining)
  {
    delete this;
  }
  return remaining;
}

A contagem de referência é diminuída e o resultado é atribuído a uma variável local. Isto é importante porque este resultado deve ser devolvido, mas se o objeto foi destruído seria então ilegal se referir à variável do membro. Assumindo que não há referências pendentes, o objeto é simplesmente excluído por meio de uma chamada para o destruidor virtual acima mencionado. Isto conclui a contagem de referência, e a classe Hen concreta ainda está tão simples como antes:

class Hen : public Implements<IHen, IHen2>
{
};

Agora é hora de considerar o maravilhoso mundo de QueryInterface. A implementação deste método IUnknown é um exercício não trivial. Eu cubro isso extensivamente em meus cursos Pluralsight e você pode ler sobre as muitas maneiras estranhas e maravilhosas de rolar sua própria implementação em "COM Essencial" (Addison-Wesley Professional, 1998) por Don Box. Esteja avisado que, enquanto este é um excelente texto sobre o COM, ele é baseado em C++ 98 e não representa o C++ moderno de qualquer forma. Por uma questão de espaço e tempo, eu assumirei que você tem alguma familiaridade com a implementação do QueryInterface e se concentra sobre a forma de implementá-lo com o C++ moderno. Aqui está o método virtual em si:

virtual HRESULT __stdcall QueryInterface(
  GUID const & id, void ** object) noexcept override
{
}

Devido a um GUID que identifica uma interface particular, o QueryInterface deve determinar se o objeto implementa a interface desejada. Se isso acontecer, ele deve incrementar a contagem de referência para o objeto e, em seguida, retornar o ponteiro de interface desejada através do parâmetro externo. Se não, ele deve retornar um ponteiro nulo. Portanto, começarei com um esboço:

*object = // Find interface somehow
if (nullptr == *object)
{
  return E_NOINTERFACE;
}
static_cast<::IUnknown *>(*object)->AddRef();
return S_OK;

Então, o QueryInterface primeiro tenta encontrar a interface desejada de alguma forma. Se a interface não é suportada, o requisito do código de erro E_NO-INTERFACE é devolvido. Observe como eu já tomei cuidado com o requisito para limpar o resultado do ponteiro de interface sobre falhas. Você deve pensar em QueryInterface muito como uma operação binária. Ele consegue tanto encontrar a interface desejada ou não. Não fique tentado a ser criativo aqui e só responder condicionalmente favoravelmente. Apesar de existirem algumas opções limitadas permitidas pela especificação do COM, a maioria dos consumidores simplesmente assumirão que a interface não é suportada, independentemente de qual código de falha você pode devolver. Quaisquer erros em sua implementação, sem dúvida, lhe causará nenhum fim da infelicidade da depuração. O QueryInterface é também fundamental para mexer. Finalmente, o AddRef é chamado através do resutado do ponteiro de interface, novamente para suportar alguns cenários de composição de classe raros, mas admissíveis. Aqueles não são explicitamente suportados pelo modelo de classe de Implementos, mas eu prefiro dar um bom exemplo aqui. É importante ter em mente que as operações de contagem de referência são específicas da interface em vez de específicas do objeto. Você não pode simplesmente chamar o AddRef ou Release em qualquer interface pertencente a um objeto. Você deve honrar as regras do COM que regem a identidade do objeto, caso contrário, você corre o risco de introduzir o código ilegal que quebrará de forma misteriosa.

Então, como faço para descobrir se o GUID solicitado representa uma interface que a classe pretende implementar? É aí que eu posso voltar para o tipo de informação que o modelo de classe de Implementos coleta através do seu pacote de parâmetro do modelo. Lembre-se, minha meta é permitir que o compilador implemente isso para mim. Eu quero o código resultante para ser tão eficiente como se eu tivesse escrito à mão, ou melhor. Eu vou, portanto, fazer essa consulta com um conjunto de modelos de função variádicos, modelos de função que se incluem pacotes de parâmetros do modelo. Eu começarei com um modelo de função BaseQueryInterface para dar o pontapé inicial:

virtual HRESULT __stdcall QueryInterface(
  GUID const & id, void ** object) noexcept override
{
  *object = BaseQueryInterface<Interfaces ...>(id);

O BaseQueryInterface é essencialmente uma projeção do C++ moderno do IUnknown QueryInterface. Em vez de devolver um HRESULT, ele devolve o ponteiro de interface diretamente. A falha é, obviamente, indicada com um ponteiro nulo. Ela aceita um argumento de função única, o GUID identifica a interface para localizar. Mais importante, eu expandi o pacote de parâmetros do modelo de classe em sua totalidade para que a função BaseQueryInterface possa começar o processo de enumeração das interfaces. Você pode inicialmente pensar que, porque o BaseQueryInterface é um membro do modelo de classe do Implementos, ele pode simplesmente acessar esta lista de interfaces diretamente, mas eu preciso permitir esta função para retirar a primeira interface na lista, como a seguir:

template <typename First, typename ... Rest>
void * BaseQueryInterface(GUID const & id) noexcept
{
}

Desta forma, o BaseQueryInterface pode identificar a primeira interface e deixar o resto para uma busca subsequente. Você vê, o COM tem uma série de regras específicas para suportar o objeto de identidade que o QueryInterface deve implementar ou pelo menos honra. Em particular, as solicitações de IUnknown devem retornar sempre o mesmo ponteiro exato para que um cliente possa determinar se os dois ponteiros de interface se referem ao mesmo objeto. Assim, a função BaseQueryInterface é um ótimo lugar para implementar alguns desses axiomas. Portanto, eu poderia começar comparando o GUID solicitado com o primeiro argumento do modelo que representa a primeira interface da classe que pretende implementar. Se isso não é uma correspondência, eu verificarei se o IUnknown está sendo solicitado:

if (id == __uuidof(First) || id == __uuidof(::IUnknown))
{
  return static_cast<First *>(this);
}

Supondo que um deles é uma correspondência, eu simplesmente devolvo o ponteiro de interface inequívoco para a primeira interface. O static_cast garante que o compilador não será ele próprio o problema com a ambiguidade de múltiplas interfaces com base no IUnknown. A conversão apenas ajusta o ponteiro para localizar o local correto na classe vtable, e porque todos os vtables de interface começam com três métodos de IUnknown, isso é perfeitamente válido.

Enquanto eu estou aqui eu poderia muito bem adicionar suporte opcional para consultas IInspectable, também. IInspectable é um animal bastante estranho. Em certo sentido, é o IUnknown do Windows Runtime porque cada interface do Windows Runtime projetada em linguagens como C# e JavaScript deve derivar diretamente do IInspectable ao invés do IUnknown meramente sozinho. Esta é uma triste realidade para acomodar a forma como o Common Language Runtime implementa objetos e interfaces, com o que está em contraste com a forma como o C++ funciona e como o COM tem sido tradicionalmente definido. Ele também tem algumas ramificações de desempenho bastante infelizes quando se trata da composição do objeto, mas isso é um grande tópico que eucobrirei em um artigo de acompanhamento. Em relação ao QueryInterface que está em causa, eu só preciso garantir que o IInspectable que pode ser consultado deve ser a implementação de uma classe do Windows Runtime, em vez de simplesmente uma classe do COM clássico. Embora as regras do COM explícitas sobre o IUnknown não se apliquem ao IInspectable, eu posso simplesmente tratar o último quase da mesma forma aqui. Mas isso apresenta dois desafios. Primeiro, eu preciso descobrir se alguma das interfaces implementadas derivam do IInspectable. E segundo, eu preciso do tipo de tal interface para que eu possa retornar um ponteiro de interface devidamente ajustado sem ambiguidade. Se eu pudesse supor que a primeira interface na lista seria sempre baseada no IInspectable, eu poderia simplesmente atualizar o BaseQueryInterface da seguinte forma:

if (id == __uuidof(First) ||
  id == __uuidof(::IUnknown) ||
  (std::is_base_of<::IInspectable, First>::value &&
  id == __uuidof(::IInspectable)))
{
  return static_cast<First *>(this);
}

Observe que eu estou usando o traço do tipo C++ 11 is_base_of para determinar se o primeiro argumento do modelo é uma interface derivada do IInspectable. Isso garante que a comparação posterior seja excluída pelo compilador, que você deve ser a implementação de uma classe do COM clássico com nenhum suporte para o Windows Runtime. Desta forma, posso perfeitamente suportar tanto o Windows Runtime como as classes do COM clássico sem qualquer complexidade sintática adicional para os desenvolvedores de componentes e sem qualquer sobrecarga de tempo de execução desnecessária. Mas isso deixa o potencial para um bug muito sutil que você deve produzir para listar uma interface não-IInspectable primeiro. O que eu preciso fazer é substituir o is_base_of com algo que pode digitalizar toda a lista de interfaces:

template <typename First, typename ... Rest>
constexpr bool IsInspectable() noexcept
{
  return std::is_base_of<::IInspectable, First>::value ||
    IsInspectable<Rest ...>();
}

O IsInspectable ainda depende do traço do tipo is_base_of, mas agora se aplica a cada interface até que seja encontrada uma correspondência. Se nenhuma interface com base em IInspectable é localizada, a função de terminação é atingida:

template <int = 0>
constexpr bool IsInspectable() noexcept
{
  return false;
}

Vou voltar para o argumento padrão sem nome curioso em um momento. Assumindo que o IsInspectable devolve a verdade, eu preciso encontrar a primeira interface baseada no IInspectable:

template <int = 0>
void * FindInspectable() noexcept
{
  return nullptr;
}
template <typename First, typename ... Rest>
void * FindInspectable() noexcept
{
  // Find somehow
}

Eu posso novamente contar com o traço do tipo is_base_of, mas desta vez devolver um ponteiro de interface real deve localizar uma correspondência:

#pragma warning(push)
#pragma warning(disable:4127) // conditional expression is constant
if (std::is_base_of<::IInspectable, First>::value)
{
  return static_cast<First *>(this);
}
#pragma warning(pop)
return FindInspectable<Rest ...>();

O BaseQueryInterface pode então simplesmente usar o IsInspectable juntamente com o FindInspectable para suportar consultas ao IInspectable:

if (IsInspectable<Interfaces ...>() && 
  id == __uuidof(::IInspectable))
{
  return FindInspectable<Interfaces ...>();
}

Novamente, devido a uma a classe Hen concreta:

class Hen : public Implements<IHen, IHen2>
{
};

O modelo de classe de Implementos garantirá que o compilador gere o código mais eficiente se o Ihen ou o IHen2 deriva do IInspectable ou simplesmente do IUnknown (ou alguma outra interface). Agora posso finalmente implementar a parte recursiva do QueryInterface para cobrir quaisquer interfaces adicionais, tais como IHen2 no exemplo anterior. O BaseQueryInterface conclui chamando um modelo de função FindInterface:

template <typename First, typename ... Rest>
void * BaseQueryInterface(GUID const & id) noexcept
{
  if (id == __uuidof(First) || id == __uuidof(::IUnknown))
  {
    return static_cast<First *>(this);
  }
  if (IsInspectable<Interfaces ...>() && 
    id == __uuidof(::IInspectable))
  {
    return FindInspectable<Interfaces ...>();
  }
  return FindInterface<Rest ...>(id);
}

Observe que eu estou chamando este modelo de função FindInterface em muito da mesma maneira como eu originalmente chamado BaseQueryInterface. Neste caso, eu estou passando no resto das interfaces. Especificamente, estou expandindo o pacote do parâmetro de tal forma que ele possa identificar novamente a primeira interface no resto da lista. Mas isso apresenta um problema. Pelo fato de o pacote de parâmetro do modelo não é expandido como argumentos da função, eu posso acabar em uma situação incômoda onde a linguagem não vai me deixar expressar o que eu realmente quero. Porém mais sobre isso em um momento. Este modelo variádico FindInterface "recursivo" é como você poderia esperar:

template <typename First, typename ... Rest>
void * FindInterface(GUID const & id) noexcept
{
  if (id == __uuidof(First))
  {
    return static_cast<First *>(this);
  }
  return FindInterface<Rest ...>(id);
}

Ele separa o seu primeiro argumento de modelo do resto, devolvendo o ponteiro de interface ajustado se houver uma correspondência. Caso contrário, ele chama a si mesmo até que a lista de interface esteja esgotada. Embora eu vagamente me refira a isso como recursão em tempo de compilação, é importante observar que este modelo de função, e outros exemplos semelhantes no modelo da classe de Implementos, não são tecnicamente recursivos, nem mesmo em tempo de compilação. Cada instanciação do modelo de função chama uma instanciação diferente do modelo de função. Pof exemplo, o FindInterface<IHen, IHen2> chama o FindInterface<IHen2>, que chama o FindInterface<>. A fim de que ele seja recursivo, o FindInterface<IHen, IHen2> precisaria chamar o FindInterface<IHen, IHen2>, que não o fez.

No entanto, tenha em mente que esta "recursão" acontece no tempo de compilação e é como se você tivesse escrito todas elas se declaradas à mão, uma após a outra. Mas agora eu me deparei com um problema. Como esta sequência termina? Quando a lista de argumentos do modelo está vazia, é claro. O problema é que o C++ já define o que uma lista de parâmetros do modelo vazio significa:

template <>
void * FindInterface(GUID const &) noexcept
{
  return nullptr;
}

Isso está quase certo, mas o compilador lhe dirá que um modelo de função não está disponível para esta especialização. E, ainda, se eu não fornecer essa função de terminação, o compilador falhará ao compilar a chamada final quando o pacote de parâmetro estiver vazio. Este não é um caso de sobrecarga de funções, porque a lista de argumentos continua a mesma. Felizmente, a solução é muito simples. Eu posso evitar a função de terminação olhando como uma especialização, ao lhe fornecer um argumento padrão sem nome:

template <int = 0>
void * FindInterface(GUID const &) noexcept
{
  return nullptr;
}

O compilador está feliz, e se uma interface sem suporte é solicitada, esta função de terminação simplesmente devolve um ponteiro nulo e o método QueryInterface virtual devolverá o código de erro E_NOINTERFACE. E isso cuida do IUnknown. Se tudo o que interessa é o COM clássico, você pode parar com segurança lá conforme tudo o que você realmente precisa. Vale a pena reiterar neste ponto que o compilador otimizará essa implementação QueryInterface, com as suas várias chamadas de funções "recursivas" e expressões constantes, de modo que o código seja, no mínimo, tão bom quanto você pode escrever com a mão. E o mesmo pode ser feito para o IInspectable.

Para as classes de tempo de execução do Windows, existe a complexidade da implementação do IInspectable. Esta interface não é tão fundamental como o IUnknown, proporcionando uma coleção duvidosa de instalações em comparação com as funções absolutamente essenciais do IUnknown. Ainda assim, deixarei uma discussão sobre isso para um artigo futuro e focaremos em uma implementação do C++ moderno e eficiente para suportar qualquer classe de tempo de execução do Windows. Primeiro, eu levarei as funções virtuais do GetRuntimeClassName e do GetTrustLevel para fora do caminho. Ambos os métodos são relativamente triviais para implementar e também raramente são utilizados de modo que suas implementações em grande parte podem ser camufladas. O método GetRuntimeClassName deve retornar uma cadeia de caracteres do tempo de execução do Windows com o nome completo da classe do tempo de execução que o objeto representa. Eu deixarei isso para a própria classe implementar caso decida fazê-lo. O modelo de classe de Implementos pode simplesmente retornar E_NOTIMPL para indicar que este método não está implementado:

HRESULT __stdcall GetRuntimeClassName(HSTRING * name) noexcept
{
  *name = nullptr;
  return E_NOTIMPL;
}

Da mesma forma, o método GetTrustLevel simplesmente devolve uma constante enumerada:

HRESULT __stdcall GetTrustLevel(TrustLevel * trustLevel) noexcept
{
  *trustLevel = BaseTrust;
  return S_OK;
}

Observe que eu não marco explicitamente esses métodos IInspectable como funções virtuais. Evitar a declaração virtual permite que o compilador retire esses métodos, a classe COM não deve realmente implementar quaisquer interfaces do IInspectable. Agora vou voltar minha atenção para o método IInspectable GetIids. Isto é ainda mais propenso a erros do que o QueryInterface. Embora a sua implementação não seja tão crítico, uma implementação eficiente gerada pelo compilador é desejável. O GetIids devolve uma matriz alocada dinamicamente do GUIDs. Cada GUID representa uma interface que o objeto pretende implementar. Você pode, a princípio, achar que isso é simplesmente uma declaração de que o objeto oferece suporte através do QueryInterface, mas isso está correto somente pelo valor de face. O método GetIids pode decidir reter algumas interfaces a partir da publicação. De qualquer forma, eu começarei com a sua definição básica:

HRESULT __stdcall GetIids(unsigned long * count, 
  GUID ** array) noexcept
{
  *count = 0;
  *array = nullptr;

O primeiro parâmetro aponta para uma variável fornecida pelo chamador que o método GetIids deve definir para o número de interfaces na matriz resultante. O segundo parâmetro aponta para uma matriz dos GUIDs e é como a implementação transmite a matriz alocada dinamicamente de volta para o chamador. Aqui, eu comecei limpando os dois parâmetros, apenas para ficar seguro. Agora eu preciso determinar quantas interfaces a classe implementa. Gostaria muito de dizer que basta usar o operador do sizeof, que pode fornecer o tamanho de um pacote de parâmetros, conforme a seguir:

unsigned const size = sizeof ... (Interfaces);

Isso pode ser prático e o compilador tem o prazer de informar o número de argumentos de modelo que estariam presentes se este pacote de parâmetro fosse expandido. Essa também é efetivamente uma expressão constante, produzindo um valor conhecido no tempo de compilação. A razão pela qual isso não pode ser, conforme me referi anteriormente, é porque é extremamente comum para implementações de GetIids reter algumas interfaces que elas não desejam compartilhar com todos. Essas interfaces são conhecidas como interfaces encobertas. Qualquer pessoa pode consultá-las através do QueryInterface, mas o GetIids não te dizerá que elas estão disponíveis. Assim, eu preciso fornecer uma substituição no tempo de compilação para o operador sizeof variádico que exclui as interfaces encobertas, e eu preciso fornecer alguma forma de declarar e identificar essas interfaces encobertas. Eu começarei com a última. Eu quero torná-la tão fácil quanto possível para os desenvolvedores de componentes implementarem as classes para um mecanismo relativamente discreto que está em ordem. Eu posso simplesmente fornecer um modelo de classe Encoberta para "decorar" as interfaces encobertas:

template <typename Interface>
struct Cloaked : Interface {};

Eu posso, então, decidir implementar uma interface "IHenNative" especial na classe Hen concreta que não seja conhecida por todos os consumidores:

class Hen : public Implements<IHen, IHen2, Cloaked<IHenNative>>
{
};

Pelo fato de o modelo de classe Encoberta derivar do seu argumento de modelo, a implementação QueryInterface existente continua a funcionar perfeitamente. Acabei adicionando um pouco de informação do tipo extra que eu posso agora consultar, mais uma vez no tempo de compilação. Para isso eu definirei um tipo de traço IsCloaked para que eu possa facilmente consultar qualquer interface para determinar se ela foi encoberta:

template <typename Interface>
struct IsCloaked : std::false_type {};
template <typename Interface>
struct IsCloaked<Cloaked<Interface>> : std::true_type {};

Agora eu posso contar o número de interfaces desencobertas novamente usando um modelo de função variádica recursiva:

template <typename First, typename ... Rest>
constexpr unsigned CounInterfaces() noexcept
{
  return !IsCloaked<First>::value + CounInterfaces<Rest ...>();
}

E, claro, eu precisarei de uma função de terminação que pode simplesmente devolver zero:

template <int = 0>
constexpr unsigned CounInterfaces() noexcept
{
  return 0;
}

A capacidade de fazer tais cálculos aritméticos no tempo de compilação com C++ moderno é incrivelmente poderoso e simples. Agora eu posso continuar a aprofundar a implementação do GetIids solicitando essa contagem:

unsigned const localCount = CounInterfaces<Interfaces ...>();

A única ruga é que o suporte do compilador para expressões constantes ainda não está muito madura. Enquanto essa é, sem dúvida, uma expressão constante, o compilador ainda não honra as funções de membro constexpr. Idealmente, eu poderia marcar os modelos de função CountInterfaces como constexpr e a expressão resultante seria igualmente uma expressão constante, mas o compilador ainda não vê dessa forma. Por outro lado, eu não tenho nenhuma dúvida de que o compilador não terá problemas para otimizar este código de qualquer forma. Agora, se por qualquer motivo o CounInterfaces não encontra qualquer interface desencoberta, o GetIids pode simplesmente devolver o sucesso porque a matriz resultante estará vazia:

if (0 == localCount)
{
  return S_OK;
}

Novamente, isso é efetivamente uma expressão constante e o compilador gerará o código, sem uma forma condicional ou de outra forma. Em outras palavras, se não existem interfaces desencobertas, o código restante é simplesmente removido da implementação. Caso contrário, a implamentação é obrigada a alocar uma matriz de tamanho adequado de GUIDs usando o alocador COM tradicional:

GUID * localArray = static_cast<GUID *>(CoTaskMemAlloc(sizeof(GUID) * localCount));

Claro, isso pode falhar, no caso em que eu simplesmente devolva o HRESULT apropriado:

if (nullptr == localArray)
{
  return E_OUTOFMEMORY;
}

Neste ponto, o GetIids tem uma disposição pronta para ser preenchida com o GUIDs. Como você poderia esperar, eu preciso enumerar as interfaces uma última vez para copiar o GUID de cada interface desencobertos para essa matriz. Usarei um par de modelos de função como fiz antes:

template <int = 0>
void CopyInterfaces(GUID *) noexcept {}
template <typename First, typename ... Rest>
void CopyInterfaces(GUID * ids) noexcept
{
}

O modelo variádico (a segunda função) pode simplesmente usar o tipo de traço IsCloaked para determinar se deve copiar o GUID para a interface identificada pelo seu primeiro argumento do modelo antes de incrementar o ponteiro. Dessa forma, a matriz é atravessada sem ter que manter o controle de quantos elementos poderia conter ou onde na matriz ele deve ser escrito. Eu também suprimi o aviso sobre esta expressão constante:

#pragma warning(push)
#pragma warning(disable:4127) // Conditional expression is constant
if (!IsCloaked<First>::value)
{
  *ids++ = __uuidof(First);
}
#pragma warning(pop)
CopyInterfaces<Rest ...>(ids);

Como você pode ver, a chamada "recursiva" para o CopyInterfaces no final usa o valor do ponteiro potencialmente incrementado. E estou quase terminando. A implementação do GetIids pode então concluir ao chamar o CopyInterfaces para preencher a matriz antes de devolvê-la ao chamador:

CopyInterfaces<Interfaces ...>(localArray);
  *count = localCount;
  *array = localArray;
  return S_OK;
}

Durante todo o tempo, a classe Hen concreta é alheia a todo o trabalho que o compilador está fazendo em seu nome:

class Hen : public Implements<IHen, IHen2, Cloaked<IHenNative>>
{
};

E é assim que deveria ser com qualquer boa biblioteca. O compilador do Visual C++ 2015 fornece suporte incrível para o C++ padrão na plataforma Windows. Ele permite que os desenvolvedores de C++ construam bibliotecas bem elegantes e eficientes. Este suporta tanto o desenvolvimento de componentes do tempo de execução do Windows no padrão C++, bem como o seu consumo a partir dos aplicativos do Windows universal escritos inteiramente em C++ padrão. O modelo de classe de Implementos é apenas um exemplo ddo C++ moderno para o tempo de execução do Windows (consulte 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