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.

Você vai aprender
  • 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 ambiguous e 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 é um int: o compilador escolhe a primeira.
mostrar(3.14);
3.14 é um double (números com ponto são double em 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ê.

Você já usa sobrecarga

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:

  1. Encaixe exato: o argumento já é do tipo do parâmetro (42 para int).
  2. Promoção: um tipo "menor" que cabe sem perda num tipo maior parecido: char e bool viram int; float vira double.
  3. Conversão: as outras transformações, como int para double, double para int, ou um texto entre aspas para std::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.

A pegadinha do 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 mostrar com um int é 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 int para long é uma conversão.
candidate 2: ‘void mostrar(double)’
De int para double també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:

CaminhoComo ficaMelhor quando
Só valor padrãovoid saudar(std::string nome = "cliente")As duas versões fazem a mesma coisa; só um valor muda. É mais curto: uma função só.
Só sobrecargavoid 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) e area(largura, altura); mostrar(int) e mostrar(std::string).
  • Não use quando as versões fazem coisas diferentes: processar(int) que salva e processar(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 compilador chamou a versão "errada", sem erro nenhum

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

TermoSignificado
Sobrecarga (overloading)Várias funções com o mesmo nome e parâmetros diferentes
AssinaturaNome + tipos dos parâmetros. O retorno e os nomes dos parâmetros não entram
Encaixe exato > promoção > conversãoA ordem de preferência do compilador ao escolher a versão
call of overloaded … is ambiguousEmpate entre duas ou mais versões
ambiguating new declarationTentou 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" prefere bool a std::string.
  • Próxima aula: Escopo, onde cada variável existe.