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.

Você vai aprender
  • A diferença entre "tempo de compilação" e "tempo de execução".
  • Variáveis constexpr e 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 int que, na execução, passaria em silêncio.
  • consteval C++20: "só na compilação", e o erro real ao chamar com um valor da execução.
  • constinit (só o nome) e if 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çãoTempo de execução
QuandoQuando você roda o g++Quando você roda ./programa
Quem calculaO compiladorO processador, com o programa pronto
Quantas vezesUma vezToda vez que o programa roda
ConheceSó o que está escrito no códigoTambém o que o usuário digita, arquivos, a hora…
Na analogiaImprimir a tabuadaCalcular 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 constexpr na 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 calcular fatorial(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 constexpr serve.
fatorial(n)
Com n vindo 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: n só 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çãoSimSim
Pode rodar na execuçãoSimNão: é erro de compilação
DesdeC++11 (bem mais flexível a partir do C++14)C++20
Quando usarQuase sempre: funções de conta simples que podem servir aos dois mundosQuando 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 momentosconstexpr na função
Uma função que nunca pode rodar na execuçãoconsteval
Conferir algo na compilaçãostatic_assert(condição);
Variável global que começa pronta mas muda depoisconstinit

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

EscritaO 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:

(dobro(4) == 8);

2. Só na compilação

Complete para que cubo nunca possa rodar na execução:

int cubo(int x) { return x * x * x; }

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. constexpr adianta 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 int vira 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: if decidido na compilação, para templates.