const, constexpr e consteval
Conforme apresentado no capítulo 2.6, o modificador const
pode ser utilizado para criar constantes (variáveis com acesso apenas de leitura). Alterar uma variável que contém o modificador const
causa um erro de compilação:
int main()
{
const int i = 10;
i = 11; // Erro de compilação: Atribuição à uma variável somente-leitura
return 0;
}
Este capítulo apresenta a diferença entre os modificadores const
, constexpr
e consteval
, considerando seu uso em relação à variáveis e funções. O uso do modificador constinit
e do modificador const
em relação a métodos de classes será abordado apenas em capítulos posteriores.
Para compreender a diferença entre os modificadores, considere o seguinte código:
#include <iostream>
int sum(int a, int b) // Case (a)
// constexpr int sum(int a, int b) // Caso (b)
{
int c = a + b;
return c;
}
int main()
{
const int a = 1;
// int a = 1; // Caso (c)
int x = sum(a, 2);
std::cout << x;
}
Ao invocar a função sum
, a soma dos valores a
e b
será computada. Esse calculo será feito em tempo de execução (runtime). Isso significa que a soma será efetuada apenas quando o usuário executar o programa. Por outro lado, ao utilizar constexpr
(introduzido no C++11) na função sum
, a função potencialmente será executada em tempo de compilação (compile time), ou seja, o valor de x
talvez seja computado no momento de compilação do programa, caso as informações necessárias para tal estejam disponíveis.
Para compreender esse processo, é importante lembrar que todo código C++ é compilado, gerando um arquivo binário que contém o código de máquina. Esse arquivo binário contém instruções em linguagem de baixo nível específicas para cada processador. Mostra-se abaixo uma comparação de dois trechos de código de baixo nível gerados pelo exemplo anterior para o caso (a) sem o uso de constexpr
e para o caso (b) com o uso de constexpr
:
a) Sem o uso de constexpr
:
mov esi, 2
mov edi, 1
call sum(int, int)
mov DWORD PTR [rbp-8], eax
b) Com o uso de constexpr
:
mov DWORD PTR [rbp-8], 3
Não se preocupe caso não compreenda totalmente os códigos de máquina apresentados acima; trata-se de uma linguagem chamada assembly, extremamente próxima da linguagem de máquina. O código assembly está sendo usado apenas para ilustrar a diferença de código gerado: menos linhas aqui significa código mais eficiente. No caso (a), a linha call sum(int, int)
mostra que a função sum
será de fato invocada em tempo de execução. Já no caso (b), uma única linha é gerada, contendo o resultado (3
) que foi, portanto, calculado em tempo de compilação e por isso aparece diretamente no código gerado.
É importante notar que constexpr
só será executado em tempo de compilação caso seja possível conhecer todos os dados necessários para sua execução. Ainda no exemplo, considerando o caso (c), nota-se que utilizar uma variável não-const
em uma função contendo o modificador constexpr
ocasionará no seguinte código de máquina gerado:
c) Com uso de constexpr
, porém, a variável a
não é const
mov DWORD PTR [rbp-4], 1
mov eax, DWORD PTR [rbp-4]
mov esi, 2
mov edi, eax
call sum(int, int)
mov DWORD PTR [rbp-8], eax
Uma forma de evitar que uma função constexpr
gere valores em tempo de execução inesperadamente, é utilizar consteval
(a partir do C++20). Diferente de constexpr
, consteval
ocasiona em um erro de compilação caso não consiga processar a função em tempo de compilação. Por exemplo:
#include <iostream>
consteval int sum(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
// int a = 1; // Caso (a), gera erro de compilação
constexpr int a = 1; // Caso (b)
// const int a = 1; // Caso (c)
int x = sum(a, 2);
std::cout << x;
}
No exemplo acima, o caso (a) ocasionará em um erro de compilação. Os casos (b) e (c) geram o valor da variável x
em tempo de compilação garantidamente. Note que, apesar dos casos (b) e (c) produzirem o mesmo código nesse caso, o uso de const
e de constexpr
não é equivalente, como será visto a seguir.
Basicamente, o modificador const
aplicado a uma variável declara que aquela variável não será modificada em tempo de execução. O modificador constexpr
aplicado a uma variável declara que aquela variável será computada em tempo de compilação e não será modificada em tempo de execução. Dessa forma, o caso (a) abaixo irá compilar normalmente, pois a variável a
irá conter uma cópia constante da variável var
, em tempo de execução. Já o caso (b) não irá compilar, pois a variável a
não pode ser processada em tempo de compilação, visto que a variável var
poderia ter sido modificada em algum momento no código.
int main()
{
int var = 1;
const int a = var; // Caso (a)
// constexpr int a = var; // Caso (b)
}