Este artigo foi traduzido por máquina.
Windows com C++
C++ e a API do Windows
Kenny Kerr
A API do Windows apresenta um desafio para o desenvolvedor C++. As várias bibliotecas que compõem a API são, na maior parte, expostas como funções de C-estilo e alças ou interfaces COM estilo. Nenhum destes é muito conveniente para trabalhar com e requer algum nível de encapsulamento ou engano.
O desafio para o desenvolvedor C++ é para determinar o nível de encapsulamento. Desenvolvedores que cresceram com bibliotecas como MFC e ATL podem ser inclinada para embrulhar tudo como classes e funções de membro, porque esse é o padrão exposto pelas bibliotecas do C++ tem invocado para assim por muito tempo. Outros desenvolvedores podem zombar qualquer tipo de encapsulamento e usar apenas as funções crus, alças e interfaces diretamente. Indiscutivelmente estes outros desenvolvedores realmente não desenvolvedores de C++, mas os desenvolvedores simplesmente c com problemas de identidade. Penso que há um meio termo mais natural para o desenvolvedor C++ contemporâneo.
Como eu reiniciar minha coluna aqui no msdn Magazine, mostrarei como você pode usar o C + + 0x, ou C++ 2011 como ele será provavelmente nomeado, juntamente com a API do Windows para levantar a arte de desenvolvimento de software Windows nativo de idade das trevas. Para os próximos meses eu vou levá-lo através de uma excursão prolongada da API de Pool de threads do Windows. Acompanhar e você vai descobrir como escrever aplicações incrivelmente escalonáveis sem a necessidade de novas linguagens de fantasia e runtimes complicados ou onerosos. Tudo que você precisará é o excelente compilador Visual C++, a API do Windows e um desejo de dominar seu ofício.
Com todos os bons projectos, alguns aspectos básicos é necessária para um bom começo. Como, então, eu estou indo para "quebrar" a API do Windows? Um pouco de atolar cada coluna subseqüente com esses detalhes, eu vou soletrar minha abordagem recomendada nesta coluna e simplesmente construir sobre esta vai para a frente. Vou deixar a questão de interfaces COM estilo para o momento, como que não vai ser necessário para avançar algumas colunas.
A API do Windows consiste de muitas bibliotecas que expõem um conjunto de funções de C-estilo e um ou mais ponteiros opacos chamados alças. Esses identificadores geralmente representam um recurso biblioteca ou sistema. Funções são fornecidas para criar, manipular e liberar os recursos usando alças. Por exemplo, a função CreateEvent cria um objeto de evento, retornando um identificador para o objeto de evento. Para liberar o identificador e informar o sistema você é feito usando o evento objeto, simplesmente passe a alça para a função CloseHandle. Se não houver nenhuma outras alças pendentes para o mesmo objeto de evento, o sistema irá destruí-lo:
auto h = CreateEvent( ...
);
CloseHandle(h);
Novo para C++
Se você é novo para C++ 2011, gostaria de salientar que a palavra-chave auto informa o compilador para deduzir o tipo da variável a partir da expressão de inicialização. Isso é útil quando você não sabe o tipo de uma expressão, como acontece frequentemente em metaprogramação, ou quando você quer apenas economizar alguns pressionamentos de teclas.
Mas você quase nunca deve escrever código como este. Sem dúvida, o único mais valioso recurso C++ oferece é que a classe. Modelos são legais, a biblioteca STL (Standard Template) é mágico, mas sem a classe nada mais em C++ faz sentido. A classe é o que faz programas em C++, sucinta e confiável. Eu não estou falando sobre outros recursos fantasia e herança e funções virtuais. Eu apenas estou falando sobre um construtor e um destruidor. Muitas vezes isso é tudo que você precisa e adivinhe? Ele não lhe custa nada. Na prática, você precisa estar atento a sobrecarga imposta pela manipulação de exceção, e que vou abordar no final desta coluna.
Para domar a API do Windows e torná-lo acessível para desenvolvedores de C++ modernos, é necessária uma classe que encapsula um identificador. Sim, sua biblioteca C++ favorita já pode ter um invólucro de identificador, mas ele foi projetado desde o início para 2011 C++? Você armazenar estas alças em um Contêiner STL confiável e passá-las em torno de seu programa sem perder o controle de quem é o proprietário?
A classe C++ é a abstração perfeita para identificadores. Observe que eu não disse "objetos". Lembre-se de que o identificador é representante do objeto dentro de seu programa e na maioria das vezes não é o próprio objeto. O identificador é o que precisa de pastoreio — não o objeto. Às vezes, pode ser conveniente ter um relacionamento um para um entre um objeto de API do Windows e uma classe C++, mas que é uma questão separada.
Apesar de alças são geralmente opacas, existem ainda diferentes tipos de alças, muitas vezes, sutis semântica diferenças e que exigem um modelo de classe para embrulhar adequadamente alças de uma maneira geral. Parâmetros de modelo são necessárias para especificar o tipo de identificador e a características específicas ou as características do identificador.
Em C++, uma classe de características é comumente usada para fornecer informações sobre um determinado tipo. Dessa forma eu posso escrever um modelo de classe única para identificadores e fornecer classes de características diferentes para os diferentes tipos de alças na API do Windows. Classe de características de um identificador também precisa definir como um identificador é liberado para que o modelo de classe do identificador pode automaticamente liberá-lo se necessário. Como tal, aqui é uma classe de características de identificadores de evento:
struct handle_traits
{
static HANDLE invalid() throw()
{
return nullptr;
}
static void close(HANDLE value) throw()
{
CloseHandle(value);
}
};
Porque muitas bibliotecas na API do Windows compartilham essa semântica, pode ser usados para mais do que apenas objetos de evento. Como você pode ver, a classe de características consiste apenas funções de membro estático. O resultado é que o compilador pode facilmente em linha o código e nenhuma sobrecarga é introduzido, oferecendo uma grande flexibilidade para metaprogramação.
A função inválida retorna o valor de um identificador inválido. Isto é normalmente um nullptr, uma nova palavra-chave em 2011 C++ que representa um valor de ponteiro nulo. Ao contrário do tradicionais alternativas, NULL é fortemente tipado para que ele funciona bem com modelos e sobrecarga de função. Existem casos onde um identificador inválido é definido como algo diferente de NULL, assim que a inclusão da função inválida na classe traços existe para isso. A função fechar encapsula o mecanismo pelo qual o identificador é fechado ou lançado.
Tendo em conta o contorno da classe traços, eu posso ir em frente e iniciar definindo o modelo de classe do identificador, como mostrado na Figura 1.
Figura 1 O identificador de classe modelo
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();
}
Eu tenho chamado unique_handle porque é semelhante em espírito para o modelo de classe padrão unique_ptr. Muitas bibliotecas também usam tipos de identificador idênticos e semântica, faz sentido para fornecer um typedef para o mais comumente usado caso, simplesmente chamado identificador:
typedef unique_handle<HANDLE, handle_traits> handle;
Eu agora pode criar um objeto de evento e "manipular"-lo da seguinte forma:
handle h(CreateEvent( ...
));
Eu tenho declarado o Construtor de cópia e copiar o operador de atribuição como privado e deixou-os unimplemented. Isso impede que o compilador automaticamente gerá-los, como eles são raramente apropriados para identificadores. A API do Windows permite que certos tipos de identificadores para ser copiado, mas este é um conceito muito diferente da semântica de cópia de C++.
O valor parâmetro Construtor depende da classe de características para fornecer um valor padrão. As chamadas de destruidor privado fechar função membro, que por sua vez depende da classe de traços para fechar o identificador, se necessário. Desta forma, eu tenho um identificador de pilha-amigável e segura de exceção.
Mas eu não sou feito ainda. A função de membro de fechar conta com a presença de uma conversão booleana para determinar se o identificador deve ser fechado. Embora C++ 2011 apresenta funções de conversão explícita, isso ainda não está disponível no Visual C++, assim que eu uso uma abordagem comum para conversão booleana para evitar as temida conversões implícitas que o compilador permite:
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;
}
Isso significa que agora pode simplesmente testar se eu tenho um identificador válido, mas sem permitir conversões perigosas passar despercebida:
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!
Usar o bool operador mais óbvio seria tenha permitido esses dois últimos erros passar despercebida. Isso, no entanto, que um soquete ser comparado com outro — daí a necessidade de qualquer explicitamente implementar os operadores de igualdade ou declará-los como privado e deixá-los unimplemented.
A forma como um unique_handle possui que um identificador é análogo à forma como o padrão unique_ptr classe modelo possui um objeto e gerencia esse objeto através de um ponteiro. Em seguida, faz sentido para fornecer o get familiar, redefinir e liberar funções para gerenciar o identificador de base. A função get é fácil:
Type get() const throw()
{
return m_value;
}
A função de reset é um pouco mais de trabalho, mas tem por base o que eu já discuti:
bool reset(Type value = Traits::invalid()) throw()
{
if (m_value != value)
{
close();
m_value = value;
}
return *this;
}
Eu tomei a liberdade de alterar a função de redefinir um pouco do padrão fornecido pelo unique_ptr, retornando um bool valor que indica se ou não o objeto foi redefinido com um identificador válido. Isto vem a calhar com a manipulação de erro, ao qual voltarei daqui a pouco. A função de lançamento agora deve ser óbvia:
Type release() throw()
{
auto value = m_value;
m_value = Traits::invalid();
return value;
}
Copie vs. Mover
O toque final é considerar a cópia versus mudança semântica. Porque eu já fui banido cópia semântica para alças, faz sentido permitir mover semântica. Isto torna-se essencial se você quiser armazenar identificadores em contêineres STL. Estes contentores tradicionalmente têm contado sobre semântica de copiar, mas com a introdução do C++ 2011, mover semântica é suportada.
Sem entrar em uma longa descrição de referências de semântica e rvalue do movimento, a idéia é permitir que o valor de um objeto para passar de um objeto para outro em uma forma previsível para o desenvolvedor e coerente para a biblioteca autores e compiladores.
Antes de 2011 C++, desenvolvedores tinham de recorrer a todos os tipos de truques complicados para evitar o carinho excessivo que o idioma — e por extensão a STL — tem para copiar objetos. O compilador seria muitas vezes criar uma cópia de um objeto e, em seguida, imediatamente destruir o original. Com semântica de mover que o desenvolvedor pode declarar que um objeto já não será usado e seu valor movido em outro lugar, muitas vezes com tão pouco como uma troca de ponteiro.
Em alguns casos o desenvolvedor precisa ser explícito e indicar isso; mas mais frequentemente do que não o compilador pode tirar vantagem do reconhecimento de mover objetos e realizar otimizações insanamente eficientes que nunca eram possíveis antes. A boa notícia é que move semântica para suas próprias classes a habilitação é simples. Apenas como copiar se baseia em um construtor de cópia e um operador de atribuição de copiar, mover semântica se baseia em um construtor de movimento e um operador de atribuição de movimento:
unique_handle(unique_handle && other) throw() :
m_value(other.release())
{
}
unique_handle & operator=(unique_handle && other) throw()
{
reset(other.release());
return *this;
}
A referência de rvalue
C++ 2011 introduz um novo tipo de referência, chamado uma referência rvalue. Ele é declarado usando & &; Isso é o que está sendo usado em Membros unique_handle no código anterior. Embora semelhante para referências de idade, agora chamado lvalue referências, as novas referências rvalue exibem um pouco diferentes regras quando se trata de inicialização e resolução de sobrecarga. Por agora, eu vou deixá-lo no que (eu vou voltar a este tema mais tarde). O benefício principal nesta fase de um identificador com semântica de movimento é que você pode corretamente e eficientemente armazenar identificadores em contêineres STL.
Tratamento de erros
Isso é tudo para o modelo de classe unique_handle. O tópico final este mês — e se preparar para as colunas à frente — é o tratamento de erros. Poderia debater interminavelmente sobre os prós e os contras de exceções contra códigos de erro, mas se você deseja abraçar as bibliotecas C++ padrão apenas terá de se acostumar com exceções. Naturalmente, a API do Windows usa códigos de erro, portanto, é necessário um compromisso.
Minha abordagem para manipulação de erros é fazer o mínimo possível e escrever código seguro de exceção, mas evitar a captura de exceções. Se não houver nenhuma manipuladores de exceção, o Windows irá gerar automaticamente um relatório de erro que inclui um minidump do acidente que você pode depurar postmortem. Lançar exceções apenas quando erros de tempo de execução inesperado ocorrerem e lidar com tudo o resto com códigos de erro. Quando uma exceção é descartada, você sabe que ele é um bug em seu código ou alguma catástrofe que recaiu o computador.
O exemplo que eu gostaria de dar é o de acessar o registro do Windows. Não escrever um valor no registro normalmente é um sintoma de um problema maior que vai ser difícil lidar com bom senso em seu programa. Isso deve resultar em uma exceção. Falhar ler um valor do registro, no entanto, deve ser antecipado e manipulado graciosamente. Isso não deve resultar em uma exceção, mas retornam um valor bool ou enum indicando se ou porque o valor não pôde ser lida.
A API do Windows não é particularmente consistente com seu erro de manipulação; que é o resultado de uma API que evoluiu ao longo dos anos. Na maior parte, os erros são retornados como valores BOOL ou HRESULT. Existem alguns outros, que tendem a manipular explicitamente, comparando o valor de retorno contra valores documentadas.
Se eu decidir uma chamada de função determinada deve ter êxito para o meu programa para continuar a funcionar de forma confiável, eu uso uma das funções listadas na Figura 2 para verificar o valor de retorno.
Figura 2 verificar a valor de retorno
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);
}
}
Há duas coisas que vale a pena mencionar sobre essas funções. A primeira é que a função de check_bool está sobrecarregada para que você também pode verificar a validade de um objeto identificador, que, com razão, não permite a conversão implícita de BOOL. A segunda é a função check_hr, que compara explicitamente contra S_OK em vez de usar a macro teve êxito mais comuns. Isso evita silenciosamente aceitando outros códigos de sucesso duvidoso, como S_FALSE, que quase nunca é o que o desenvolvedor espera.
Verificar a minha primeira tentativa de escrever estas funções foi um conjunto de sobrecargas. Mas como eu usei-los em vários projetos, percebi que a API do Windows simplesmente define muitos tipos de resultado e macros, assim que criar um conjunto de sobrecargas que iria trabalhar para todos eles simplesmente não é possível. Portanto, os nomes de função decorado. Eu encontrei alguns casos onde erros não foram sendo capturados devido à resolução de Sobrecarga inesperado. O tipo de check_failed está sendo lançado é bastante simple:
struct check_failed
{
explicit check_failed(long result) :
error(result)
{
}
long error;
};
Eu podia decorá-lo com todos os tipos de recursos de fantasia, como adicionar suporte para mensagens de erro, mas o que é o ponto? Posso incluir o valor de erro para que eu possa facilmente pegá-lo ao realizar uma autópsia em uma aplicação deixada de funcionar. Além disso, ele só vai ficar no caminho.
Tendo em conta estas Verifique funções, posso criar um objeto de evento e de sinal, gerar uma exceção se algo der errado:
handle h(CreateEvent( ...
));
check_bool(h);
check_bool(SetEvent(h.get()));
Manipulação de exceção
O outro problema com manipulação de exceção diz respeito à eficiência. Novamente, os desenvolvedores são divididos, mas mais frequentemente do que não porque eles mantêm alguns pressuposto não com base na realidade.
O custo de manipulação de exceção surge em duas áreas. O primeiro é gerar exceções. Isso tende a ser mais lento do que usando códigos de erro e é uma das razões que você só deve lançar exceções quando ocorre um erro fatal. Se tudo correr bem, você nunca vai pagar esse preço.
A segunda e mais comum, causa de problemas de desempenho tem a ver com a sobrecarga de tempo de execução de assegurar que os destruidores apropriados são chamados, no caso improvável de uma exceção é descartada. Código é necessária para controlar quais destruidores precisam ser executado; naturalmente, isso também aumenta o tamanho da pilha, que em bases de código grande pode afetar significativamente o desempenho. Observe que você pagar esse custo ou não uma exceção é lançada, assim minimizar isso é essencial para garantir um bom desempenho.
Que significa garantir que o compilador tem uma boa idéia de quais funções potencialmente pode lançar exceções. Se o compilador pode provar que não vai haver quaisquer excepções a certas funções, pode otimizar o código gera para definir e gerenciar a pilha. Eis porque eu decorado o modelo de classe do identificador inteiro e traços de classe funções de membro com a especificação de exceção. Embora preterido em C++ 2011, é uma importante otimização específicos da plataforma.
Isso é tudo para este mês. Agora você tem um dos ingredientes principais para escrever programas confiáveis usando a API do Windows. Join me próximo mês como eu começar a explorar o Thread Pool de API do Windows.
Kenny Kerr é um artesão de software com uma paixão para desenvolvimento Windows nativo. Contatá-lo em kennykerr.ca.