Um Nome, Vários Sentidos — Sobrecarga de Funções

Um Nome, Vários Sentidos — Sobrecarga de Funções

Ter três nomes para valor absoluto não era capricho da biblioteca do C: dois símbolos iguais brigam no linker, e a saída foi um nome por tipo. O C++ dispensa a cerimônia — várias funções podem ter o mesmo nome, e o compilador escolhe pelos argumentos. Aqui está onde essa liberdade vira ambiguidade.
Linguagem C++

12 min de leitura

Fechamos o artigo A Disciplina do const — Promessas que o Compilador Cobra com a const-correctness firme e uma provocação: em C, quando você queria calcular o valor absoluto, tinha três nomes diferentes na cabeça — abs para int, labs para long, fabs para double. Isso não era um capricho; era uma limitação. Em C, dois símbolos com o mesmo nome brigam entre si no linker, então a biblioteca padrão foi obrigada a inventar um nome distinto para cada tipo. O C++ elimina essa cerimônia com a sobrecarga de funções: você pode ter várias funções chamadas abs, e o compilador descobre qual usar olhando os argumentos. Hoje você aprende a projetar APIs assim, entende exatamente como o compilador faz a escolha, e conhece as ambiguidades que surgem quando essa liberdade é usada sem cuidado.

A ideia: mesmo nome, assinaturas diferentes

Sobrecarregar é declarar duas ou mais funções com o mesmo nome, desde que difiram na lista de parâmetros — em quantidade ou em tipo. O compilador trata cada uma como uma função distinta e, na hora da chamada, escolhe a que melhor encaixa nos argumentos:

#include <iostream>
#include <string>

// Três funções, um só nome. O compilador as distingue pelos parâmetros.
void imprime(int x) {
    std::cout << "inteiro: " << x << '\n';
}

void imprime(double x) {
    std::cout << "real: " << x << '\n';
}

void imprime(const std::string& x) {
    std::cout << "texto: " << x << '\n';
}

int main() {
    imprime(42);           // casa com imprime(int)    → inteiro: 42
    imprime(3.14);         // casa com imprime(double) → real: 3.14
    imprime(std::string{"oi"});  // casa com imprime(const std::string&) → texto: oi
    return 0;
}

Um único nome, imprime, com três comportamentos que fazem sentido para o leitor. Isso é mais do que economia de nomes: é uma API coerente. Quem usa não precisa lembrar imprime_int, imprime_double, imprime_string — só imprime, e o compilador acerta o alvo.

O que conta como "diferença" — e o que não conta

Aqui mora a primeira armadilha. A diferença tem de estar na assinatura, e a assinatura é feita dos tipos dos parâmetros, não do tipo de retorno. Duas funções que diferem no retorno não são uma sobrecarga válida:

int  calcula(int x);
// double calcula(int x);   // ERRO: difere só no retorno — ambíguo, não compila

O motivo é lógico: quando você escreve calcula(5); sem usar o resultado, o compilador não teria como saber qual versão você quis. O retorno não participa da escolha, então não pode ser o único critério de distinção.

Um segundo ponto sutil: const no parâmetro por valor também não conta. void f(int) e void f(const int) são a mesma função aos olhos do compilador, porque uma cópia ser const ou não é irrelevante para quem chama. Mas const numa referência conta, porque muda o que pode ser passado:

#include <iostream>
#include <string>

void toca(std::string& s)       { std::cout << "versão modificável\n"; s += "."; }
void toca(const std::string& s) { std::cout << "versão só-leitura\n"; }

int main() {
    std::string mutavel = "abc";
    const std::string fixo = "xyz";

    toca(mutavel);   // escolhe a versão std::string& (modificável)
    toca(fixo);      // escolhe a versão const std::string& (fixo não aceita &)
    toca("literal"); // literal é temporário → também vai para a const&
    return 0;
}

Repare como as lições anteriores se encaixam: a distinção T& vs const T& (artigos Referências e Ponteiros, Frente a Frente e A Disciplina do const — Promessas que o Compilador Cobra) agora vira um critério de sobrecarga. Um objeto const ou um temporário só casam com a versão const&; um objeto modificável prefere a versão não-const quando ela existe.

Como o compilador escolhe: resolução de sobrecarga

Quando você chama imprime(algo), o compilador faz uma triagem em etapas. Primeiro, monta o conjunto de candidatas (todas as imprime visíveis). Depois, descarta as que não poderiam receber aquele argumento. Entre as que sobraram, procura a melhor correspondência: uma casa exata (int para int) ganha de uma que exige promoção (char para int), que por sua vez ganha de uma que exige conversão (int para double). Se, no fim, houver um vencedor único, ele é escolhido. Se duas candidatas empatam em "qualidade", o compilador declara ambiguidade e recusa compilar:

void g(long x)   { }
void g(double x) { }

