C++ constexpr e consteval
Quando você compila um programa, o compilador já lê todo o código. Se uma conta só depende de valores escritos no próprio código (quantos segundos tem um dia, o fatorial de 10, o tamanho de uma tabela), por que fazer essa conta toda vez que o programa roda? Com constexpr, você pede ao compilador: "calcule isto agora, na compilação, e coloque só o resultado no programa". Nesta aula você vai provar que isso acontece mesmo, ver o compilador pegar erros que passariam despercebidos na execução e conhecer o consteval, que obriga o cálculo a ser feito na compilação.
- A diferença entre "tempo de compilação" e "tempo de execução".
- Variáveis
constexpre o que pode (e não pode) ir nelas. - Funções
constexpr: as mesmas funções servem na compilação e na execução. - Como provar que algo foi calculado na compilação, com
static_assert. - O compilador pegando um estouro de
intque, na execução, passaria em silêncio. constevalC++20: "só na compilação", e o erro real ao chamar com um valor da execução.constinit(só o nome) eif constexpr(só a ideia).
Os programas foram compilados com o g++ 15.2 na máquina fictícia da Ana, na pasta ~/cpp, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa. A aula Constantes (const e constexpr) já apresentou constexpr em variáveis simples; aqui vamos mais fundo, com funções. Você vai precisar de Funções e for.
A ideia: a tabuada impressa
A professora da Ana pode fazer duas coisas: pedir que cada aluno calcule 7 × 8 na hora, toda vez que precisar, ou entregar uma tabuada já impressa. A tabuada impressa foi calculada uma vez só, antes da aula; durante a aula, basta olhar.
| Tempo de compilação | Tempo de execução | |
|---|---|---|
| Quando | Quando você roda o g++ | Quando você roda ./programa |
| Quem calcula | O compilador | O processador, com o programa pronto |
| Quantas vezes | Uma vez | Toda vez que o programa roda |
| Conhece | Só o que está escrito no código | Também o que o usuário digita, arquivos, a hora… |
| Na analogia | Imprimir a tabuada | Calcular de cabeça na hora |
constexpr vem de constant expression, "expressão constante": uma expressão cujo valor pode ser descoberto na compilação.
1. Variáveis constexpr
Uma variável constexpr é uma constante cujo valor tem de ser conhecido na compilação:
constexpr int dias_semana = 7; constexpr int segundos_dia = 24 * 60 * 60; // o compilador faz a conta: 86400 constexpr double pi = 3.14159;
Por ser constante, não dá para mudar depois (o compilador recusa, como com const). E por precisar ser conhecida na compilação, ela não pode depender de algo que só existe na execução, como o que o usuário digita:
#include <iostream>
int main() {
int idade = 0;
std::cin >> idade;
constexpr int dobro = idade * 2;
std::cout << dobro << '\n';
}
$ g++ -std=c++23 -Wall -Wextra nao_constante.cpp -o nao_constante
nao_constante.cpp: In function ‘int main()’:
nao_constante.cpp:6:27: error: the value of ‘idade’ is not usable in a constant expression
6 | constexpr int dobro = idade * 2;
| ^~~~~
nao_constante.cpp:4:9: note: ‘int idade’ is not const
4 | int idade = 0;
| ^~~~~
The value of ‘idade’ is not usable in a constant expression = "o valor de idade não pode ser usado numa expressão constante". Faz sentido: na hora de compilar, ninguém digitou idade nenhuma ainda. Se o valor vem da execução, use const (constante, mas calculada na execução) ou uma variável comum.
2. Funções constexpr
A parte mais poderosa: funções. Uma função marcada com constexpr pode ser executada pelo compilador, desde que receba valores conhecidos na compilação. E a mesma função continua funcionando normalmente na execução:
#include <array>
#include <iostream>
constexpr long long fatorial(int n) {
long long resultado = 1;
for (int i = 2; i <= n; ++i) {
resultado *= i;
}
return resultado;
}
int main() {
constexpr long long f10 = fatorial(10); // calculado na compilação
static_assert(fatorial(5) == 120); // conferido na compilação
std::array<int, fatorial(4)> caixas{}; // tamanho 24, conhecido na compilação
std::cout << "10! = " << f10 << '\n';
std::cout << "Caixas: " << caixas.size() << '\n';
int n = 0;
std::cout << "Fatorial de quanto? ";
std::cin >> n;
std::cout << n << "! = " << fatorial(n) << '\n'; // calculado na execução
}
$ g++ -std=c++23 -Wall -Wextra fatorial.cpp -o fatorial $ echo 15 | ./fatorial 10! = 3628800 Caixas: 24 Fatorial de quanto? 15! = 1307674368000
- constexpr long long fatorial(int n)
- Uma função comum, com
constexprna frente. O corpo pode ter variáveis,for,if… (aula Funções). O fatorial de n é 1 × 2 × 3 × … × n. - constexpr long long f10 = fatorial(10);
- Como
f10éconstexpr, o compilador é obrigado a calcularfatorial(10)na compilação. O programa final contém só o número 3628800. - static_assert(fatorial(5) == 120);
- Uma conferência feita pelo compilador (assert = "afirmar"). Se a condição for falsa, o programa nem compila.
- std::array<int, fatorial(4)>
- O tamanho de um std::array precisa ser conhecido na compilação. Uma função
constexprserve. - fatorial(n)
- Com
nvindo do teclado, a mesma função roda normalmente, na execução.
O echo 15 | só "digita" 15 para o programa (é por isso que o 15! = … aparece colado na pergunta: o 15 não foi digitado no terminal, então não aparece na tela).
constexpr na função é uma permissão, não uma ordem
Marcar a função como constexpr diz "ela pode rodar na compilação". Quem decide se roda é o lugar onde ela é chamada: num lugar que exige uma constante (constexpr, static_assert, tamanho de array), ela roda na compilação; com um valor da execução, roda na execução. Com otimização (-O2), o compilador ainda pode adiantar outras contas por conta própria, mas aí é escolha dele, sem garantia.
A prova: static_assert
Como saber que o compilador calculou mesmo? Faça-o conferir uma resposta errada. Se ele reclama na compilação, é porque fez a conta:
constexpr long long fatorial(int n) {
long long resultado = 1;
for (int i = 2; i <= n; ++i) {
resultado *= i;
}
return resultado;
}
static_assert(fatorial(5) == 100);
int main() {}
$ g++ -std=c++23 -Wall -Wextra prova.cpp -o prova
prova.cpp:9:27: error: static assertion failed
9 | static_assert(fatorial(5) == 100);
| ~~~~~~~~~~~~^~~~~~
prova.cpp:9:27: note: the comparison reduces to ‘(120 == 100)’
Leia a nota: the comparison reduces to ‘(120 == 100)’ = "a comparação se reduz a 120 == 100". O compilador rodou o laço do fatorial, chegou em 120 e só então viu que não era 100. Nenhum ./prova foi executado: o programa nem chegou a existir.
O static_assert é ótimo para deixar "testes" dentro do próprio código: se alguém mudar a função e quebrar o resultado, o programa para de compilar na hora.
Missão: este programa compila e roda. Provoque o erro: troque o 120 do static_assert por 100 e clique em Rodar. O editor usa o Clang, que escreve a mensagem de outro jeito (static assertion failed due to requirement… e expression evaluates to '120 == 100'), mas a ideia é a mesma.
#include <iostream>
constexpr long long fatorial(int n) {
long long resultado = 1;
for (int i = 2; i <= n; ++i) {
resultado *= i;
}
return resultado;
}
static_assert(fatorial(5) == 120);
int main() {
std::cout << "5! = " << fatorial(5) << '\n';
}
#include <iostream>
constexpr long long fatorial(int n) {
long long resultado = 1;
for (int i = 2; i <= n; ++i) {
resultado *= i;
}
return resultado;
}
static_assert(fatorial(5) == 100);
int main() {
std::cout << "5! = " << fatorial(5) << '\n';
}
No laboratório, o mesmo erro com o clang++ 21:
$ clang++ -std=c++23 -Wall -Wextra prova2.cpp -o prova2
prova2.cpp:11:15: error: static assertion failed due to requirement 'fatorial(5) == 100'
11 | static_assert(fatorial(5) == 100);
| ^~~~~~~~~~~~~~~~~~
prova2.cpp:11:27: note: expression evaluates to '120 == 100'
11 | static_assert(fatorial(5) == 100);
| ~~~~~~~~~~~~^~~~~~
1 error generated.
O compilador pega erros que a execução esconde
Um int guarda até pouco mais de 2 bilhões (aula Inteiros), e 20! é bem maior que isso. Veja a mesma função com int, primeiro na execução:
#include <iostream>
constexpr int fatorial(int n) {
int resultado = 1;
for (int i = 2; i <= n; ++i) {
resultado *= i;
}
return resultado;
}
int main() {
int n = 20;
std::cout << fatorial(n) << '\n';
}
$ g++ -std=c++23 -Wall -Wextra estouro_exec.cpp -o estouro_exec $ ./estouro_exec -2102132736 « um fatorial negativo! e nenhum aviso
O resultado estourou o int (isso é comportamento indefinido) e saiu um número negativo, sem nenhum aviso. Agora peça o mesmo cálculo na compilação, com constexpr int grande = fatorial(20);:
$ g++ -std=c++23 -Wall -Wextra estouro.cpp -o estouro
estouro.cpp: In function ‘int main()’:
estouro.cpp:10:36: in ‘constexpr’ expansion of ‘fatorial(20)’
estouro.cpp:4:19: error: overflow in constant expression [-fpermissive]
4 | resultado *= i;
| ~~~~~~~~~~^~~~
estouro.cpp:4:19: error: overflow in constant expression [-fpermissive]
« … a mesma linha se repete mais 6 vezes
Overflow in constant expression = "estouro numa expressão constante". Na compilação, o C++ não permite comportamento indefinido: o compilador vigia cada passo da conta e para no estouro. É uma vantagem a mais do constexpr: o que é calculado na compilação vem "conferido". (O [-fpermissive] é o nome de uma opção que rebaixaria esse erro a aviso; não use.)
3. consteval: só na compilação C++20
Às vezes você quer garantir que uma função nunca rode na execução: um cálculo que deve estar pronto antes, ou uma conferência de um valor escrito no código. Para isso existe consteval (constant evaluation, "avaliação constante"): toda chamada tem de ser calculada na compilação.
#include <iostream>
consteval int quadrado(int x) {
return x * x;
}
int main() {
constexpr int a = quadrado(7);
int b = quadrado(3) + 1;
std::cout << a << ' ' << b << '\n';
}
$ g++ -std=c++23 -Wall -Wextra quadrado.cpp -o quadrado $ ./quadrado 49 10
Mesmo em int b = quadrado(3) + 1;, uma variável comum, o quadrado(3) foi calculado na compilação, porque 3 é conhecido. Agora com um valor da execução:
#include <iostream>
consteval int quadrado(int x) {
return x * x;
}
int main() {
int n = 0;
std::cin >> n;
std::cout << quadrado(n) << '\n';
}
$ g++ -std=c++23 -Wall -Wextra consteval_erro.cpp -o consteval_erro
consteval_erro.cpp: In function ‘int main()’:
consteval_erro.cpp:10:26: error: call to consteval function ‘quadrado(n)’ is not a constant expression
10 | std::cout << quadrado(n) << '\n';
| ~~~~~~~~^~~
consteval_erro.cpp:10:27: error: the value of ‘n’ is not usable in a constant expression
10 | std::cout << quadrado(n) << '\n';
| ^
consteval_erro.cpp:8:9: note: ‘int n’ is not const
8 | int n = 0;
| ^
- call to consteval function ‘quadrado(n)’ is not a constant expression
- "A chamada à função consteval
quadrado(n)não é uma expressão constante": a chamada não pode ser feita na compilação. - the value of ‘n’ is not usable in a constant expression
- O motivo:
nsó vai ter valor quando o usuário digitar. - note: ‘int n’ is not const
- Uma pista:
né uma variável comum.
Com constexpr no lugar de consteval, esse programa compilaria e calcularia na execução. É essa a diferença:
constexpr (função) | consteval | |
|---|---|---|
| Pode rodar na compilação | Sim | Sim |
| Pode rodar na execução | Sim | Não: é erro de compilação |
| Desde | C++11 (bem mais flexível a partir do C++14) | C++20 |
| Quando usar | Quase sempre: funções de conta simples que podem servir aos dois mundos | Quando rodar na execução seria um erro de projeto |
Você já usou uma função consteval sem saber: o std::format (aula std::format e std::print) confere o texto de formato na compilação com um construtor consteval. É por isso que um {} malformado no format dá erro de compilação, e não erro ao rodar.
Missão: este programa não compila: o quadrado é consteval e recebe um número digitado. Troque uma palavra para que a função possa rodar também na execução. Com a entrada 7, a saída deve ser 49.
#include <iostream>
consteval int quadrado(int x) {
return x * x;
}
int main() {
int n = 0;
std::cin >> n;
std::cout << quadrado(n) << '\n';
}
#include <iostream>
constexpr int quadrado(int x) {
return x * x;
}
int main() {
int n = 0;
std::cin >> n;
std::cout << quadrado(n) << '\n';
}
4. constinit: só para conhecer C++20
Existe uma terceira palavra parecida, constinit ("inicialização constante"). Ela vale para variáveis globais (fora de funções) ou static, e garante que o valor inicial é calculado na compilação, mas a variável pode mudar depois:
#include <iostream>
constinit int visitas = 0;
int main() {
++visitas;
++visitas;
std::cout << "Visitas: " << visitas << '\n';
}
$ g++ -std=c++23 -Wall -Wextra constinit.cpp -o constinit $ ./constinit Visitas: 2
Ela resolve um problema de programas grandes, com variáveis globais em vários arquivos, que você dificilmente vai encontrar agora. Guarde só o resumo: constexpr = calculado na compilação e constante; constinit = começa calculado na compilação, mas pode mudar.
5. if constexpr: só a ideia
Um if comum decide na execução. O if constexpr C++17 decide na compilação, e o lado que não foi escolhido nem entra no programa. Isso é útil em templates, funções que funcionam com vários tipos:
#include <iostream>
#include <string>
#include <type_traits>
template <typename T>
void descrever(const T& valor) {
if constexpr (std::is_integral_v<T>) {
std::cout << valor << " é um número inteiro\n";
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << valor << " é um número com vírgula\n";
} else {
std::cout << "\"" << valor << "\" é outra coisa (tamanho " << valor.size() << ")\n";
}
}
int main() {
descrever(42);
descrever(3.14);
descrever(std::string("Ana"));
}
$ g++ -std=c++23 -Wall -Wextra se_constexpr.cpp -o se_constexpr $ ./se_constexpr 42 é um número inteiro 3.14 é um número com vírgula "Ana" é outra coisa (tamanho 3)
Repare no valor.size() do último ramo: um int não tem .size(). Com um if comum, descrever(42) nem compilaria, porque os três ramos precisariam fazer sentido para int. Com if constexpr, para int só o primeiro ramo existe. Os detalhes (template, std::is_integral_v) ficam para as aulas de Templates e Concepts.
Qual usar?
| Você quer… | Use |
|---|---|
| Uma constante com valor escrito no código (dias da semana, limites, π) | constexpr na variável |
| Uma constante que só se sabe na execução (o que o usuário digitou) | const |
| Uma função de conta que pode servir nos dois momentos | constexpr na função |
| Uma função que nunca pode rodar na execução | consteval |
| Conferir algo na compilação | static_assert(condição); |
| Variável global que começa pronta mas muda depois | constinit |
E no lugar de #define PI 3.14, o costume antigo do C, prefira constexpr double pi = 3.14;: tem tipo, respeita escopo e aparece nas mensagens de erro (aula O Pré-processador).
Quando dá errado
error: the value of ‘x’ is not usable in a constant expression
O que significa: algo que precisa ser conhecido na compilação (uma variável constexpr, um static_assert, o tamanho de um std::array, um argumento de função consteval) depende de uma variável comum, que só tem valor na execução.
Como consertar: se o valor realmente vem da execução, tire o constexpr da variável (use const) ou troque consteval por constexpr. Se o valor é fixo, declare a variável de origem como constexpr também.
error: static assertion failed / note: the comparison reduces to ‘(120 == 100)’
O que significa: a condição do static_assert é falsa. A nota mostra os valores que o compilador calculou.
Como consertar: descubra quem está errado, a função ou a expectativa, e corrija.
error: overflow in constant expression [-fpermissive]
O que significa: durante um cálculo na compilação, um número passou do limite do tipo. A linha in ‘constexpr’ expansion of ‘fatorial(20)’ diz qual chamada causou.
Como consertar: use um tipo maior (long long) ou valores menores. E fique feliz: na execução, o mesmo estouro teria passado em silêncio.
error: call to consteval function ‘…’ is not a constant expression
O que significa: uma função consteval foi chamada com algo que só se sabe na execução.
Como consertar: passe valores fixos, ou troque consteval por constexpr se a função também deve servir na execução.
Referência rápida
| Escrita | O que faz |
|---|---|
constexpr int n = 10; | Constante calculada na compilação |
constexpr int f(int x) { … } | Função que pode rodar na compilação ou na execução |
constexpr int v = f(5); | Obriga f(5) a ser calculado na compilação |
static_assert(condição); | Conferência na compilação; falsa = erro |
static_assert(condição, "mensagem"); | Idem, com sua mensagem |
consteval int f(int x) { … } | Função que só roda na compilação (C++20) |
constinit int g = 0; | Global com valor inicial da compilação, que pode mudar (C++20) |
if constexpr (condição) { … } | if decidido na compilação (C++17, para templates) |
Exercícios
1. Conferir na compilação
Complete para o compilador conferir que dobro(4) vale 8:
2. Só na compilação
Complete para que cubo nunca possa rodar na execução:
3. Uma função constexpr int f(int x) é chamada como f(n), com n lido do teclado. O que acontece?
4. Qual destas linhas não compila?
5. Por que constexpr int g = fatorial(20); (com int) dá erro, se na execução o mesmo cálculo "funciona"?
Missão final: potência na compilação
Missão: escreva a função constexpr long long potencia(long long base, int expoente) (multiplique base por ela mesma expoente vezes, começando de 1). Prove que ela funciona na compilação com static_assert(potencia(2, 10) == 1024);. A saída deve ser:
2^10 = 1024 3^4 = 81
#include <iostream>
// escreva a função potencia aqui
// e o static_assert aqui
int main() {
// constexpr long long kb = potencia(2, 10);
// std::cout << "2^10 = " << kb << '\n';
// std::cout << "3^4 = " << potencia(3, 4) << '\n';
}
#include <iostream>
constexpr long long potencia(long long base, int expoente) {
long long resultado = 1;
for (int i = 0; i < expoente; ++i) {
resultado *= base;
}
return resultado;
}
static_assert(potencia(2, 10) == 1024);
static_assert(potencia(5, 0) == 1);
int main() {
constexpr long long kb = potencia(2, 10);
std::cout << "2^10 = " << kb << '\n';
std::cout << "3^4 = " << potencia(3, 4) << '\n';
}
A solução, no laboratório:
$ g++ -std=c++23 -Wall -Wextra potencia.cpp -o potencia $ ./potencia 2^10 = 1024 3^4 = 81
Resumo
- Tempo de compilação = quando o
g++roda; tempo de execução = quando o programa roda.constexpradianta contas para a compilação. - Variável
constexpr: constante com valor conhecido na compilação; não pode depender do que o usuário digita. - Função
constexpr: pode rodar na compilação (quando o lugar exige uma constante) ou na execução (com valores comuns). static_assert(condição)confere na compilação e prova que o cálculo foi feito ali; a nota mostra os valores calculados.- Na compilação, comportamento indefinido como estouro de
intvira erro. consteval(C++20): só na compilação; chamar com valor da execução é erro.constinit: valor inicial da compilação, mas pode mudar.if constexpr:ifdecidido na compilação, para templates.