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.

Você vai aprender
  • 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_ordered e outros.
  • A mensagem de erro sem concept × com concept, lado a lado.
  • Como escrever o seu próprio concept, com concept e requires.

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 satisfazer std::integral." Antes de gerar a função, o compilador confere a regra.
std::boolalpha
Faz o cout escrever true/false em vez de 1/0 (aula Booleanos).
eh_par('A')
Um char també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
FormaEscritaQuando usar
1. Concept no lugar de typenametemplate <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 templatetemplate <typename T> requires std::integral<T>Quando a regra combina condições: requires std::integral<T> && (sizeof(T) >= 4).
3. requires no fim da assinaturaT f(T x) requires std::integral<T>Rara. Aparece em métodos de templates de classe.
4. Concept antes do autoauto 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 conceptCom 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 dizinvalid operands … to binary ‘operator%’: um sintoma, lá dentrono matching function for call to ‘eh_par(double)’: "não aceito double"
Por quêVocê precisa ler o corpo para deduzir que % exige inteiroconstraints not satisfied → integral<T> com T = double → evaluated to ‘false’
O template chegou a ser gerado?Sim, e quebrou no meioNão: barrado na entrada

Leia a versão com concept assim:

  1. error: no matching function for call to ‘eh_par(double)’: na linha 11, ninguém aceita um double.
  2. candidate 1: ‘template<class T> requires integral<T> …’: a candidata tinha uma exigência (requires).
  3. constraints not satisfied: "restrições não satisfeitas".
  4. A última nota diz qual: integral<T>, com T = double, deu false. Ou seja: double não é inteiro.
No editor do guia (Clang), fica ainda mais direto

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.

Sendo honesto sobre o tamanho

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:

ConceptQuer dizerSatisfazemNão satisfazem
std::integral<T>Tipo inteiroint, long, short, char, bool, unsigneddouble, std::string
std::signed_integral<T>Inteiro com sinalint, longunsigned, bool
std::unsigned_integral<T>Inteiro sem sinal (aula Inteiros)unsigned, std::size_tint
std::floating_point<T>Número com vírgulafloat, double, long doubleint
std::totally_ordered<T>Comparável com < > <= >= == !=int, double, std::stringuma 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 tiposame_as<int, int>same_as<int, long>
std::convertible_to<T, U>T pode virar Uconvertible_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: "um T tem que aceitar coisa.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 com template <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 coisa do tipo T. 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.

Convenção de nomes

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

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

template < T>
T metade(T x) { return x / 2; }

2. A palavra que exige

Complete a cláusula:

template <typename T>
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> e std::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 com requires(T x) { … }.
  • Próximo passo: a biblioteca padrão, feita de templates, em O que é a STL.