Genéricos e tipificação em C++: por que isso não é "apenas templates"?
Quando um desenvolvedor migra do Python ou Java para C++, muitas vezes a palavra "template" o assusta. Na mente surgem associações com colchetes angulares infinitos, erros de compilação incompreensíveis e a magia de std::vector<int>. Na verdade, genéricos em C++ (ou programação genérica) são uma ferramenta poderosa que permite escrever código uma vez e usá-lo com qualquer tipo de dado. E sim, isso não é a mesma coisa que genéricos em Java ou C#.
Neste artigo, vamos explorar como funciona a tipificação em C++ através de templates, como os templates de classes diferem dos templates de funções e por que os conceitos foram adicionados no C++20. Haverá muitos exemplos — desde os simples até os que realmente serão úteis em produção. Vamos lá!
O que são genéricos (templates) em C++?
De forma simples: templates em C++ são uma maneira de escrever código que funciona com qualquer tipo de dado, sem perder desempenho. Ao contrário da tipificação dinâmica do Python, onde o tipo é verificado em tempo de execução, o C++ gera uma versão separada da função ou classe para cada tipo usado em tempo de compilação. Isso é chamado de tipificação estática com parametrização.
Sintaticamente, um template é declarado usando a palavra-chave template e colchetes angulares com parâmetros:
template <typename T>T max(T a, T b) { return (a > b) ? a : b;}Aqui, typename T é o parâmetro do template. Você pode usar class em vez de typename — não há diferença. Quando você escreve max(3, 7), o compilador substitui automaticamente int por T e cria a função int max(int, int). Se escrever max(3.14, 2.71) — obtém a versão para double. Simples e eficiente.
Templates de funções: o primeiro passo para genéricos
Vamos começar com o básico — templates de funções em C++. Esta é a maneira ideal de evitar duplicação de código quando a lógica é a mesma, mas os tipos são diferentes.
Exemplo: uma função que imprime um elemento no console (útil para depuração):
#include <iostream>#include <string>
template <typename T>void print(const T& value) { std::cout << value << std::endl;}
int main() { print(42); // int print(3.1415); // double print("Hello"); // const char* print(std::string("Genéricos!")); // std::string return 0;}Observe: usamos const T& — isso é tipificação em C++ com passagem por referência constante, para evitar cópia de objetos grandes. Para tipos primitivos (int, double) isso não é crítico, mas é um bom hábito.
E se precisarmos passar dois argumentos de tipos diferentes? Fácil:
template <typename T1, typename T2>auto add(T1 a, T2 b) -> decltype(a + b) { return a + b;}
// Em C++14 e posteriores, pode-se simplesmente:template <typename T1, typename T2>auto add(T1 a, T2 b) { return a + b;}Aqui, auto deduz o tipo de retorno automaticamente. Isso é genéricos em ação — um código, infinitas combinações de tipos.
Templates de classes: construindo estruturas universais
Agora vamos para os templates de classes em C++. Esta é a base da biblioteca padrão (STL): std::vector<T>, std::map<Key, Value>, std::optional<T> — todos são implementados via templates.
Vamos escrever um exemplo simples, mas ilustrativo — uma classe template "Par" (análoga a std::pair):
template <typename First, typename Second>class Pair {private: First first_; Second second_;public: