C++ Concepts C++20
Um template aceita "qualquer tipo" só no papel. Na prática, o corpo usa operações (%, >, .size()) que nem todo tipo tem, e quando você passa o tipo errado, o erro aparece lá dentro do template, longe da sua linha. Um concept (em português, "conceito") é uma regra escrita na entrada do template: "aqui só entram números inteiros". O compilador confere a regra antes de abrir o template, e o erro sai na sua chamada, dizendo qual regra falhou.
- O problema que os concepts resolvem, com um erro real.
- Como restringir um template:
template <std::integral T>. - As quatro formas de escrever a mesma regra, incluindo a cláusula
requires. - Os concepts prontos da biblioteca:
std::integral,std::floating_point,std::totally_orderede outros. - A mensagem de erro sem concept × com concept, lado a lado.
- Como escrever o seu próprio concept, com
concepterequires.
Todos os programas desta aula foram compilados com o g++ 15.2 na máquina fictícia da Ana, na pasta ~/cpp/conc, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa. Antes, leia Templates de Função: esta aula continua de onde ela parou.
A ideia: a placa na porta do brinquedo
No parque de diversões, a montanha-russa tem uma placa na entrada: "altura mínima: 1,40 m". O funcionário confere na fila. Sem a placa, a criança pequena entraria, sentaria, e só no meio da volta alguém perceberia que a trava não fecha. Bem pior.
Um template sem concept é o brinquedo sem placa: aceita qualquer tipo e só descobre o problema no meio do corpo. Um concept é a placa: template <std::integral T> diz "só entram tipos inteiros", e o compilador barra os outros na entrada, na linha em que você chamou.
1. O problema, sem concept
A Ana escreveu um template que diz se um número é par. Ele usa % (o resto da divisão, aula Operadores Aritméticos), que só existe para inteiros. Por engano, ela chamou com 7.5:
#include <iostream>
template <typename T>
bool eh_par(T numero) {
return numero % 2 == 0;
}
int main() {
std::cout << eh_par(10) << '\n';
std::cout << eh_par(7.5) << '\n';
}
$ g++ -std=c++23 -Wall -Wextra par-sem.cpp -o par-sem
par-sem.cpp: In instantiation of ‘bool eh_par(T) [with T = double]’:
par-sem.cpp:10:24: required from here
10 | std::cout << eh_par(7.5) << '\n';
| ~~~~~~^~~~~
par-sem.cpp:5:19: error: invalid operands of types ‘double’ and ‘int’ to binary ‘operator%’
5 | return numero % 2 == 0;
| ~~~~~~~^~~
O error: aponta a linha 5, dentro do template: "operandos inválidos (double e int) para o %". Mas a linha 5 está certa! O erro de verdade é a chamada da linha 10, que aparece só como required from here. Num template pequeno, você acha. Num template da biblioteca, como o std::max da aula anterior, essa mesma situação vira 61 linhas.
E tem outro problema: quem lê template <typename T> bool eh_par(T numero) não sabe que T precisa ser inteiro. Essa regra só existe "escondida" no corpo.
2. A solução: template <std::integral T>
Troque typename pelo nome de um concept. std::integral ("integral", de número inteiro) vem do cabeçalho <concepts> e quer dizer "tipo inteiro": int, long, char, bool…
#include <concepts>
#include <iostream>
template <std::integral T>
bool eh_par(T numero) {
return numero % 2 == 0;
}
int main() {
std::cout << std::boolalpha;
std::cout << eh_par(10) << '\n';
std::cout << eh_par(7) << '\n';
std::cout << eh_par(42L) << '\n';
std::cout << eh_par('A') << '\n';
}
- #include <concepts>
- O cabeçalho dos concepts prontos da biblioteca.
- template <std::integral T>
- "
Té um tipo, e tem que satisfazerstd::integral." Antes de gerar a função, o compilador confere a regra. - std::boolalpha
- Faz o
coutescrevertrue/falseem vez de1/0(aula Booleanos). - eh_par('A')
- Um
chartambém é inteiro para o C++:'A'vale 65 (aula Caracteres).
$ g++ -std=c++23 -Wall -Wextra par-ok.cpp -o par-ok $ ./par-ok true false true false « 'A' é 65, que é ímpar
Com inteiros, nada muda. A diferença aparece quando você erra o tipo. A seção 4 compara as mensagens.
Missão (provoque o erro): o template media foi feito para números com vírgula, mas hoje aceita inteiros, e media(3, 4) dá 3 (a divisão de inteiros joga fora o .5). Rode para ver. Depois troque typename por std::floating_point e rode de novo: o compilador deve recusar media(3, 4) com constraints not satisfied ("restrições não satisfeitas").
#include <concepts>
#include <iostream>
template <typename T>
T media(T a, T b) {
return (a + b) / 2;
}
int main() {
std::cout << media(3.0, 4.0) << '\n';
std::cout << media(3, 4) << '\n';
}
#include <concepts>
#include <iostream>
template <std::floating_point T>
T media(T a, T b) {
return (a + b) / 2;
}
int main() {
std::cout << media(3.0, 4.0) << '\n';
std::cout << media(3, 4) << '\n';
}
Depois de ver o erro, conserte a chamada para media(3.0, 4.0), ou media(3.0, 4.5), e veja que tudo volta a compilar. O concept transformou um resultado errado em silêncio (3) num erro de compilação claro.
3. Quatro formas de escrever a mesma regra
O C++20 permite escrever a restrição de quatro jeitos. Todas fazem exatamente a mesma coisa:
#include <concepts>
#include <iostream>
template <std::integral T>
T dobro1(T x) { return x * 2; }
template <typename T>
requires std::integral<T>
T dobro2(T x) { return x * 2; }
template <typename T>
T dobro3(T x) requires std::integral<T> { return x * 2; }
auto dobro4(std::integral auto x) { return x * 2; }
int main() {
std::cout << dobro1(5) << ' ' << dobro2(5) << ' '
<< dobro3(5) << ' ' << dobro4(5) << '\n';
}
$ g++ -std=c++23 -Wall -Wextra formas.cpp -o formas $ ./formas 10 10 10 10
| Forma | Escrita | Quando usar |
|---|---|---|
1. Concept no lugar de typename | template <std::integral T> | A mais comum e a mais fácil de ler. Use sempre que a regra for um concept só. |
2. Cláusula requires depois do template | template <typename T> requires std::integral<T> | Quando a regra combina condições: requires std::integral<T> && (sizeof(T) >= 4). |
3. requires no fim da assinatura | T f(T x) requires std::integral<T> | Rara. Aparece em métodos de templates de classe. |
4. Concept antes do auto | auto f(std::integral auto x) | Funções curtas, no estilo do template abreviado (auto nos parâmetros). |
requires quer dizer "exige". Na forma 2, leia: "template de T, que exige que T seja inteiro". Repare que, ali, o concept é usado como uma pergunta de sim ou não: std::integral<T> é true ou false. Dá até para mostrar a resposta:
#include <concepts>
#include <iostream>
#include <string>
int main() {
std::cout << std::boolalpha;
std::cout << "int é integral? " << std::integral<int> << '\n';
std::cout << "char é integral? " << std::integral<char> << '\n';
std::cout << "bool é integral? " << std::integral<bool> << '\n';
std::cout << "double é integral? " << std::integral<double> << '\n';
std::cout << "double é floating_point? " << std::floating_point<double> << '\n';
std::cout << "string é totally_ordered? " << std::totally_ordered<std::string> << '\n';
}
$ g++ -std=c++23 -Wall -Wextra perguntar.cpp -o perguntar $ ./perguntar int é integral? true char é integral? true bool é integral? true double é integral? false double é floating_point? true string é totally_ordered? true
A resposta é calculada durante a compilação; o programa só mostra o resultado pronto.
4. A mensagem de erro: sem concept × com concept
Agora o mesmo erro da seção 1 (eh_par(7.5)), com o template protegido por std::integral:
$ g++ -std=c++23 -Wall -Wextra par-com.cpp -o par-com
par-com.cpp: In function ‘int main()’:
par-com.cpp:11:24: error: no matching function for call to ‘eh_par(double)’
11 | std::cout << eh_par(7.5) << '\n';
| ~~~~~~^~~~~
par-com.cpp:11:24: note: there is 1 candidate
par-com.cpp:5:6: note: candidate 1: ‘template<class T> requires integral<T> bool eh_par(T)’
5 | bool eh_par(T numero) {
| ^~~~~~
par-com.cpp:5:6: note: template argument deduction/substitution failed:
par-com.cpp:5:6: note: constraints not satisfied
In file included from par-com.cpp:1:
/usr/include/c++/15/concepts: In substitution of ‘template<class T> requires integral<T> bool eh_par(T) [with T = double]’:
par-com.cpp:11:24: required from here
11 | std::cout << eh_par(7.5) << '\n';
| ~~~~~~^~~~~
/usr/include/c++/15/concepts:109:13: required for the satisfaction of ‘integral<T>’ [with T = double]
/usr/include/c++/15/concepts:109:24: note: the expression ‘is_integral_v<_Tp> [with _Tp = double]’ evaluated to ‘false’
109 | concept integral = is_integral_v<_Tp>;
| ^~~~~~~~~~~~~~~~~~
Ficou mais comprida, mas é bem melhor. Compare:
| Sem concept | Com std::integral | |
|---|---|---|
Onde está o error: | Linha 5, dentro do template (que está certo) | Linha 11, na sua chamada (que é o erro) |
| O que ele diz | invalid operands … to binary ‘operator%’: um sintoma, lá dentro | no matching function for call to ‘eh_par(double)’: "não aceito double" |
| Por quê | Você precisa ler o corpo para deduzir que % exige inteiro | constraints not satisfied → integral<T> com T = double → evaluated to ‘false’ |
| O template chegou a ser gerado? | Sim, e quebrou no meio | Não: barrado na entrada |
Leia a versão com concept assim:
error: no matching function for call to ‘eh_par(double)’: na linha 11, ninguém aceita umdouble.candidate 1: ‘template<class T> requires integral<T> …’: a candidata tinha uma exigência (requires).constraints not satisfied: "restrições não satisfeitas".- A última nota diz qual:
integral<T>, comT = double, deufalse. Ou seja: double não é inteiro.
O Clang resume a mesma situação numa frase: note: because 'double' does not satisfy 'integral' ("porque double não satisfaz integral"). Experimente na missão da seção 2.
Um caso maior: std::totally_ordered
Lembra do maior(p1, p2) com dois Produto, da aula Templates de Função? Agora com a regra std::totally_ordered ("totalmente ordenado": dá para comparar com <, >, <=, >=, == e !=):
#include <concepts>
#include <iostream>
#include <string>
struct Produto {
std::string nome;
double preco;
};
template <std::totally_ordered T>
T maior(T a, T b) {
return a > b ? a : b;
}
int main() {
Produto p1{"caderno", 12.5};
Produto p2{"caneta", 3.0};
Produto m = maior(p1, p2);
std::cout << m.nome << '\n';
}
$ g++ -std=c++23 -Wall -Wextra maior-ordenado.cpp -o maior-ordenado
maior-ordenado.cpp: In function ‘int main()’:
maior-ordenado.cpp:18:22: error: no matching function for call to ‘maior(Produto&, Produto&)’
18 | Produto m = maior(p1, p2);
| ~~~~~^~~~~~~~
maior-ordenado.cpp:18:22: note: there is 1 candidate
maior-ordenado.cpp:11:3: note: candidate 1: ‘template<class T> requires totally_ordered<T> T maior(T, T)’
11 | T maior(T a, T b) {
| ^~~~~
maior-ordenado.cpp:11:3: note: template argument deduction/substitution failed:
maior-ordenado.cpp:11:3: note: constraints not satisfied
In file included from maior-ordenado.cpp:1:
/usr/include/c++/15/concepts: In substitution of ‘template<class T> requires totally_ordered<T> T maior(T, T) [with T = Produto]’:
maior-ordenado.cpp:18:22: required from here
18 | Produto m = maior(p1, p2);
| ~~~~~^~~~~~~~
/usr/include/c++/15/concepts:304:15: required for the satisfaction of ‘__weakly_eq_cmp_with<_Tp, _Tp>’ [with _Tp = Produto]
/usr/include/c++/15/concepts:314:13: required for the satisfaction of ‘equality_comparable<_Tp>’ [with _Tp = Produto]
/usr/include/c++/15/concepts:343:13: required for the satisfaction of ‘totally_ordered<T>’ [with T = Produto]
/usr/include/c++/15/concepts:305:4: in requirements with ‘std::remove_reference_t<_Tp>& __t’, ‘std::remove_reference_t<_Rhs>& __u’ [with _Tp = Produto; _Up = Produto]
/usr/include/c++/15/concepts:306:17: note: the required expression ‘(__t == __u)’ is invalid
306 | { __t == __u } -> __boolean_testable;
| ~~~~^~~~~~
/usr/include/c++/15/concepts:307:17: note: the required expression ‘(__t != __u)’ is invalid
307 | { __t != __u } -> __boolean_testable;
| ~~~~^~~~~~
/usr/include/c++/15/concepts:308:17: note: the required expression ‘(__u == __t)’ is invalid
308 | { __u == __t } -> __boolean_testable;
| ~~~~^~~~~~
/usr/include/c++/15/concepts:309:17: note: the required expression ‘(__u != __t)’ is invalid
309 | { __u != __t } -> __boolean_testable;
| ~~~~^~~~~~
cc1plus: note: set ‘-fconcepts-diagnostics-depth=’ to at least 2 for more detail
Mesma receita: o error: está na linha 18 (a sua chamada); a cadeia de required for the satisfaction of mostra que totally_ordered depende de equality_comparable ("comparável por igualdade"); e as últimas notas dizem o que falta: the required expression ‘(__t == __u)’ is invalid, "a expressão exigida a == b é inválida". Ou seja: Produto nem sabe dizer se dois produtos são iguais, que dirá qual é maior. A última linha sugere uma opção (-fconcepts-diagnostics-depth=2) para o g++ contar ainda mais detalhes; normalmente não precisa.
No g++, uma mensagem com concept nem sempre é curta: aqui deu 32 linhas. O ganho é que ela começa pela sua linha e termina dizendo qual regra falhou, em vez de mostrar um sintoma lá no fundo da biblioteca. A técnica de leitura é a mesma da aula anterior: primeiro error:, depois required from here, depois as últimas note:.
5. Os concepts prontos da biblioteca
O cabeçalho <concepts> traz dezenas. Estes são os que você vai usar primeiro:
| Concept | Quer dizer | Satisfazem | Não satisfazem |
|---|---|---|---|
std::integral<T> | Tipo inteiro | int, long, short, char, bool, unsigned | double, std::string |
std::signed_integral<T> | Inteiro com sinal | int, long | unsigned, bool |
std::unsigned_integral<T> | Inteiro sem sinal (aula Inteiros) | unsigned, std::size_t | int |
std::floating_point<T> | Número com vírgula | float, double, long double | int |
std::totally_ordered<T> | Comparável com < > <= >= == != | int, double, std::string | uma struct sem operadores |
std::equality_comparable<T> | Comparável com == e != | int, std::string, std::vector<int> | uma struct sem == |
std::same_as<T, U> | T e U são o mesmo tipo | same_as<int, int> | same_as<int, long> |
std::convertible_to<T, U> | T pode virar U | convertible_to<int, double> | convertible_to<std::string, int> |
Concepts também permitem ter duas versões de uma função, uma para cada "família" de tipos. O compilador escolhe pela regra que o tipo satisfaz:
#include <concepts>
#include <iostream>
void descrever(std::integral auto x) {
std::cout << x << " é um número inteiro\n";
}
void descrever(std::floating_point auto x) {
std::cout << x << " é um número com vírgula\n";
}
int main() {
descrever(10);
descrever(2.5);
descrever(7L);
descrever(1.5f);
}
$ g++ -std=c++23 -Wall -Wextra descrever.cpp -o descrever $ ./descrever 10 é um número inteiro 2.5 é um número com vírgula 7 é um número inteiro « 7L é long: também é integral 1.5 é um número com vírgula « 1.5f é float: também é floating_point
Com sobrecarga comum, você precisaria de uma função para int, outra para long, outra para double, outra para float… Aqui são duas, uma por família.
6. Escrever o seu próprio concept
Um concept é só um nome para uma condição sobre tipos. Você o cria com a palavra concept. Há dois jeitos de escrever a condição:
- Combinando concepts que já existem, com
&&(e) e||(ou), como num if. - Com uma expressão
requires, que lista código que tem que compilar com o tipo: "umTtem que aceitarcoisa.size()".
#include <concepts>
#include <iostream>
#include <string>
#include <vector>
template <typename T>
concept Numero = std::integral<T> || std::floating_point<T>;
template <typename T>
concept TemTamanho = requires(T coisa) {
coisa.size();
};
template <Numero T>
T dobro(T x) {
return x * 2;
}
void mostrar_tamanho(const TemTamanho auto& coisa) {
std::cout << "tamanho: " << coisa.size() << '\n';
}
int main() {
std::cout << std::boolalpha;
std::cout << Numero<int> << ' ' << Numero<double> << ' '
<< Numero<std::string> << '\n';
std::cout << dobro(21) << ' ' << dobro(1.25) << '\n';
mostrar_tamanho(std::string("arroz"));
mostrar_tamanho(std::vector<int>{7, 9, 10});
}
- template <typename T> concept Numero = …;
- Cria o concept
Numero. Todo concept começa comtemplate <typename T>e termina com;. - std::integral<T> || std::floating_point<T>
- A condição: "
Té inteiro ou é com vírgula". - requires(T coisa) { coisa.size(); }
- Uma expressão requires: "imagine uma variável
coisado tipoT. O código entre chaves tem que compilar". O código não roda; o compilador só confere se ele faz sentido. - template <Numero T>
- Usa o seu concept exatamente como os da biblioteca.
- const TemTamanho auto& coisa
- A forma 4 (concept antes do
auto), recebendo por referência constante.
$ g++ -std=c++23 -Wall -Wextra meu-concept.cpp -o meu-concept $ ./meu-concept true true false 42 2.5 tamanho: 5 tamanho: 3
E se alguém passar um int (que não tem .size()) para mostrar_tamanho?
$ g++ -std=c++23 -Wall -Wextra tamanho-erro.cpp -o tamanho-erro « mostrar_tamanho(42);
tamanho-erro.cpp: In function ‘int main()’:
tamanho-erro.cpp:13:20: error: no matching function for call to ‘mostrar_tamanho(int)’
13 | mostrar_tamanho(42);
| ~~~~~~~~~~~~~~~^~~~
tamanho-erro.cpp:13:20: note: there is 1 candidate
tamanho-erro.cpp:8:6: note: candidate 1: ‘template<class auto:11> requires TemTamanho<auto:11> void mostrar_tamanho(const auto:11&)’
8 | void mostrar_tamanho(const TemTamanho auto& coisa) {
| ^~~~~~~~~~~~~~~
tamanho-erro.cpp:8:6: note: template argument deduction/substitution failed:
tamanho-erro.cpp:8:6: note: constraints not satisfied
tamanho-erro.cpp: In substitution of ‘template<class auto:11> requires TemTamanho<auto:11> void mostrar_tamanho(const auto:11&) [with auto:11 = int]’:
tamanho-erro.cpp:13:20: required from here
13 | mostrar_tamanho(42);
| ~~~~~~~~~~~~~~~^~~~
tamanho-erro.cpp:4:9: required for the satisfaction of ‘TemTamanho<auto:11>’ [with auto:11 = int]
tamanho-erro.cpp:4:22: in requirements with ‘T coisa’ [with T = int]
tamanho-erro.cpp:5:15: note: the required expression ‘coisa.size()’ is invalid
5 | coisa.size();
| ~~~~~~~~~~^~
cc1plus: note: set ‘-fconcepts-diagnostics-depth=’ to at least 2 for more detail
A última nota aponta para a sua regra: the required expression ‘coisa.size()’ is invalid, "a expressão exigida coisa.size() é inválida" para T = int. (O auto:11 é o nome que o g++ inventou para o tipo do auto.) Um concept com um bom nome vira documentação: TemTamanho diz o que a função espera antes de você ler uma linha do corpo.
Os concepts da biblioteca usam minúsculas com sublinhado (floating_point, totally_ordered). Nos seus, use o estilo do seu projeto; neste guia, usamos maiúscula no começo (Numero, TemTamanho), para não confundir com variáveis.
7. Antes do C++20
Concepts só existem a partir do C++20. Com -std=c++17, o g++ nem reconhece o nome, e avisa de onde ele é:
$ g++ -std=c++17 -Wall -Wextra par-com.cpp -o par-com
par-com.cpp:4:11: error: ‘std::integral’ has not been declared
4 | template <std::integral T>
| ^~~
par-com.cpp:4:11: note: ‘std::integral’ is only available from C++20 onwards
par-com.cpp:5:13: error: ‘T’ was not declared in this scope
5 | bool eh_par(T numero) {
| ^
par-com.cpp:5:22: error: expected ‘;’ before ‘{’ token
5 | bool eh_par(T numero) {
| ^~
| ;
par-com.cpp: In function ‘int main()’:
par-com.cpp:10:24: error: missing template arguments before ‘(’ token
10 | std::cout << eh_par(10) << '\n';
| ^
par-com.cpp:11:24: error: missing template arguments before ‘(’ token
11 | std::cout << eh_par(7.5) << '\n';
| ^
O primeiro erro já diz tudo: ‘std::integral’ is only available from C++20 onwards, "só está disponível do C++20 em diante". Os outros quatro são consequência. Em código antigo, você vai ver truques para fazer o mesmo papel (std::enable_if, static_assert); são bem mais difíceis de ler, e os concepts vieram justamente para substituí-los.
Quando dá errado
error: no matching function for call to … + note: constraints not satisfied
O que significa: o tipo que você passou não satisfaz o concept do template. As últimas note: dizem qual regra falhou (evaluated to ‘false’ ou the required expression … is invalid).
Como consertar: passe um tipo que satisfaça a regra (por exemplo, 3.0 em vez de 3 para std::floating_point). Se a regra estiver exigente demais, revise o concept.
error: ‘std::integral’ has not been declared
O que significa: ou você compilou com um padrão anterior ao C++20 (a nota diz only available from C++20 onwards), ou esqueceu o #include <concepts>.
Como consertar: compile com -std=c++23 e inclua <concepts>.
Referência rápida
| Escrita | O que faz |
|---|---|
#include <concepts> | Concepts prontos da biblioteca |
template <std::integral T> | Template que só aceita tipos inteiros |
template <typename T> requires std::integral<T> | O mesmo, com cláusula requires |
auto f(std::integral auto x) | O mesmo, na forma abreviada |
requires A<T> && B<T> | Exige as duas regras |
template <typename T> concept Nome = condição; | Cria um concept |
requires(T x) { x.size(); } | Condição: "este código tem que compilar com T" |
std::integral<int> | Usado como valor: true ou false, calculado na compilação |
Exercícios
1. Restringir
Complete para o template aceitar só números com vírgula:
T metade(T x) { return x / 2; }
2. A palavra que exige
Complete a cláusula:
std::integral<T>
T dobro(T x) { return x * 2; }
3. Qual é a principal vantagem da mensagem de erro com concept?
4. Qual destes tipos não satisfaz std::integral?
5. O que faz requires(T x) { x.size(); } dentro de um concept?
Missão final: o seu concept
Missão: crie o concept Numero ("inteiro ou com vírgula") e use-o no template triplo, no lugar do typename. O programa deve mostrar 12 e 4.5. Depois, como teste, acrescente triplo(std::string("a")) e leia a mensagem.
#include <concepts>
#include <iostream>
#include <string>
// crie o concept Numero aqui
template <typename T>
T triplo(T x) {
return x * 3;
}
int main() {
std::cout << triplo(4) << '\n';
std::cout << triplo(1.5) << '\n';
}
#include <concepts>
#include <iostream>
#include <string>
template <typename T>
concept Numero = std::integral<T> || std::floating_point<T>;
template <Numero T>
T triplo(T x) {
return x * 3;
}
int main() {
std::cout << triplo(4) << '\n';
std::cout << triplo(1.5) << '\n';
}
Resumo
- Um concept é uma regra sobre tipos, conferida na entrada do template C++20.
- Forma mais comum:
template <std::integral T>. Outras:requires std::integral<T>estd::integral auto x. - Concepts prontos (
<concepts>):std::integral,std::floating_point,std::totally_ordered,std::equality_comparable,std::same_as… - Com concept, o erro aponta a sua chamada (
constraints not satisfied) e diz qual regra falhou, em vez de um sintoma no meio do template. - O seu concept:
template <typename T> concept Nome = condição;, combinando concepts com&&/||ou exigindo código comrequires(T x) { … }. - Próximo passo: a biblioteca padrão, feita de templates, em O que é a STL.