Nos artigos Escreva uma Vez, Use com Todo Tipo — Templates de Função e Estruturas de Dados para Qualquer Tipo — Templates de Classe dominamos os templates e vimos que a STL inteira é construída sobre eles. Mas ficou uma dívida que anotei duas vezes: os templates têm um ponto fraco tradicional. Quando você passa um tipo que não atende aos requisitos implícitos do código — lembra do operator> que faltava na struct Ponto? —, o erro só aparece lá dentro, na instanciação, com mensagens quilométricas que mencionam linhas do interior da função em vez do seu erro real. Por décadas isso foi o preço da genericidade em C++. O C++20 trouxe a cura: os concepts, que permitem declarar explicitamente quais requisitos um tipo deve cumprir, transformando erros ilegíveis em mensagens claras e tornando a intenção dos templates legível para humanos. Hoje fechamos a Fase 5 com essa ferramenta que amadurece toda a programação genérica. Digo desde já: isto é C++20, e vale confirmar que seu compilador o suporta.
O problema, revisitado com nitidez
Relembre a dor concreta. Um template diz "funciona com qualquer T", mas na verdade exige que T suporte certas operações. Quando o requisito é violado, veja o que acontece:
template <typename T>
T maior(T a, T b) { return a > b ? a : b; } // exige que T tenha operator>
struct Ponto { int x, y; };
// maior(Ponto{1,2}, Ponto{3,4});
// ERRO: uma parede de texto mencionando "no match for operator>"
// apontando para DENTRO de maior, não para a sua chamada.
O compilador aceita o template (é um molde válido) e só descobre o problema ao tentar instanciá-lo para Ponto, gerando um erro que fala da linha interna a > b. Em templates reais e aninhados da STL, essas mensagens podem ter centenas de linhas. O programador fica caçando, no meio do ruído, qual requisito faltou. Os concepts atacam exatamente isso.
A solução: nomear e exigir requisitos
Um concept é um requisito nomeado sobre um tipo — uma condição que o compilador verifica antes de instanciar. A biblioteca padrão já traz muitos prontos, no cabeçalho <concepts>. Você pode restringir um template a aceitar só tipos que satisfaçam um concept:
#include <iostream>
#include <concepts> // std::integral, std::floating_point, etc. (C++20)
// 'std::integral T' RESTRINGE o template: T deve ser um tipo inteiro.
// Se alguém chamar com um tipo não-inteiro, o erro é CLARO e IMEDIATO.
template <std::integral T>
T dobro(T x) {
return x * 2;
}
int main() {
std::cout << dobro(21) << '\n'; // 42 — int satisfaz std::integral
std::cout << dobro(100L) << '\n'; // 200 — long também
// dobro(3.14); // ERRO CLARO: "constraint 'std::integral<double>' not satisfied"
// A mensagem diz exatamente qual requisito falhou.
return 0;
}
A diferença é enorme. Ao escrever template <std::integral T>, você declara "este template só aceita tipos inteiros". Se alguém tentar dobro(3.14), o erro não é uma parede sobre x * 2 — é uma linha direta: "a restrição std::integral<double> não foi satisfeita". O requisito, antes implícito e descoberto tarde, agora é explícito e verificado cedo. O código também comunica melhor: quem lê a assinatura sabe de imediato o que o template exige.
Escrevendo seu próprio concept
Você não depende só dos concepts prontos — pode definir os seus com a palavra-chave concept e a construção requires, que lista as operações que o tipo deve suportar:
#include <iostream>
#include <concepts>
// Define um concept 'Somavel': tipos T para os quais 'a + b' é válido
// e resulta em algo conversível para T.
template <typename T>
concept Somavel = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // exige que a + b exista e devolva ~T
};
// Usa o concept como restrição.
template <Somavel T>
T soma_tres(T a, T b, T c) {
return a + b + c;
}
int main() {
std::cout << soma_tres(1, 2, 3) << '\n'; // 6
std::cout << soma_tres(1.5, 2.5, 3.0) << '\n'; // 7
std::cout << soma_tres(std::string("a"),
std::string("b"),
std::string("c")) << '\n'; // abc — string tem +
return 0;
}
O bloco requires(T a, T b) { { a + b } -> std::convertible_to<T>; } lê-se como: "para dois valores a e b do tipo T, a expressão a + b deve ser válida e produzir algo conversível para T". Se você chamar soma_tres com um tipo sem operator+, o erro dirá que o concept Somavel não foi satisfeito — claro e no ponto certo. Você acabou de tornar explícito o requisito que, no artigo Escreva uma Vez, Use com Todo Tipo — Templates de Função, ficava perigosamente implícito.
A honestidade: concepts são poder, não obrigação
Prometo sempre a medida certa. Concepts são uma melhoria notável, mas nem todo template precisa deles. Para código genérico simples e de uso interno, um template sem restrição pode ser perfeitamente adequado — adicionar concepts a tudo seria cerimônia excessiva. O valor deles cresce com a exposição e a complexidade do código: bibliotecas usadas por muita gente, templates com requisitos não-óbvios, ou casos onde mensagens de erro ruins custariam caro a quem usa. A diretriz madura: use concepts quando eles esclarecem a intenção e melhoram os erros para os usuários do seu código; não os imponha onde a simplicidade já basta. E lembre-se do custo de adoção: concepts exigem C++20, então em bases de código presas a padrões anteriores eles simplesmente não estão disponíveis — mais uma razão para eu sempre marcar a versão.
Concept transforma um requisito implícito em parte da assinatura: em vez de o erro estourar dentro do template, a chamada é rejeitada na hora, com o nome da restrição que falhou. O ganho é duplo — mensagens legíveis para quem usa, e documentação que não envelhece para quem lê. Como toda ferramenta de rigor, tem custo de cerimônia, e não precisa estar em tudo: um template pequeno, de uso interno e óbvio, vive bem sem ele.
Aqui se encerra a genericidade, e o salto é considerável — de enxergar <> como sintaxe misteriosa da STL para escrever código genérico próprio. Lambdas que capturam o ambiente, funções que valem para muitos tipos, estruturas de dados que guardam qualquer coisa e, por fim, requisitos declarados em vez de descobertos no erro: é o conjunto que sustenta praticamente toda biblioteca C++ moderna.
Fontes e leituras recomendadas
- cppreference.com/w/cpp/language/constraints: a referência completa sobre concepts,
requirese restrições de template (C++20). - cppreference.com/w/cpp/concepts: o catálogo dos concepts prontos da biblioteca padrão (
integral,convertible_to,sortable, etc.). - Bjarne Stroustrup, A Tour of C++ (3ª ed.), capítulo sobre concepts e programação genérica: a apresentação do recurso pelo criador, que o defendeu por anos.
- ISO C++ Core Guidelines, seção "T.concepts", regras T.10 e T.11 ("Specify concepts for all template arguments"): as diretrizes sobre quando e como usar concepts.
- Bjarne Stroustrup, "Concepts: The Future of Generic Programming" (artigo): a motivação histórica e os problemas que os concepts resolvem.
Exercícios
Exercício 1
Escreva um template T triplo(T x) restrito ao concept std::integral, que devolva o triplo de um inteiro. Teste com int e comente o que acontece (mensagem de erro) ao tentar chamá-lo com um double.
Ver resposta
✓ Resposta: Template restrito:
#include <iostream>
#include <concepts>
template <std::integral T>
T triplo(T x) { return x * 3; }
int main() {
std::cout << triplo(7) << '\n'; // 21
// triplo(2.5); // ERRO
return 0;
}
Ao chamar triplo(2.5), o compilador rejeita imediatamente com uma mensagem clara indicando que a restrição std::integral<double> não foi satisfeita — não uma parede de texto sobre x * 3, mas uma linha dizendo exatamente qual requisito o double não cumpre.
Exercício 2
Explique, com base na aula, por que a mensagem de erro de um template restrito por concept é mais útil que a de um template não-restrito quando se passa um tipo inadequado.
Ver resposta
✓ Resposta: Porque o concept é verificado antes da instanciação e tem um nome. Num template não-restrito, o compilador aceita a chamada, tenta gerar o código para o tipo, e só falha lá dentro, ao encontrar uma operação inválida — a mensagem então aponta para uma linha interna do template (x * 3, a > b) que o programador nem escreveu, cercada do contexto de instanciação. Num template restrito por concept, o compilador checa a restrição na fronteira da chamada e, se ela falha, reporta "a restrição std::integral<double> não foi satisfeita" — uma frase que nomeia o requisito violado e o tipo culpado, apontando para o lugar certo (a chamada). O erro deixa de ser "algo deu errado no interior" e passa a ser "seu tipo não cumpre este requisito nomeado".
Exercício 3
Defina um concept Imprimivel que exija que um tipo T possa ser enviado a std::cout com <<. (Dica: requires(std::ostream& os, T x) { { os << x }; }.) Use-o para restringir uma função void mostra(T x).
Ver resposta
✓ Resposta: Concept Imprimivel:
#include <iostream>
#include <concepts>
template <typename T>
concept Imprimivel = requires(std::ostream& os, T x) {
{ os << x }; // exige que 'os << x' seja válido
};
template <Imprimivel T>
void mostra(T x) {
std::cout << "valor: " << x << '\n';
}
int main() {
mostra(42); // ok
mostra(std::string("oi")); // ok
// struct Vazio {}; mostra(Vazio{}); // ERRO: Vazio não satisfaz Imprimivel
return 0;
}
O concept exige que os << x compile; tipos sem operator<< para stream são rejeitados com mensagem clara.
Exercício 4
Restrinja o template maior<T> do artigo Escreva uma Vez, Use com Todo Tipo — Templates de Função com um concept apropriado que exija que T seja comparável com > (você pode usar std::totally_ordered da biblioteca). Explique como isso muda o comportamento ao passar uma struct sem operator>.
Ver resposta
✓ Resposta: Com std::totally_ordered:
#include <concepts>
template <std::totally_ordered T>
T maior(T a, T b) { return a > b ? a : b; }
Antes, passar uma struct sem operator> causava um erro tardio e verboso apontando para dentro de maior, na linha a > b. Agora, com a restrição std::totally_ordered (que exige os operadores de comparação completos), o compilador rejeita a chamada na fronteira, com uma mensagem dizendo que o tipo não satisfaz std::totally_ordered — clara, imediata e apontando para a chamada. O requisito que era implícito no corpo virou explícito na assinatura, e o erro melhorou proporcionalmente.
Exercício 5
Discuta o trade-off de adicionar concepts a um template. Dê um exemplo de situação em que vale muito a pena e outra em que seria cerimônia desnecessária, justificando com a diretriz de "poder, não obrigação" da aula.
Ver resposta
✓ Resposta: O trade-off: adicionar concepts torna a intenção do template explícita e melhora drasticamente as mensagens de erro para quem o usa, ao custo de mais código na assinatura, dependência de C++20, e algum esforço de projetar a restrição certa. Vale muito a pena numa biblioteca pública — por exemplo, um container ou algoritmo genérico que muitos programadores vão usar: ali, mensagens de erro claras economizam horas de frustração alheia, e a restrição documenta o contrato. Seria cerimônia desnecessária num template interno simples e de uso único — digamos, uma pequena função auxiliar genérica usada só dentro de um arquivo, com tipos que você mesmo controla: adicionar um concept elaborado ali só polui o código sem beneficiar ninguém, já que não há usuários externos para proteger e o uso é óbvio. A diretriz "poder, não obrigação" se aplica: use concepts onde a clareza e os erros importam para outros, não como ritual obrigatório em todo template.