int main() {
    g(10);   // ERRO de ambiguidade: 10 é int; converter para long e para
             // double são conversões de "mesmo peso". O compilador não decide.
    return 0;
}

A mensagem será algo como call of overloaded 'g(int)' is ambiguous. A cura é dar ao compilador uma escolha inequívoca — por exemplo, chamar g(10L) (o sufixo L faz o literal ser long) ou fornecer uma sobrecarga exata g(int). A lição honesta: sobrecarga é poderosa, mas sobrecargas cujos tipos são "conversíveis entre si" convidam à ambiguidade. Prefira conjuntos de sobrecargas com tipos bem distintos.

Argumentos padrão: um primo próximo (e uma pegadinha)

O C++ tem outro recurso que reduz a necessidade de sobrecarga: argumentos padrão. Você dá um valor de reserva a um parâmetro, e quem chama pode omiti-lo:

#include <iostream>
#include <string>

// 'moldura' tem valor padrão; se omitido, vale "***".
void anuncia(const std::string& msg, const std::string& moldura = "***") {
    std::cout << moldura << ' ' << msg << ' ' << moldura << '\n';
}

int main() {
    anuncia("Bem-vindo");            // usa o padrão → *** Bem-vindo ***
    anuncia("Perigo", "!!!");        // sobrescreve   → !!! Perigo !!!
    return 0;
}

Isso substitui, com elegância, duas sobrecargas que só diferiam por ter ou não o segundo parâmetro. A pegadinha: argumentos padrão só podem vir à direita — depois de um parâmetro com padrão, todos os seguintes também precisam ter. E cuidado ao misturar argumentos padrão com sobrecarga: é fácil criar duas formas de chamar que o compilador não consegue distinguir. Quando em dúvida entre argumento padrão e sobrecarga, prefira o argumento padrão para casos que diferem só na quantidade de parâmetros, e a sobrecarga quando diferem no tipo ou no comportamento.

Juntando tudo: uma pequena API coerente

Reunindo streams (A Filosofia por Trás do C++ e a Saída Elegante do printf), referências e const (Referências e Ponteiros, Frente a Frente, A Disciplina do const) e a sobrecarga de hoje, eis uma função de formatação com três rostos que convivem sem conflito:

#include <iostream>
#include <string>

// Formata um rótulo seguido de um valor, para tipos diferentes.
void log(const std::string& rotulo, int valor) {
    std::cout << rotulo << " = " << valor << " (int)\n";
}
void log(const std::string& rotulo, double valor) {
    std::cout << rotulo << " = " << valor << " (double)\n";
}
void log(const std::string& rotulo, const std::string& valor) {
    std::cout << rotulo << " = \"" << valor << "\" (texto)\n";
}

int main() {
    log("idade", 30);              // idade = 30 (int)
    log("altura", 1.75);           // altura = 1.75 (double)
    log("nome", std::string{"Ana"}); // nome = "Ana" (texto)
    return 0;
}

O chamador usa sempre log, e o compilador roteia para a implementação certa. Cada sobrecarga recebe o rotulo por const std::string& — a decisão de projeto que você já toma no automático. Esse é o cheiro de uma API C++ bem-feita: poucos nomes, muitos significados bem escolhidos.

A sobrecarga resolve um problema de nomes e cria outro, de ambiguidade — e a fronteira entre os dois é mais estreita do que parece. Tipo de retorno não conta como diferença; conversão implícita entrega o desempate a regras que ninguém decora; e argumento padrão combinado com sobrecarga produz chamadas que o compilador recusa sem explicar direito o motivo. A saída não é técnica, é de projeto: sobrecarregue quando as versões fazem a mesma coisa com tipos diferentes, e dê nomes distintos quando fazem coisas distintas — porque o leitor não lê a tabela de resolução, lê o nome.

Fontes e leituras recomendadas

  • Bjarne Stroustrup, A Tour of C++ (3ª ed.), seção sobre funções e sobrecarga: a apresentação canônica do recurso pelo criador da linguagem.
  • cppreference.com/w/cpp/language/overload_resolution: a descrição técnica completa do algoritmo de resolução de sobrecarga, útil para decifrar mensagens de ambiguidade.
  • cppreference.com/w/cpp/language/default_arguments: as regras precisas dos argumentos padrão, incluindo a exigência de virem à direita.
  • Scott Meyers, Effective C++ (3ª ed.), Item 24 ("Declare non-member functions when type conversions should apply to all parameters"): discute como conversões e sobrecarga interagem em casos sutis.
  • ISO C++ Core Guidelines, regras F.51 ("Where there is a choice, prefer default arguments over overloading") e C.163: orientação oficial sobre quando escolher argumento padrão e quando sobrecarregar.

Exercícios

Exercício 1

Escreva duas sobrecargas de uma função area: uma que recebe o lado de um quadrado (double lado) e outra que recebe base e altura de um retângulo (double base, double altura). Demonstre as duas no main.

