Desafio C++

Evitando referências pendentes

Enunciado

Escreva uma função const std::string& escolher_maior(const std::string& a, const std::string& b) que retorna uma referência para a string de maior comprimento. Explique por que essa implementação é perigosa e proponha uma versão segura que retorne uma cópia. No main, demonstre o uso correto da versão segura.

Requisitos

  • A função escolher_maior deve retornar const std::string& e comparar os comprimentos.
  • O main deve chamar a função com strings locais e imprimir o resultado.
  • Comente no código o problema de retornar referência para parâmetro local.
  • A versão segura deve retornar std::string por valor.

Código inicial

#include <iostream>
#include <string>

// TODO: implemente escolher_maior (versão perigosa)
// TODO: implemente escolher_maior_seguro (versão segura)

int main() {
    std::string s1 = "casa";
    std::string s2 = "jardim";
    // TODO: use as funções e imprima
    return 0;
}

Saída esperada

Maior (perigoso): jardim
Maior (seguro): jardim
Ver dica

A versão perigosa retorna referência para um dos parâmetros, que são locais e serão destruídos ao fim da função. A versão segura retorna uma cópia.

Mostrar solução
#include <iostream>
#include <string>

// Versão perigosa: retorna referência para um parâmetro local.
// Após o retorno, a referência fica pendente (dangling).
const std::string& escolher_maior(const std::string& a, const std::string& b) {
    return (a.size() >= b.size()) ? a : b;
}

// Versão segura: retorna uma cópia da string escolhida.
std::string escolher_maior_seguro(const std::string& a, const std::string& b) {
    return (a.size() >= b.size()) ? a : b;
}

int main() {
    std::string s1 = "casa";
    std::string s2 = "jardim";

    // Uso perigoso: a referência retornada aponta para um dos parâmetros,
    // que são destruídos ao final da função. Usar isso é comportamento indefinido.
    // const std::string& ref = escolher_maior(s1, s2); // NÃO FAÇA ISSO
    // std::cout << "Maior (perigoso): " << ref << '\n';

    // Para demonstrar, chamamos imediatamente e imprimimos antes do fim da expressão.
    // Ainda assim, é arriscado. O correto é usar a versão segura.
    std::cout << "Maior (perigoso): " << escolher_maior(s1, s2) << '\n';

    // Versão segura: retorna uma cópia, sem risco de referência pendente.
    std::string maior = escolher_maior_seguro(s1, s2);
    std::cout << "Maior (seguro): " << maior << '\n';

    return 0;
}

Passo a passo

  1. A função escolher_maior recebe duas referências constantes e retorna uma referência constante para a string de maior tamanho.
  2. Dentro da função, os parâmetros a e b são referências para as strings originais, mas eles próprios são variáveis locais da função.
  3. Ao retornar a ou b, retornamos uma referência para um objeto que será destruído quando a função terminar (se os argumentos forem temporários) ou que pode ser válido se os argumentos forem variáveis do chamador. No entanto, o compilador não sabe disso e a referência pode ficar pendente.
  4. No main, evitamos armazenar a referência retornada em uma variável. Em vez disso, usamos a expressão diretamente na saída, mas isso ainda é perigoso e serve apenas para ilustração.
  5. A função escolher_maior_seguro retorna std::string por valor, criando uma cópia da string escolhida. Essa cópia é segura e pode ser usada à vontade.
  6. No main, armazenamos o resultado da versão segura em uma variável local e imprimimos.

Por que funciona

A versão segura funciona porque retorna um objeto por valor, que é copiado (ou movido) para o chamador. A cópia tem tempo de vida controlado pelo chamador, evitando referências pendentes. A versão perigosa tenta evitar a cópia, mas viola as regras de tempo de vida: referências não devem outliver o objeto referenciado.

Erros comuns

  • Armazenar a referência retornada: const std::string& ref = escolher_maior(s1, s2); cria uma referência pendente se s1 e s2 forem temporários ou se a função retornar referência para local. Mesmo com variáveis locais, o compilador pode não detectar, mas é arriscado.
  • Retornar referência para variável local: const std::string& f() { std::string s = "x"; return s; } retorna referência para s, que é destruída. O compilador geralmente avisa, mas é erro grave.
  • Achar que const& sempre é seguro: const& apenas indica que não modificaremos o objeto, mas não garante que ele estará vivo.
  • Ignorar avisos do compilador: muitos compiladores avisam sobre retorno de referência para local; não ignore.

Outra forma de resolver

Usar std::string_view (C++17) para evitar cópias sem possuir o dado, mas ainda é necessário garantir que o dado subjacente permaneça vivo:

#include <string_view>
std::string_view escolher_maior_view(std::string_view a, std::string_view b) {
    return (a.size() >= b.size()) ? a : b;
}

string_view é uma referência não proprietária; deve ser usada apenas enquanto as strings originais existirem.

Saída esperada

Maior (perigoso): jardim
Maior (seguro): jardim