C++ Sobrecarga de Funções
Na aula Funções, duas funções com o mesmo nome deram o erro redefinition. Mas isso só vale quando elas têm também os mesmos parâmetros. Se os parâmetros forem diferentes (outros tipos ou outra quantidade), o C++ aceita várias funções com o mesmo nome, e escolhe a certa olhando os argumentos de cada chamada. Isso se chama sobrecarga (em inglês, overloading). Nesta aula você vai criar funções sobrecarregadas, entender como o compilador escolhe, e conhecer os três erros típicos: a chamada ambígua, a mistura com valor padrão e a tentativa de sobrecarregar só pelo retorno.
- Como criar várias funções com o mesmo nome e parâmetros diferentes.
- Como o compilador escolhe qual delas chamar, e algumas escolhas surpreendentes.
- O erro
call of overloaded ‘…’ is ambiguouse como resolvê-lo. - Por que sobrecarga e valor padrão podem brigar entre si.
- Por que mudar só o tipo de retorno não cria uma sobrecarga (
ambiguating new declaration).
Todos os programas desta aula foram compilados com o g++ 15 na máquina fictícia da Ana, na pasta ~/cpp, com o comando padrão do guia, g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.
A ideia: "abrir" uma porta, uma lata, um arquivo
Em português, usamos o mesmo verbo para coisas bem diferentes: abrir a porta, abrir uma lata, abrir uma conta no banco. Ninguém se confunde, porque o complemento deixa claro o que fazer: com a porta, você gira a maçaneta; com a lata, usa o abridor.
Na sobrecarga, o nome da função é o verbo, e os argumentos são o complemento. mostrar(42) e mostrar("Ana") usam o mesmo nome, mas o compilador vê que um argumento é número e o outro é texto, e chama a versão certa de cada um.
1. Mesmo nome, tipos diferentes
Sem sobrecarga, a Ana precisaria inventar um nome para cada tipo: mostrarInt, mostrarDouble, mostrarTexto. Com sobrecarga, todas se chamam mostrar:
#include <iostream>
#include <string>
void mostrar(int x) {
std::cout << "inteiro: " << x << "\n";
}
void mostrar(double x) {
std::cout << "com vírgula: " << x << "\n";
}
void mostrar(std::string x) {
std::cout << "texto: " << x << "\n";
}
int main() {
mostrar(42);
mostrar(3.14);
mostrar(std::string("Ana"));
}
$ g++ -std=c++23 -Wall -Wextra mostrar.cpp -o mostrar $ ./mostrar inteiro: 42 com vírgula: 3.14 texto: Ana
- void mostrar(int x)
- A versão para inteiros.
- void mostrar(double x)
- Mesmo nome, parâmetro de outro tipo: é outra função. O compilador não reclama de redefinição, porque as duas são diferentes.
- mostrar(42);
42é umint: o compilador escolhe a primeira.- mostrar(3.14);
3.14é umdouble(números com ponto sãodoubleem C++): a segunda.- mostrar(std::string("Ana"));
- Um
std::string: a terceira.
Para o compilador, uma função é identificada pelo nome junto com os tipos dos parâmetros. Esse conjunto se chama assinatura. mostrar(int), mostrar(double) e mostrar(std::string) têm assinaturas diferentes, então são três funções. Repare que as mensagens de erro do g++ sempre mostram a assinatura completa, como ‘void item(std::string, int, double)’: é assim que o compilador "chama" cada função.
Mesmo nome, quantidade diferente
A sobrecarga também vale quando o número de parâmetros muda:
#include <iostream>
double area(double lado) {
return lado * lado;
}
double area(double largura, double altura) {
return largura * altura;
}
int main() {
std::cout << "Quadrado 3: " << area(3) << "\n";
std::cout << "Retângulo 3 x 5: " << area(3, 5) << "\n";
}
$ g++ -std=c++23 -Wall -Wextra area.cpp -o area $ ./area Quadrado 3: 9 Retângulo 3 x 5: 15
Um argumento: área do quadrado. Dois: área do retângulo. O nome conta a mesma história ("calcule a área"), e os argumentos dizem de quê.
O std::cout << mostra números, textos e caracteres porque o << é sobrecarregado para cada tipo. std::to_string (Texto ↔ Número) tem uma versão para int, outra para double… E std::abs e std::max (Matemática) também. Quando bem usada, a sobrecarga é invisível: você só pensa "mostrar", "converter", "valor absoluto".
Um exemplo completo: o maior de dois
#include <iostream>
#include <string>
int maior(int a, int b) {
if (a > b) {
return a;
}
return b;
}
double maior(double a, double b) {
if (a > b) {
return a;
}
return b;
}
std::string maior(std::string a, std::string b) {
if (a.size() > b.size()) {
return a;
}
return b;
}
int main() {
std::cout << maior(3, 9) << "\n";
std::cout << maior(2.5, 1.5) << "\n";
std::cout << maior(std::string("Ana"), std::string("Bruno")) << "\n";
}
$ g++ -std=c++23 -Wall -Wextra maior.cpp -o maior $ ./maior 9 2.5 Bruno « para textos, "maior" aqui é o de mais letras
Cada versão pode devolver um tipo diferente (int, double, std::string): o que precisa mudar são os parâmetros. Repare também que as versões int e double são idênticas por dentro. Quando isso acontece, existe um recurso melhor que escrever a mesma função várias vezes: os templates de função, que você verá no Nível 3.
2. Como o compilador escolhe
Em cada chamada, o compilador olha os tipos dos argumentos e procura a versão que combina melhor. De forma simplificada, ele prefere, nesta ordem:
- Encaixe exato: o argumento já é do tipo do parâmetro (
42paraint). - Promoção: um tipo "menor" que cabe sem perda num tipo maior parecido:
chareboolviramint;floatviradouble. - Conversão: as outras transformações, como
intparadouble,doubleparaint, ou um texto entre aspas parastd::string.
Veja o que acontece com argumentos que não são exatamente int, double ou std::string, com as mesmas três funções mostrar do começo:
int main() {
mostrar('A');
mostrar(2.5f);
mostrar(true);
mostrar("Ana");
}
$ g++ -std=c++23 -Wall -Wextra escolha.cpp -o escolha $ ./escolha inteiro: 65 « 'A' é um char: promovido a int (65 é o código do A) com vírgula: 2.5 « 2.5f é um float: promovido a double inteiro: 1 « true é um bool: promovido a int texto: Ana « "Ana" convertido para std::string
As três primeiras são promoções, e cada uma encontrou o seu destino sem dúvida. A quarta é uma conversão, mas só existia uma versão capaz de receber texto. Nenhuma deu erro, mas duas ('A' e true virando números) provavelmente não eram o que a Ana queria. Sobrecarga com tipos "parecidos" pode surpreender.
bool
Se existir uma versão para bool, um texto entre aspas vai para ela, e não para a de std::string:
#include <iostream>
#include <string>
void mostrar(bool x) {
std::cout << "sim/não: " << x << "\n";
}
void mostrar(std::string x) {
std::cout << "texto: " << x << "\n";
}
int main() {
mostrar("Ana");
}
$ g++ -std=c++23 -Wall -Wextra bool.cpp -o bool $ ./bool sim/não: 1 « sem erro, sem aviso
Um texto entre aspas não é um std::string (é um const char*, um endereço na memória), e a regra do C++ prefere transformá-lo em bool ("o endereço existe? sim") a criar um std::string. Para chamar a versão de texto, passe um std::string de verdade: mostrar(std::string("Ana")), ou uma variável std::string. Foi por isso que o primeiro exemplo da aula usou std::string("Ana").
3. Quando o compilador não consegue escolher: ambiguidade
E se duas versões combinam igualmente bem? A Ana escreveu uma versão para long (inteiro grande, aula Inteiros) e outra para double, e chamou com um int:
#include <iostream>
void mostrar(long x) {
std::cout << "long: " << x << "\n";
}
void mostrar(double x) {
std::cout << "double: " << x << "\n";
}
int main() {
mostrar(5);
}
$ g++ -std=c++23 -Wall -Wextra ambiguo.cpp -o ambiguo
ambiguo.cpp: In function ‘int main()’:
ambiguo.cpp:12:12: error: call of overloaded ‘mostrar(int)’ is ambiguous
12 | mostrar(5);
| ~~~~~~~^~~
ambiguo.cpp:12:12: note: there are 2 candidates
ambiguo.cpp:3:6: note: candidate 1: ‘void mostrar(long int)’
3 | void mostrar(long x) {
| ^~~~~~~
ambiguo.cpp:7:6: note: candidate 2: ‘void mostrar(double)’
7 | void mostrar(double x) {
| ^~~~~~~
- call of overloaded ‘mostrar(int)’ is ambiguous
- "A chamada da função sobrecarregada
mostrarcom uminté ambígua". Ambígua = tem mais de um sentido possível. - note: there are 2 candidates
- "Há 2 candidatas", e o g++ lista cada uma com a linha onde está.
- candidate 1: ‘void mostrar(long int)’
- De
intparalongé uma conversão. - candidate 2: ‘void mostrar(double)’
- De
intparadoubletambém é uma conversão. Empate: nenhuma é melhor que a outra, e o compilador se recusa a sortear.
Ambiguidade é erro de compilação, não aviso: o C++ prefere parar a adivinhar. Para resolver, deixe a chamada exata: mostrar(5L) (o L faz o número ser long), mostrar(5.0) (um double), ou acrescente uma versão mostrar(int).
Com vários parâmetros, o empate aparece quando cada versão ganha num argumento diferente:
$ g++ -std=c++23 -Wall -Wextra misto.cpp -o misto « maior(3, 2.5) com as versões int e double
misto.cpp: In function ‘int main()’:
misto.cpp:18:23: error: call of overloaded ‘maior(int, double)’ is ambiguous
18 | std::cout << maior(3, 2.5) << "\n";
| ~~~~~^~~~~~~~
misto.cpp:18:23: note: there are 2 candidates
misto.cpp:3:5: note: candidate 1: ‘int maior(int, int)’
3 | int maior(int a, int b) {
| ^~~~~
misto.cpp:10:8: note: candidate 2: ‘double maior(double, double)’
10 | double maior(double a, double b) {
| ^~~~~
Para a versão int, o 3 encaixa exato e o 2.5 precisa de conversão; para a versão double, é o contrário. Empate de novo.
Missão: rode e leia o erro (no editor, o Clang escreve call to 'maior' is ambiguous). Conserte só a chamada, sem apagar nenhuma das duas funções, para o programa mostrar 3.
#include <iostream>
int maior(int a, int b) {
if (a > b) {
return a;
}
return b;
}
double maior(double a, double b) {
if (a > b) {
return a;
}
return b;
}
int main() {
std::cout << maior(3, 2.5) << "\n";
}
#include <iostream>
int maior(int a, int b) {
if (a > b) {
return a;
}
return b;
}
double maior(double a, double b) {
if (a > b) {
return a;
}
return b;
}
int main() {
std::cout << maior(3.0, 2.5) << "\n";
}
4. Sobrecarga × valor padrão
Na aula Valores Padrão, uma função só podia ser chamada com menos argumentos. A sobrecarga também permite isso. Misturar as duas coisas no mesmo nome pode criar um empate:
#include <iostream>
#include <string>
void saudar() {
std::cout << "Olá!\n";
}
void saudar(std::string nome = "cliente") {
std::cout << "Olá, " << nome << "!\n";
}
int main() {
saudar("Ana");
saudar();
}
$ g++ -std=c++23 -Wall -Wextra padrao.cpp -o padrao
padrao.cpp: In function ‘int main()’:
padrao.cpp:14:11: error: call of overloaded ‘saudar()’ is ambiguous
14 | saudar();
| ~~~~~~^~
padrao.cpp:14:11: note: there are 2 candidates
padrao.cpp:4:6: note: candidate 1: ‘void saudar()’
4 | void saudar() {
| ^~~~~~
padrao.cpp:8:6: note: candidate 2: ‘void saudar(std::string)’
8 | void saudar(std::string nome = "cliente") {
| ^~~~~~
As duas funções, sozinhas, são válidas, e saudar("Ana") (linha 13) não teve problema: só a segunda aceita um texto. Mas saudar() sem argumentos combina com as duas: a primeira não pede nada, e a segunda tem tudo com padrão. Repare que o erro aparece na chamada, não na definição. Escolha um dos dois caminhos:
| Caminho | Como fica | Melhor quando |
|---|---|---|
| Só valor padrão | void saudar(std::string nome = "cliente") | As duas versões fazem a mesma coisa; só um valor muda. É mais curto: uma função só. |
| Só sobrecarga | void saudar() e void saudar(std::string nome) | As versões fazem coisas diferentes (aqui, "Olá!" sem nome é uma frase diferente). |
5. Só o tipo de retorno diferente não basta
A Ana quis duas versões de lerNumero: uma que devolve int e outra que devolve double, ambas sem parâmetros:
#include <iostream>
int lerNumero() {
return 7;
}
double lerNumero() {
return 7.5;
}
int main() {
std::cout << lerNumero() << "\n";
}
$ g++ -std=c++23 -Wall -Wextra retorno.cpp -o retorno
retorno.cpp:7:8: error: ambiguating new declaration of ‘double lerNumero()’
7 | double lerNumero() {
| ^~~~~~~~~
retorno.cpp:3:5: note: old declaration ‘int lerNumero()’
3 | int lerNumero() {
| ^~~~~~~~~
Ambiguating new declaration = "nova declaração que cria ambiguidade"; a nota old declaration ("declaração antiga") mostra a primeira. O erro aparece já na definição, sem precisar de nenhuma chamada. O motivo: na chamada lerNumero(), o compilador só tem o nome e os argumentos para decidir. O que você vai fazer com o resultado (guardar num int, mostrar, somar) não entra na escolha. Como o tipo de retorno não faz parte da assinatura, as duas funções têm a mesma assinatura, lerNumero().
No editor do guia, o Clang diz isso com todas as letras: functions that differ only in their return type cannot be overloaded ("funções que diferem só no tipo de retorno não podem ser sobrecarregadas"). A saída é dar nomes diferentes: lerInteiro() e lerDecimal().
Nomes de parâmetros diferentes também não contam
$ g++ -std=c++23 -Wall -Wextra nomes.cpp -o nomes « int dobro(int x) e int dobro(int numero)
nomes.cpp:7:5: error: redefinition of ‘int dobro(int)’
7 | int dobro(int numero) {
| ^~~~~
nomes.cpp:3:5: note: ‘int dobro(int)’ previously defined here
3 | int dobro(int x) {
| ^~~~~
Para o compilador, as duas são dobro(int): o nome do parâmetro não faz parte da assinatura. Resultado: redefinição.
| O que muda entre as duas funções | É sobrecarga válida? |
|---|---|
Tipos dos parâmetros: f(int) × f(double) | Sim |
Quantidade de parâmetros: f(int) × f(int, int) | Sim |
Ordem dos tipos: f(int, double) × f(double, int) | Sim (mas confunde quem lê; evite) |
Só o tipo de retorno: int f() × double f() | Não: ambiguating new declaration |
Só o nome dos parâmetros: f(int x) × f(int y) | Não: redefinition |
Quando usar sobrecarga
- Use quando as versões fazem a mesma coisa com tipos ou quantidades diferentes de informação:
area(lado)earea(largura, altura);mostrar(int)emostrar(std::string). - Não use quando as versões fazem coisas diferentes:
processar(int)que salva eprocessar(std::string)que apaga é uma armadilha para quem lê. Dê nomes diferentes. - Evite sobrecargas com tipos "vizinhos" (
int/long/double,bool/std::string): é onde moram as ambiguidades e as escolhas surpreendentes. - Se o corpo das versões é idêntico e só o tipo muda, prefira um template.
Quando dá errado
error: call of overloaded ‘…’ is ambiguous
O que significa: mais de uma versão da função combina igualmente bem com os argumentos da chamada. A nota there are N candidates lista as versões empatadas.
Como consertar: deixe os argumentos com o tipo exato de uma das versões (5.0 em vez de 5, 5L para long, std::string("…") para texto); ou acrescente a versão que falta; ou, se o empate é com um valor padrão, tire o padrão ou a sobrecarga.
error: ambiguating new declaration of ‘double lerNumero()’
O que significa: duas funções com o mesmo nome e os mesmos parâmetros, diferindo só no tipo de retorno.
Como consertar: dê nomes diferentes às funções, ou faça os parâmetros serem diferentes.
error: redefinition of ‘int dobro(int)’
O que significa: duas funções com a mesma assinatura. Mudar o nome dos parâmetros não basta.
Como consertar: apague uma, ou mude o tipo ou a quantidade dos parâmetros, se a ideia era uma sobrecarga.
O que significa: o argumento foi promovido ou convertido para outro tipo: 'A' foi para a versão int, "Ana" foi para a versão bool.
Como consertar: passe argumentos do tipo exato da versão desejada, ou acrescente a versão para o tipo que você está usando (por exemplo, mostrar(char)).
Referência rápida
| Termo | Significado |
|---|---|
| Sobrecarga (overloading) | Várias funções com o mesmo nome e parâmetros diferentes |
| Assinatura | Nome + tipos dos parâmetros. O retorno e os nomes dos parâmetros não entram |
| Encaixe exato > promoção > conversão | A ordem de preferência do compilador ao escolher a versão |
call of overloaded … is ambiguous | Empate entre duas ou mais versões |
ambiguating new declaration | Tentou sobrecarregar só pelo tipo de retorno |
Exercícios
1. Qual é chamada?
Existem void f(int) e void f(double). Qual versão a chamada f(2.0) usa? Responda int ou double.
2. Promoção
Existem void f(int) e void f(std::string). Que número aparece se f(int) mostra o valor recebido e a chamada é f('B')? (O código do 'B' é um a mais que o do 'A'.)
3. A assinatura
Escreva a assinatura (nome e tipos, como o g++ mostra, sem o retorno) de double media(double a, double b, double c):
4. Qual par de funções não pode existir no mesmo programa?
5. Existem void tocar() e void tocar(int vezes = 1). O que acontece com tocar(3) e com tocar()?
6. Existem void mostrar(bool) e void mostrar(std::string). O que mostrar("Oi") chama?
Missão final: três etiquetas, um nome
Missão: crie três funções chamadas etiqueta, sem usar valores padrão. etiqueta(std::string produto) mostra produto: consulte o preço; etiqueta(std::string produto, double preco) mostra produto: R$ preco; etiqueta(std::string produto, int quantidade, double preco) mostra quantidade x produto: R$ total. Saída esperada:
borracha: consulte o preço caneta: R$ 2.5 3 x caderno: R$ 30
#include <iostream>
#include <string>
// crie as três funções etiqueta aqui
int main() {
etiqueta("borracha");
etiqueta("caneta", 2.5);
etiqueta("caderno", 3, 10.0);
}
#include <iostream>
#include <string>
void etiqueta(std::string produto) {
std::cout << produto << ": consulte o preço\n";
}
void etiqueta(std::string produto, double preco) {
std::cout << produto << ": R$ " << preco << "\n";
}
void etiqueta(std::string produto, int quantidade, double preco) {
std::cout << quantidade << " x " << produto << ": R$ " << quantidade * preco << "\n";
}
int main() {
etiqueta("borracha");
etiqueta("caneta", 2.5);
etiqueta("caderno", 3, 10.0);
}
Resumo
- Sobrecarga: várias funções com o mesmo nome, desde que os parâmetros sejam diferentes (tipo ou quantidade).
- O compilador escolhe pela assinatura (nome + tipos dos parâmetros), preferindo encaixe exato, depois promoção (
char/bool→int,float→double), depois conversão. - Empate entre versões:
call of overloaded ‘…’ is ambiguous. Resolva deixando o argumento com o tipo exato. - Sobrecarga + valor padrão no mesmo nome pode empatar na chamada sem argumentos. Escolha um dos dois.
- Só o tipo de retorno diferente não é sobrecarga:
ambiguating new declaration. Só o nome do parâmetro diferente:redefinition. - Cuidado com tipos vizinhos:
"Ana"prefereboolastd::string. - Próxima aula: Escopo, onde cada variável existe.