Usando macro X no código C ++ moderno

As tendências modernas de desenvolvimento de C ++ sugerem a máxima rejeição possível de macros no código. Mas às vezes sem macros, e em sua manifestação especialmente feia, não se pode fazer, pois sem elas é ainda pior. Sobre isso e a história.

Como você sabe, a primeira etapa na compilação de C e C ++ é o pré-processador, que substitui as diretivas de macros e pré-processador por texto sem formatação.

Isso nos permite fazer coisas estranhas, por exemplo, como:

// xmacro.h "look, I'm a string!" // xmacro.cpp std::string str = #include "xmacro.h" ; 

Após o pré-processador funcionar, esse mal-entendido se transformará no código correto:

 std::string str = "look, I'm a string!" ; 

Obviamente, esse cabeçalho terrível não pode ser incluído em nenhum outro lugar. E sim, devido ao fato de adicionarmos esse cabeçalho várias vezes ao mesmo arquivo - sem #pragma uma vez ou incluir guardas.

Na verdade, vamos escrever um exemplo mais complexo que fará coisas diferentes com a ajuda de macros e, ao mesmo tempo, defenderá #include aleatório:

 // xmacro.h #ifndef XMACRO #error "Never include me directly" #endif XMACRO(first) XMACRO(second) #undef XMACRO // xmacro.cpp enum class xenum { #define XMACRO(x) x, #include "xmacro.h" }; std::ostream& operator<<(std::ostream& os, xenum enm) { switch (enm) { #define XMACRO(x) case xenum::x: os << "xenum::" #x; break; #include "xmacro.h" } return os; } 

Isso ainda é feio, mas um certo encanto já aparece: quando você adiciona um novo elemento à classe enum, ele será automaticamente adicionado à instrução de saída sobrecarregada.

Aqui você pode formalizar a área de aplicação desse método: a necessidade de geração de código em locais diferentes, com base em uma fonte.

E agora a triste história do X-Macro e Windows. Existe um sistema como o Windows Performance Counters, que permite enviar determinados contadores ao sistema operacional para que outros aplicativos possam buscá-los. Por exemplo, o Zabbix pode ser configurado para coletar e monitorar qualquer contador de desempenho. Isso é bastante conveniente e você não precisa reinventar a roda com o retorno / consulta de dados.

Sinceramente, pensei que a adição de um novo contador parece a HANDLE counter = AddCounter ("name"). Ah, como eu estava errado.

Primeiro, você precisa escrever um manifesto XML especial ( exemplo ) ou gerá-lo usando o programa ecmangen.exe no Windows SDK, mas por algum motivo esse ecmangen foi removido das novas versões do Windows 10 SDK. Em seguida, você precisa gerar o código e o arquivo .rc usando o utilitário ctrpp com base em nosso manifesto XML. A adição de novos contadores ao próprio sistema é feita apenas com o utilitário lodctr com nosso manifesto XML no argumento.

O que é um arquivo .rc?
Esta é uma invenção da Microsoft, não relacionada ao C ++ padrão. Usando esses arquivos, você pode incorporar recursos em exe \ dll, como strings \ icons \ pictures etc., e depois buscá-los usando a API especial do Windows.

Os Perfcounters usam esses arquivos .rc para localizar nomes de contadores, e não está muito claro por que esses nomes devem ser localizados.

Resumindo o acima: para adicionar 1 contador, você precisa:

  1. Alterar manifesto XML
  2. Gere novos arquivos de projeto .c e .rc com base no manifesto
  3. Escreva uma nova função que incrementará um novo contador
  4. Escreva uma nova função que terá o valor do contador

Total: 4-5 arquivos modificados em diff-e, para obter um único contador e sofrer constantemente ao trabalhar com o manifesto XML, que é a fonte de informações no código positivo. É isso que a Microsoft nos oferece.

Na verdade, a solução inventada parece assustadora, mas a adição de um novo contador é feita exatamente 1 linha em um arquivo. Além disso, tudo é gerado automaticamente usando macros e, infelizmente, um script de pré-compilação, pois o manifesto XML ainda é necessário, embora agora não seja o principal.

