No desenvolvimento de software para sistemas embarcados, deslocar operações para a fase de compilação pode reduzir significativamente o tamanho do binário, garantir comportamentos de inicialização previsíveis e minimizar a sobrecarga em tempo de execução. Com o advento do C++20, dois novos mecanismos linguísticos tornam-se particularmente úteis para atingir esses objetivos: consteval e constinit. Embora possam parecer meros detalhes sintáticos, eles resolvem problemas práticos como a geração de tabelas de consulta em tempo de compilação, o controle da inicialização de variáveis com armazenamento estático e a extração de lógicas imutáveis do código de execução.
consteval: Funções de Avaliação Imediata
O especificader consteval declara uma função ou construtor que deve obrigatoriamente ser avaliada durante a compilação. Em contraste com constexpr, que permite avaliação tanto em tempo de compilação quanto de execução, consteval restringe a chamada a contextos de expressão constante. Qualquer tentativa de avaliação em tempo de execução resultará em um erro de compilação.
Cálculo de Fatorial em Tempo de Compilação
Um uso comum é gerar valores constantes para dimensionar estruturas de dados, evitando cálculos redundantes em tempo de execução.
#include <array>
#include <cstddef>
consteval std::size_t calcular_fatorial(std::size_t valor) {
return valor <= 1 ? 1 : valor * calcular_fatorial(valor - 1);
}
const std::size_t TAMANHO = calcular_fatorial(6); // Avaliação em tempo de compilação
static_assert(TAMANHO == 720);
std::array<int, TAMANHO> tabela_lut{}; // Tamanho determinado em tempo de compilação
Geração de Identificadores por Hash em Tempo de Compilação
Em protocolos embarcados, mapear strings de comandos para identificadores numéricos de forma segura é essencial. A seguir, um exemplo que calcula hashes FNV-1a durante a compilação, permitindo a detecção de colisões através de static_assert.
#include <cstdint>
#include <cstddef>
consteval std::uint32_t hash_fnv1a32(const char* dados, std::size_t comprimento) {
std::uint32_t semente = 0x811c9dc5u;
for (std::size_t i = 0; i < comprimento; ++i) {
semente ^= static_cast<std::uint8_t>(dados[i]);
semente *= 0x01000193u;
}
return semente;
}
template <std::size_t N>
consteval std::uint32_t obter_id_literal(const char (&literal)[N]) {
// N inclui o caractere nulo final '\0'
return hash_fnv1a32(literal, N - 1);
}
// Exemplo de uso
constexpr auto id_ligar_led = obter_id_literal("LIGAR_LED");
constexpr auto id_desligar_led = obter_id_literal("DESLIGAR_LED");
static_assert(id_ligar_led != id_desligar_led); // Garantia de unicidade em tempo de compilação
Construtores consteval para Metadados de Compilação
É possível aplicar consteval a construtores, forçando que instâncias de uma classe sejam criadas exclusivamente durante a compilação. Isso é útil para tipos que representam metadados ou descrições estáticas.
#include <cstdint>
struct DescricaoTag {
const char* nome;
std::uint32_t codigo;
consteval DescricaoTag(const char* n, std::uint32_t c) : nome(n), codigo(c) {}
};
consteval DescricaoTag criar_descricao(const char* texto, std::uint32_t valor) {
return DescricaoTag{texto, valor};
}
constexpr auto DESC_EXEMPLO = criar_descricao("EXEMPLO", 0x01);
// DescricaoTag erro{"RUNTIME", 0x02}; // Erro: construtor é consteval
Controle de Fluxo com if consteval
A instrução if consteval permite bifurcar a lógica dentro de uma função constexpr dependendo do contexto de avaliação. Diferente de if constexpr, que depende de propriedades de tipos ou parâmetros de template, if consteval verifica se a avaliação ocorre em tempo de compilação.
#include <string_view>
constexpr std::string_view mensagem_sistema() {
if consteval {
return "Modo de depuracao: compilacao";
} else {
return "Modo de producao: execucao";
}
}
// Ao usar em um contexto constexpr, a primeira ramificação é selecionada.
constexpr auto msg_compilacao = mensagem_sistema(); // Resultado: "Modo de depuracao: compilacao"
constinit: Garantindo a Forma de Inicialização Estática
O especificador constinit é aplicado a variáveis com duração de armazenamento estático ou de thread para exigir que sua inicialização seja uma inicialização constante (ou pelo menos não dinâmica). Se a variável requer inicialização dinâmica (por exemplo, através de um construtor não constexpr), o programa será considerado malformado. Isso previne o chamado "desastre de ordem de inicialização estática" (SIOF), em que dependências entre variáveis estáticas em diferentes unidades de tradução levam a comportamentos indefinidos.
Prevenção de Inicialização Dinâmica Acidental
constinit atua como uma verificação em tempo de compilação para garantir que uma variável global ou estática tenha seu valor inicial determinado durante a carga do programa.
#include <array>
// Esta tabela deve ser inicializada em tempo de carga e pode ser modificada depois.
constinit std::array<int, 4> tabela_global = {10, 20, 30, 40}; // OK: inicialização constante (agregado)
// Se a inicialização exigir execução, o compilador emitirá um erro.
// int obter_valor_runtime();
// constinit std::array<int,4> tabela_erro = [](){ return std::array<int,4>{ obter_valor_runtime() }; }(); // Erro
Relação entre constinit e constexpr
Variáveis constexpr implicam inicialização constante e são imutáveis. constinit, por outro lado, é usado para variáveis mutáveis que ainda assim precisam de uma inicialização determinada em tempo de compilação. Usar ambos na mesma declaração é redundante e geralmente não faz sentido, pois constexpr já garante a imutabilidade e a inicialização constante.
Evitando Problemas de Ordem de Inicialização
Em projetos com múltiplos arquivos de código, constinit força o uso de inicializações constantes, eliminando dependências em tempo de execução entre variáveis estáticas. Isso transforma erros sutis de SIOF em erros de compilação claros, facilitando a manutenção de grandes bases de código embarcado.
// Em um projeto com múltiplos arquivos:
// Arquivo: config.cpp
constinit int configuracao_global = 0x1F; // Inicialização constante garantida.
// Arquivo: init.cpp
// Se tentarmos inicializar configuracao_global com uma função não constante, obteremos um erro.
// int calcular_config(); // Função não constexpr.
// constinit int configuracao_global = calcular_config(); // Erro: inicialização dinâmica proibida.
Na prática, integrar consteval e constinit no fluxo de trabalho de desenvolvimento embarcado permite externalizar lógicas de geração de dados (como tabelas e hashes) para a fase de compilação, enquanto garante que variáveis críticas para a inicialização do sistema tenham seus valores fixados de maneira segura e previsível.