Ver resposta

✓ Resposta: Duas assinaturas distintas pelo número de parâmetros:

#include <iostream>

double area(double lado) {             // quadrado
    return lado * lado;
}
double area(double base, double altura) {  // retângulo
    return base * altura;
}

int main() {
    std::cout << "quadrado: "  << area(4.0)       << '\n';   // 16
    std::cout << "retângulo: " << area(3.0, 5.0)  << '\n';   // 15
    return 0;
}

O compilador distingue pela quantidade de argumentos na chamada.

Exercício 2

Diga, sem compilar, se cada par abaixo é uma sobrecarga válida. Justifique:

// (a)
int  f(int x);
long f(int x);
// (b)
void g(int x);
void g(double x);
// (c)
void h(int x);
void h(const int x);
Ver resposta

✓ Resposta: Análise: - (a) Inválida. As duas diferem apenas no tipo de retorno (int vs long); a lista de parâmetros é idêntica (int x). O retorno não faz parte da assinatura, então não compila. - (b) Válida. Os tipos dos parâmetros diferem (int vs double), que é exatamente o critério legítimo de sobrecarga. - (c) Inválida. Para parâmetros por valor, const int e int são a mesma assinatura — o const numa cópia não muda nada para o chamador. É uma redefinição, não uma sobrecarga.

Exercício 3

O código a seguir não compila por ambiguidade. Explique a causa e mostre duas formas de resolver.

void mostra(float x)  { }
void mostra(double x) { }
int main() {
    mostra(3);   // 3 é int
    return 0;
}
Ver resposta

✓ Resposta: A ambiguidade ocorre porque 3 é int, e converter int para float e para double são conversões consideradas de peso equivalente pelo compilador; nenhuma é "melhor", então ele se recusa a escolher. Duas soluções:

// Solução A: tornar o literal explicitamente double.
mostra(3.0);   // agora casa exatamente com mostra(double)
// Solução B: adicionar uma sobrecarga que case exatamente com int.
void mostra(int x) { /* ... */ }
// Agora mostra(3) tem correspondência exata e ganha das conversões.

A solução A ajusta a chamada; a B ajusta a API. Ambas removem o empate.

Exercício 4

Reescreva o par de funções abaixo — que só diferem por uma ter um parâmetro a mais — usando um argumento padrão em vez de duas sobrecargas.

#include <iostream>
void repete(const std::string& s) {
    std::cout << s << '\n';
}
void repete(const std::string& s, int vezes) {
    for (int i = 0; i < vezes; ++i) std::cout << s << '\n';
}
Ver resposta

✓ Resposta: Uma única função com argumento padrão substitui as duas:

#include <iostream>
#include <string>

// 'vezes' padrão = 1 cobre o caso de "imprimir uma vez".
void repete(const std::string& s, int vezes = 1) {
    for (int i = 0; i < vezes; ++i)
        std::cout << s << '\n';
}

int main() {
    repete("olá");       // imprime uma vez (usa o padrão)
    repete("eco", 3);    // imprime três vezes
    return 0;
}

Isso segue a diretriz F.51 das Core Guidelines: quando as formas diferem só na quantidade de parâmetros, o argumento padrão é mais enxuto que duas sobrecargas.

Exercício 5

Considere as sobrecargas void p(std::string& s) e void p(const std::string& s). Explique qual delas é chamada em cada uma das três situações abaixo e por quê:

std::string a = "x";
const std::string b = "y";
p(a);
p(b);
p("z");
Ver resposta

✓ Resposta: As escolhas: - p(a) → chama p(std::string&). a é modificável, então casa com a referência não-const, que é a correspondência preferida quando disponível. - p(b) → chama p(const std::string&). b é const; ligá-lo a um std::string& não-const violaria sua imutabilidade, então só a versão const& é viável. - p("z") → chama p(const std::string&). O literal vira um std::string temporário; temporários não podem ligar-se a referências não-const, apenas a const&. É a mesma regra dos artigos Referências e Ponteiros, Frente a Frente e A Disciplina do const — Promessas que o Compilador Cobra governando a resolução de sobrecarga.

Comentários

Mais em Linguagem C++

A Disciplina do const — Promessas que o Compilador Cobra
A Disciplina do const — Promessas que o Compilador Cobra

Em C o const é etiqueta modesta, que muita gente ignora sem prejuízo. Em C++…

Percorrendo com Elegância — o Range-Based for a Fundo
Percorrendo com Elegância — o Range-Based for a Fundo

A sintaxe curta esconde uma decisão que custa caro se for tomada por descuido…

Posse Compartilhada e seus Perigos — shared_ptr e weak_ptr
Posse Compartilhada e seus Perigos — shared_ptr e weak_ptr

Quando várias partes do programa precisam legitimamente compartilhar um…