Doadores E Receptores Universais - As Pessoas De Qual Tipo Sanguíneo São Consideradas Doadores Universais ...
As Pessoas De Qual Tipo Sanguíneo São Consideradas Doadores Universais ...

O que são doadores e receptores universais

São funções ou métodos escritos de forma que aceitam qualquer tipo de dado sem precisar de especialização para cada caso. No C++, você faz isso com templates. Na prática, isso quer dizer que um vetor genérico, por exemplo, funciona com int, float, std::string ou uma struct que você criou hoje de manhã, sem reescrever nada. Um doador universal é a função que recebe qualquer coisa como parâmetro. Um receptor universal é a função que devolve qualquer coisa. O template resolve os dois lados da moeda.

Como implementar doadores e receptores universais em C++

Vou mostrar o padrão mais usado, que é o template de função e o template de classe. Para um doador universal simples:

template<typename T>
void receber_qualquer_coisa(T valor) {
  // faz algo com valor
} Chamar passando int, double ou seu próprio objeto funciona da mesma forma. O compilador gera a versão certa automaticamente.

Para o receptor universal, o truque é usar template em conjunto com return type deduction, que existe desde o C++14: template<typename T>
T criar_valor() {
  return T{};
}

Assim você pede um double, um int, uma std::vector — tudo funciona.

👉 Clique no botão abaixo para saber mais sobre o assunto!

Quando isso dá problema

A parte que ninguém conta é que template genérico não significa ilimitado. Se sua função usa operadores que não existem em todos os tipos, o compilador trava na compilação. Já perdi duas horas tentando debugar um erro de "no match for operator<

" quando na verdade o problema era que o tipo passado não tinha stream de saída definido. O erro aparecia em uma linha inteira no topo do stack, mas a causa estava em uma chamada que parecia inofensiva. O workaround que uso hoje é adicionar constraints com concepts (C++20). Fica assim:

template<std::integral T>
void fazer_algo(T valor) {
  // só aceita inteiros
} Isso elimina aambiguidade e o erro de compilação mostra exatamente qual constraint falhou. Antes eu usava SFINAE com std::enable_if, que funcionava mas gerava mensagens de erro quase ilegíveis.

Limitações reais

Doadores e receptores universais não resolvem tudo. Eles geram código duplicado por tipo — se você passa int e double, o compilador gera duas versões da função. Isso aumenta o tamanho do binário. Em projetos com templates pesados, o tempo de compilação pode triplicar comparado a uma função especializada. Também existe o problema de polimorfismo. Templates resolvem polimorfismo em tempo de compilação. Se você precisa decidir em tempo de execução qual tipo tratar, herança com classes base e ponteiros para base ainda é o caminho mais prático.

Para bibliotecas externas sem acesso ao código-fonte, o template não funciona tão bem. É mais fácil usar void* combinado com casting controlado, ainda que isso abra mão da segurança de tipos.

Dica prática sobre especialização

A coisa mais importante que aprendi é que sempre deve haver uma especialização total ou parcial para tipos problemáticos. Um vetor genérico que funciona para tipos trivialmente copiáveis pode travar com std::unique_ptr porque esse tipo não é copiável, apenas movível. A solução foi especializar o construtor de cópia para esses casos e usar std::move internamente. Demorou um dia inteiro para identificar, porque o erro só aparecia em certas arquiteturas devido a alinhamento de memória diferente. Se você está começando com doadores e receptores universais, comece com functions template simples antes de partir para classes template. A curva de aprendizado é menor e os erros são mais fáceis de rastrear. A maioria dos bugs que vejo em código de produção vem de templates aninhados demais, onde ninguém sabe mais qual instância está sendo usada em cada ponto da chamada.