C++ Templates de Classe
Na aula Templates de Função, você escreveu uma função-molde que serve para qualquer tipo. A mesma ideia vale para classes: uma classe Caixa que guarda um int, ou um texto, ou um preço, escrita uma vez só. Você já usa isso desde o Nível 1 sem saber: std::vector<int> é um template de classe, e o <int> é você dizendo qual "massa" vai na forma.
- Como escrever um template de classe:
template <typename T> class Caixa. - Como criar objetos dele:
Caixa<int>,Caixa<std::string>. - Como o C++17 descobre o tipo pelo construtor (
Caixa c{5};), a "CTAD", e a armadilha do texto entre aspas. - Por que
std::vectoré um template, e o que o<int>significa. - Como escrever os métodos fora da classe.
- Por que templates ficam no arquivo
.h, e o erro real de ligação (referência não definida) quando você os separa.
Todos os programas desta aula foram compilados com o g++ 15.2 na máquina fictícia da Ana, na pasta ~/cpp, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa. Você precisa conhecer Classes e Objetos, Construtores e Métodos const.
A ideia: a caixa de presente
Uma loja vende caixas de presente do mesmo modelo: tampa, laço, etiqueta. Numa vai um livro, noutra um perfume, noutra um par de meias. O modelo da caixa é o mesmo; muda o que vai dentro.
Um template de classe é o projeto da caixa. Na hora de usar, você diz o que vai dentro: Caixa<int> é "uma caixa que guarda um int"; Caixa<std::string> é "uma caixa que guarda um texto". Para o compilador, essas são duas classes diferentes, geradas a partir do mesmo molde.
1. O primeiro template de classe
#include <iostream>
#include <string>
template <typename T>
class Caixa {
public:
Caixa(T valor) : conteudo{valor} {}
T pegar() const {
return conteudo;
}
void trocar(T novo) {
conteudo = novo;
}
private:
T conteudo;
};
int main() {
Caixa<int> moedas{25};
Caixa<std::string> bilhete{"Feliz aniversário!"};
Caixa<double> peso{1.75};
std::cout << moedas.pegar() << '\n';
std::cout << bilhete.pegar() << '\n';
std::cout << peso.pegar() << '\n';
moedas.trocar(40);
std::cout << moedas.pegar() << '\n';
}
- template <typename T>
- A mesma linha dos templates de função: "o que vem abaixo é um molde, e
Té o tipo em branco". - class Caixa { … };
- Uma classe comum, só que pode usar
Tem qualquer lugar onde usaria um tipo. - Caixa(T valor) : conteudo{valor} {}
- O construtor recebe um
Te guarda emconteudo(a lista depois dos:inicializa o atributo). - T pegar() const
- Um método que devolve o que está na caixa. O
constpromete não mexer no objeto (aula Métodos const). - void trocar(T novo)
- Troca o conteúdo por outro do mesmo tipo
T. - T conteudo;
- O atributo privado: um
intnumaCaixa<int>, umstd::stringnumaCaixa<std::string>. - Caixa<int> moedas{25};
- Cria um objeto. O
<int>diz o que vai no lugar deT. O nome completo do tipo éCaixa<int>;Caixasozinho é só o molde.
$ g++ -std=c++23 -Wall -Wextra caixa.cpp -o caixa $ ./caixa 25 Feliz aniversário! 1.75 40
Com um molde, três classes: Caixa<int>, Caixa<std::string> e Caixa<double>. E cada uma é rígida com o seu tipo. Tente pôr um texto na caixa de moedas:
$ g++ -std=c++23 -Wall -Wextra caixa-errada.cpp -o caixa-errada « moedas.trocar("quarenta");
caixa-errada.cpp: In function ‘int main()’:
caixa-errada.cpp:14:19: error: invalid conversion from ‘const char*’ to ‘int’ [-fpermissive]
14 | moedas.trocar("quarenta");
| ^~~~~~~~~~
| |
| const char*
caixa-errada.cpp:7:19: note: initializing argument 1 of ‘void Caixa<T>::trocar(T) [with T = int]’
7 | void trocar(T novo) { conteudo = novo; }
| ~~^~~~
"Conversão inválida de const char* (um texto entre aspas) para int." A nota explica onde: no argumento 1 de Caixa<T>::trocar(T) com T = int. Depois que você escolheu int, a moedas é uma caixa de inteiros para sempre. É a mesma proteção do std::vector<int>, que não aceita textos.
Experimente: crie uma Caixa<char> com a letra 'A' e mostre o conteúdo. Depois crie uma Caixa<bool> com true e mostre (o cout escreve 1). Por fim, tente moedas.trocar(2.9);: compila? O que aparece?
#include <iostream>
#include <string>
template <typename T>
class Caixa {
public:
Caixa(T valor) : conteudo{valor} {}
T pegar() const { return conteudo; }
void trocar(T novo) { conteudo = novo; }
private:
T conteudo;
};
int main() {
Caixa<int> moedas{25};
std::cout << moedas.pegar() << '\n';
}
2. CTAD: deixar o compilador descobrir o T C++17
Com templates de função, o compilador deduz o tipo pelos argumentos. Até o C++14, isso não valia para classes: era obrigatório escrever Caixa<int>. Desde o C++17, o compilador deduz pelo que você passa ao construtor. O nome técnico é CTAD (Class Template Argument Deduction, "dedução de argumentos de template de classe").
#include <iostream>
#include <string>
#include <vector>
template <typename T>
class Caixa {
public:
Caixa(T valor) : conteudo{valor} {}
T pegar() const { return conteudo; }
private:
T conteudo;
};
int main() {
Caixa a{5};
Caixa b{2.5};
Caixa c{std::string("Ana")};
std::vector notas{7, 9, 10};
std::cout << a.pegar() << ' ' << b.pegar() << ' ' << c.pegar() << '\n';
std::cout << notas.size() << " notas\n";
}
$ g++ -std=c++23 -Wall -Wextra ctad.cpp -o ctad $ ./ctad 5 2.5 Ana 3 notas
| Você escreveu | O compilador entendeu | Por quê |
|---|---|---|
Caixa a{5}; | Caixa<int> a{5}; | 5 é int |
Caixa b{2.5}; | Caixa<double> b{2.5}; | 2.5 é double |
Caixa c{std::string("Ana")}; | Caixa<std::string> c{…}; | o argumento é um std::string |
std::vector notas{7, 9, 10}; | std::vector<int> notas{7, 9, 10}; | os elementos são int |
Com um padrão mais antigo, o mesmo arquivo nem compila. O g++ reclama que faltam os argumentos do template:
$ g++ -std=c++14 -Wall -Wextra ctad.cpp -o ctad
ctad.cpp: In function ‘int main()’:
ctad.cpp:15:11: error: missing template arguments before ‘a’
15 | Caixa a{5};
| ^
ctad.cpp:16:11: error: missing template arguments before ‘b’
16 | Caixa b{2.5};
| ^
ctad.cpp:17:11: error: missing template arguments before ‘c’
17 | Caixa c{std::string("Ana")};
| ^
ctad.cpp:18:17: error: missing template arguments before ‘notas’
18 | std::vector notas{7, 9, 10};
| ^~~~~
ctad.cpp:20:18: error: ‘a’ was not declared in this scope
20 | std::cout << a.pegar() << ' ' << b.pegar() << ' ' << c.pegar() << '\n';
| ^
ctad.cpp:20:38: error: ‘b’ was not declared in this scope
20 | std::cout << a.pegar() << ' ' << b.pegar() << ' ' << c.pegar() << '\n';
| ^
ctad.cpp:20:58: error: ‘c’ was not declared in this scope
20 | std::cout << a.pegar() << ' ' << b.pegar() << ' ' << c.pegar() << '\n';
| ^
ctad.cpp:21:18: error: ‘notas’ was not declared in this scope
21 | std::cout << notas.size() << " notas\n";
| ^~~~~
Oito erros, mas só quatro de verdade: os quatro missing template arguments ("faltam argumentos de template antes de…"). Os outros (‘a’ was not declared) são consequência: como a linha 15 falhou, a nunca existiu. Esse é o motivo de o guia compilar sempre com -std=c++23.
A armadilha do texto entre aspas
Por que o exemplo escreveu std::string("Ana") e não só "Ana"? Porque um texto entre aspas não é um std::string; é um ponteiro para caracteres, herança do C. Veja o que a CTAD deduz:
$ g++ -std=c++23 -Wall -Wextra ctad-texto.cpp -o ctad-texto « Caixa nome{"Ana"}; e depois nome.pegar().size()
ctad-texto.cpp: In function ‘int main()’:
ctad-texto.cpp:15:31: error: request for member ‘size’ in ‘nome.Caixa<const char*>::pegar()’, which is of non-class type ‘const char*’
15 | std::cout << nome.pegar().size() << '\n';
| ^~~~
A mensagem entrega o que aconteceu: Caixa<const char*>. O T virou const char*, que não tem .size() ("pedido do membro size em algo que não é classe"). Quando quiser texto, diga Caixa<std::string> nome{"Ana"}; ou passe um std::string.
Use a CTAD quando o tipo é óbvio pelo valor (std::vector notas{7, 9, 10}). Escreva o tipo quando ele não é óbvio, quando o valor engana (textos entre aspas) ou quando o objeto começa vazio: std::vector<int> notas; não tem valor nenhum para o compilador olhar.
Por falar em "começar vazio": a CTAD precisa de um argumento para olhar. Sem argumento, ela falha:
$ g++ -std=c++23 -Wall -Wextra sem-args.cpp -o sem-args « Caixa vazia;
sem-args.cpp: In function ‘int main()’:
sem-args.cpp:10:11: error: class template argument deduction failed:
10 | Caixa vazia;
| ^~~~~
sem-args.cpp:10:11: error: no matching function for call to ‘Caixa()’
sem-args.cpp:10:11: note: there are 2 candidates
sem-args.cpp:4:5: note: candidate 1: ‘template<class T> Caixa(T) -> Caixa<T>’
4 | Caixa(T valor) : conteudo{valor} {}
| ^~~~~
sem-args.cpp:4:5: note: candidate expects 1 argument, 0 provided
sem-args.cpp:2:7: note: candidate 2: ‘template<class T> Caixa(Caixa<T>) -> Caixa<T>’
2 | class Caixa {
| ^~~~~
sem-args.cpp:2:7: note: candidate expects 1 argument, 0 provided
"A dedução de argumentos de template de classe falhou." As notas mostram as "regras de dedução" que o compilador criou a partir dos construtores (Caixa(T) -> Caixa<T>: "se receber um T, é uma Caixa<T>") e dizem que todas esperam 1 argumento, e você deu 0.
3. std::vector é um template
Agora o std::vector<int> da aula std::vector faz sentido por inteiro: vector é um template de classe da biblioteca padrão, e <int> é o argumento do template. Dá para ver a primeira linha dele no cabeçalho que vem com o g++:
$ sed -n 457,458p /usr/include/c++/15/bits/stl_vector.h
template<typename _Tp, typename _Alloc = std::allocator<_Tp> >
class vector : protected _Vector_base<_Tp, _Alloc>
- template<typename _Tp, …>
- O mesmo
template <typename T>que você escreveu. A biblioteca chama o tipo de_Tp(nomes com_no começo são reservados para ela). - typename _Alloc = std::allocator<_Tp>
- Um segundo parâmetro, com valor padrão (o
=), como os valores padrão de funções. Ele diz como o vetor pede memória. Você quase nunca mexe nele; por isso escreve sóstd::vector<int>. - class vector
- A classe em si. O resto (
protected _Vector_base…) é herança, um detalhe interno.
Quase tudo na biblioteca padrão é template: std::vector<T>, std::map<Chave, Valor> (aula std::map), std::optional<T>, std::unique_ptr<T> (aula Ponteiros Inteligentes). Até std::string é um apelido para std::basic_string<char>. Por isso os nomes nas mensagens de erro ficam tão compridos.
Um template também pode receber um número. O std::array é assim: em std::array<int, 5>, o int é o tipo e o 5 é o tamanho, fixo, conhecido na compilação. Você escreveria algo como template <typename T, int N> class Prateleira. É menos comum no dia a dia; basta reconhecer.
4. Métodos escritos fora da classe
Numa classe comum, você pode declarar o método dentro e escrever o corpo fora, com Classe::metodo (aula Métodos). Num template, a linha template <typename T> precisa ser repetida antes de cada corpo, e o nome da classe vira Caixa<T>:
#pragma once
template <typename T>
class Caixa {
public:
Caixa(T valor);
T pegar() const;
private:
T conteudo;
};
template <typename T>
Caixa<T>::Caixa(T valor) : conteudo{valor} {}
template <typename T>
T Caixa<T>::pegar() const {
return conteudo;
}
- Caixa(T valor);
- Dentro da classe, só a declaração (sem corpo, com
;). - template <typename T>
- Repetido antes de cada corpo escrito fora: cada corpo também é um molde.
- Caixa<T>::pegar()
- "O método
pegardaCaixa<T>". Esquecer o<T>aqui é um erro comum. - #pragma once
- Impede que o cabeçalho seja incluído duas vezes (aula Arquivos .h e .cpp).
Repare que, aqui, os corpos estão no mesmo arquivo .h, logo abaixo da classe. Isso não é descuido. É o assunto da próxima seção.
5. Por que templates ficam no .h
Na aula Arquivos .h e .cpp, você aprendeu a regra: declarações no .h, corpos no .cpp. A Ana seguiu a regra com a Caixa:
#pragma once
template <typename T>
class Caixa {
public:
Caixa(T valor);
T pegar() const;
private:
T conteudo;
};
#include "caixa.h"
template <typename T>
Caixa<T>::Caixa(T valor) : conteudo{valor} {}
template <typename T>
T Caixa<T>::pegar() const {
return conteudo;
}
#include <iostream>
#include "caixa.h"
int main() {
Caixa<int> moedas{25};
std::cout << moedas.pegar() << '\n';
}
Cada arquivo compila sem erro. Mas na hora de juntar:
$ g++ -std=c++23 -Wall -Wextra caixa.cpp main.cpp -o caixa /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccMfHt4T.o: na função "main": main.cpp:(.text+0x28): referência não definida para "Caixa<int>::Caixa(int)" /usr/bin/x86_64-linux-gnu-ld.bfd: main.cpp:(.text+0x34): referência não definida para "Caixa<int>::pegar() const" collect2: error: ld returned 1 exit status
- /usr/bin/x86_64-linux-gnu-ld.bfd
- Quem reclama não é o compilador: é o ligador (
ld, de linker), o programa que junta os pedaços compilados num executável (aula Compilar Vários Arquivos). O ligador do Ubuntu fala português, por isso essa parte saiu traduzida. - /tmp/ccMfHt4T.o: na função "main"
- O pedaço compilado do
main.cpp(um arquivo temporário, com nome sorteado), dentro da funçãomain. - referência não definida para "Caixa<int>::Caixa(int)"
- "O
mainchama o construtor deCaixa<int>, e eu não achei o corpo dele em lugar nenhum." O mesmo parapegar(). - collect2: error: ld returned 1 exit status
- O resumo: a ligação falhou, nenhum executável foi criado.
O que aconteceu, passo a passo
Lembre: um template é um molde, e o compilador só "assa" uma função quando vê alguém usando o molde com um tipo. E o g++ compila cada .cpp sozinho, sem olhar os outros. Dá para ver o que saiu de cada um com -c (só compilar, sem ligar) e o nm (lista as funções de um arquivo compilado):
$ g++ -std=c++23 -Wall -Wextra -c caixa.cpp
$ nm -C caixa.o « vazio: nenhuma função foi gerada!
$ g++ -std=c++23 -Wall -Wextra -c main.cpp
$ nm -C main.o | grep Caixa
U Caixa<int>::Caixa(int) « U = undefined: "preciso disto, alguém tem?"
U Caixa<int>::pegar() const
- Ao compilar
caixa.cpp, o compilador vê os moldes, mas ninguém ali usaCaixa<int>. Ele não sabe que tipos você vai querer, então não gera nada. Ocaixa.osai vazio. - Ao compilar
main.cpp, ele vê o uso deCaixa<int>, mas ocaixa.hsó tem as declarações. Sem o corpo, não dá para assar. Ele anota "preciso deCaixa<int>::pegar()" e confia que algum outro arquivo vai ter. - O ligador procura
Caixa<int>::pegar()nos dois arquivos e não acha:referência não definida.
A solução: o corpo do template precisa estar visível onde ele é usado. Na prática, o template inteiro vai no .h (como no caixa.h da seção 4), e não existe caixa.cpp:
$ g++ -std=c++23 -Wall -Wextra main.cpp -o caixa $ ./caixa 25
É por isso que os cabeçalhos da biblioteca padrão (<vector>, <map>) são enormes e cheios de código: são templates, e templates moram no cabeçalho.
Se você sabe de antemão todos os tipos que vai usar, pode manter o caixa.cpp e pedir a geração explicitamente, no fim dele: template class Caixa<int>; ("instanciação explícita"). A Ana testou: com essa linha, o mesmo comando liga e o programa mostra 25. Mas aí uma Caixa<double> volta a dar referência não definida. Para começar, fique com "template inteiro no .h".
6. Formas diferentes, e qual usar
| Forma | Exemplo | Quando usar |
|---|---|---|
| Tipo escrito | Caixa<int> moedas{25}; | Sempre funciona. Use quando o tipo não é óbvio, quando o objeto começa vazio ou com textos entre aspas. |
| CTAD C++17 | Caixa moedas{25}; | Quando o valor deixa o tipo óbvio. |
| Métodos dentro da classe | T pegar() const { return conteudo; } | Métodos curtos. O jeito mais simples. |
Métodos fora da classe, no mesmo .h | template <typename T> T Caixa<T>::pegar() const {…} | Classes grandes, para a declaração da classe ficar curta de ler. |
Corpos num .cpp separado | caixa.cpp | Não use com templates (dá referência não definida), salvo com instanciação explícita. |
Missão: transforme a classe Dupla (que hoje só guarda int) num template, para que o main funcione com notas inteiras e com preços. A saída deve ser 9 e 4.99.
#include <iostream>
class Dupla {
public:
Dupla(int a, int b) : primeiro{a}, segundo{b} {}
int maior() const {
return primeiro > segundo ? primeiro : segundo;
}
private:
int primeiro;
int segundo;
};
int main() {
Dupla<int> notas{7, 9};
Dupla precos{4.99, 3.50};
std::cout << notas.maior() << '\n';
std::cout << precos.maior() << '\n';
}
#include <iostream>
template <typename T>
class Dupla {
public:
Dupla(T a, T b) : primeiro{a}, segundo{b} {}
T maior() const {
return primeiro > segundo ? primeiro : segundo;
}
private:
T primeiro;
T segundo;
};
int main() {
Dupla<int> notas{7, 9};
Dupla precos{4.99, 3.50};
std::cout << notas.maior() << '\n';
std::cout << precos.maior() << '\n';
}
Quando dá errado
referência não definida para "Caixa<int>::pegar() const" (undefined reference)
O que significa: o corpo do template está num .cpp separado, e o arquivo que usa o template só viu a declaração. (Num sistema em inglês, a mensagem é undefined reference to.)
Como consertar: mova os corpos para o .h, logo abaixo da classe, e apague o .cpp do template.
error: missing template arguments before ‘a’
O que significa: você escreveu Caixa a{5}; num padrão anterior ao C++17, que não tem CTAD.
Como consertar: compile com -std=c++23, ou escreva o tipo: Caixa<int> a{5};.
error: class template argument deduction failed
O que significa: você pediu a CTAD (sem <tipo>) e não deu nenhum valor do qual o tipo possa ser deduzido, como em Caixa vazia; ou std::vector lista;.
Como consertar: escreva o tipo: std::vector<int> lista;.
request for member ‘size’ in … which is of non-class type ‘const char*’
O que significa: a CTAD deduziu const char* de um texto entre aspas, e esse tipo não tem métodos.
Como consertar: Caixa<std::string> nome{"Ana"};.
error: invalid conversion from ‘const char*’ to ‘int’ com [with T = int]
O que significa: o objeto foi criado com um tipo (Caixa<int>) e você tentou guardar outro.
Como consertar: crie outro objeto, do tipo certo (Caixa<std::string>). Um objeto nunca muda de tipo.
Referência rápida
| Escrita | O que faz |
|---|---|
template <typename T> class Caixa { … }; | Declara um template de classe |
Caixa<int> c{5}; | Objeto de Caixa com T = int |
Caixa c{5}; C++17 | CTAD: T deduzido pelo construtor |
template <typename T> T Caixa<T>::pegar() const { … } | Corpo de método escrito fora da classe |
template <typename T, typename U = int> | Dois parâmetros, o segundo com valor padrão |
template <typename T, int N> | Parâmetro que é um número (como em std::array<int, 5>) |
template class Caixa<int>; | Instanciação explícita (gera Caixa<int> naquele .cpp) |
Exercícios
1. Usar o template
Complete para criar uma caixa de double com o valor 9.90, escrevendo o tipo:
2. Corpo fora da classe
Complete a primeira linha do corpo do método trocar, escrito fora da classe:
void (T novo) { conteudo = novo; }
3. Com C++17 ou mais novo, qual é o tipo de c em Caixa c{7.5};?
4. A Ana pôs a classe template em caixa.h e os corpos dos métodos em caixa.cpp. O que acontece ao compilar g++ caixa.cpp main.cpp?
5. Em std::vector<std::string> nomes;, o que é std::string?
Missão final: o template no lugar certo
Missão: este projeto tem três arquivos (veja as abas). Rode: a ligação falha, porque os corpos do template estão em caixa.cpp. Mova os dois corpos para o fim do caixa.h e deixe o caixa.cpp vazio (só um comentário). O programa deve mostrar Ana e 42.
#include <iostream>
#include <string>
#include "caixa.h"
int main() {
Caixa<std::string> nome{"Ana"};
Caixa<int> idade{42};
std::cout << nome.pegar() << '\n';
std::cout << idade.pegar() << '\n';
}
#pragma once
template <typename T>
class Caixa {
public:
Caixa(T valor);
T pegar() const;
private:
T conteudo;
};
#include "caixa.h"
template <typename T>
Caixa<T>::Caixa(T valor) : conteudo{valor} {}
template <typename T>
T Caixa<T>::pegar() const {
return conteudo;
}
#pragma once
template <typename T>
class Caixa {
public:
Caixa(T valor);
T pegar() const;
private:
T conteudo;
};
template <typename T>
Caixa<T>::Caixa(T valor) : conteudo{valor} {}
template <typename T>
T Caixa<T>::pegar() const {
return conteudo;
}
// vazio: o template inteiro mora no caixa.h
Resumo
- Um template de classe é o molde de uma classe:
template <typename T> class Caixa { … };. - Cada tipo gera uma classe diferente:
Caixa<int>eCaixa<std::string>não se misturam. - Desde o C++17, a CTAD deduz o tipo pelo construtor:
Caixa c{5};. Cuidado com textos entre aspas (viramconst char*) e objetos vazios (escreva o tipo). std::vector,std::map,std::string… são templates. O<int>destd::vector<int>é o argumento do template.- Corpos fora da classe repetem
template <typename T>e usamCaixa<T>::. - Templates ficam inteiros no
.h. Separados num.cpp, dãoreferência não definidana ligação, porque o compilador só gera o código onde vê o corpo e o uso juntos. - Próximo passo: dizer quais tipos um template aceita, em Concepts.