Nosso perfcounters_ctr.h parece quase idêntico ao exemplo acima:

 #ifndef NV_PERFCOUNTER #error "You cannot do this!" #endif ... NV_PERFCOUNTER(copied_bytes) NV_PERFCOUNTER(copied_files) ... #undef NV_PERFCOUNTER 

Como escrevi anteriormente, a adição de contadores é feita carregando o manifesto XML usando lodctr.exe. No nosso programa, podemos apenas inicializá-los e modificá-los.

Os fragmentos de inicialização que nos interessam no abridor gerado são assim:

 #define COPIED_BYTES 0 //     0 #define COPIED_FILES 1 //      const PERF_COUNTERSET_INFO counterset_info{ ... 2, //    XML-  ... }; struct { PERF_COUNTERSET_INFO set; PERF_COUNTER_INFO counters[2]; //     } counterset { counterset_info, { //     { COPIED_BYTES, ... }, { COPIED_FILES, ... } } } 

Total: precisamos de uma correspondência no formato “nome do contador - índice crescente” e, no estágio de compilação, é necessário conhecer o número de contadores e coletar uma matriz de inicialização dos índices do contador. É aqui que a macro X é resgatada.

A correspondência de um nome de contador ao seu índice crescente é bastante simples.

O código abaixo se transformará em uma classe enum, cujos índices internos iniciam em 0 e aumentam em um. Adicionando o último elemento com as mãos, descobrimos imediatamente quantos contadores totais temos:

 enum class counter_enum : int { #define NV_PERFCOUNTER(ctr) ctr, #include "perfcounters_ctr.h" total_counters }; 

Além disso, com base em nossa enumeração, precisamos inicializar os contadores:

 static constexpr int counter_count = static_cast<int>(counter_enum::total_counters); const PERF_COUNTERSET_INFO counterset_info{ ... counter_count, ... }; struct { PERF_COUNTERSET_INFO set; PERF_COUNTER_INFO counters[counter_count]; } counterset { counterset_info, { //     #define NV_PERFCOUNTER(ctr) \ { static_cast<int>(counter_enum::ctr), ... }, #include "perfcounters_ctr.h" } } 

O resultado foi que a inicialização de um novo contador agora ocupa 1 linha e não requer alterações adicionais em outros arquivos (anteriormente, cada regeneração alterava três partes do código apenas na inicialização).

E vamos adicionar uma API conveniente para incrementar contadores. Algo no espírito:

 #define NV_PERFCOUNTER(ctr) \ inline void ctr##_tick(size_t diff = 1) { /*   counter_enum::ctr */ } #include "perfcounters_ctr.h" #define NV_PERFCOUNTER(ctr) \ inline size_t ctr##_get() { /*    counter_enum::ctr */ } #include "perfcounter_ctr.h" 

O pré-processador irá gerar belos getters / setters para nós, que podemos usar imediatamente no código, por exemplo:

 inline void copied_bytes_tick(size_t diff = 1); inline size_t copied_bytes_get(); 

Mas ainda temos duas coisas tristes: o manifesto XML e o arquivo .rc (infelizmente, é necessário).

Nós simplificamos bastante - um script de pré-compilação que lê o arquivo original com macros definindo contadores, analisa o que está entre "NV_COUNTER (" e ")" e, com base nisso, gera os dois arquivos que estão no .gitignore, para que não desarrume diffs.

Foi : Software especial baseado no código de codificação gerado pelo manifesto XML. Muitas mudanças no projeto para cada adição / remoção do contador.

Agora: o script de pré-processador e pré-construção gera todos os contadores, o manifesto XML e o arquivo .rc. Exatamente uma linha no diff-e para adicionar / remover um contador. Agradeço ao pré-processador que ajudou a resolver esse problema, mostrando neste caso em particular mais bem do que mal.

Source: https://habr.com/ru/post/pt475162/


All Articles