CodeGym /Cursos /C++ SELF /const T&: leitura sem cópia e ligação a temporários

const T&: leitura sem cópia e ligação a temporários

C++ SELF
Nível 40, Lição 0
Disponível

1. Por que usar const T&

Quando você está começando a escrever funções, tende a escolher parâmetros pelo princípio “contanto que compile”. Mas logo se nota que copiar objetos grandes (strings, vetores, structs com muitos campos) pode ser caro, e T& é um contrato demasiado “forte”: ele promete que a função pode modificar o argumento. const T& é o meio-termo ideal: sem cópia, mas somente leitura, e ainda aceita temporários.

É prático pensar assim: const T& é “vou tomar seu objeto emprestado durante a chamada e prometo não modificar nada nele”.

O que significa const em const T&

Importante começar com uma pequena introdução: a palavra const em C++ frequentemente assusta iniciantes, que pensam que “tudo fica proibido”. Na verdade const geralmente não é sobre proibição, mas sobre contrato. Você diz ao compilador (e a quem lê o código): “por esse nome eu não vou modificar o objeto”. E se você tentar — o compilador te impedirá, como um segurança no museu: “não toque com as mãos”.

Sentido chave:

const T& é uma referência para T, mas não se pode modificar o objeto através dessa referência.

O objeto referenciado pode, no entanto, não ser const por si só.

Veja um mini-exemplo:

#include <iostream>

int main() {
    int x = 10;
    const int& r = x;   // r - apelido somente leitura de x

    std::cout << r << '\n'; // 10
    // r += 1; // erro de compilação: não é possível modificar via referência const
}

Aqui x é um int normal, pode ser modificado. Mas via o nome r — não se pode. Isso é o contrato.

A que se pode ligar const T&

As referências em C++ se ligam seguindo regras (frequentemente chamadas de binding rules). E essas regras são uma das razões pelas quais const T& aparece no código real mais frequentemente do que apenas T&.

Resumo: T& é a referência “editável”, e const T& é a “somente leitura”. Portanto, const T& permite a ligação em mais situações, porque é mais seguro: você não poderá modificar algo que não deve ou não vale a pena modificar.

Mini-tabela de compatibilidade

O que temos à direita (expressão) Pode ligar a T& Pode ligar a const T&
Variável normal T x Sim Sim
Constante const T cx Não Sim
Valor temporário (temporary), por exemplo T() Não Sim

Esta é uma versão intuitiva das regras, suficiente para nosso nível. No padrão existem formulações formais e até discussões separadas sobre “temporaries bound to references” e “lifetime extension”, pois o tema é sutil.

Exemplo: T& não se liga a const T

#include <string>

int main() {
    const std::string name = "Alice";

    // std::string& bad = name; // erro: não se pode dar referência "modificadora" a um objeto const
    const std::string& ok = name; // ok: eu prometo não modificar
    (void)ok;
}

A lógica é simples: se std::string& se ligasse a const std::string, você poderia modificar a string “constante” — isso quebraria a própria ideia de const.

2. Objetos temporários e extensão de tempo de vida

O que são objetos temporários e por que const T& funciona bem com eles

Objetos temporários são os “de um dia” do C++: surgem como resultado de expressões e geralmente desaparecem rapidamente. Iniciantes muitas vezes não notam sua existência porque eles não têm nome. Mas o compilador os cria, e as regras de tempo de vida são realmente importantes.

Exemplos de valores temporários:

  • o resultado de a + b (para números),
  • o resultado de std::string("Hi"),
  • o resultado da concatenação de strings s1 + s2.

Exemplo: const int& para um número temporário

#include <iostream>

int main() {
    const int& answer = 42;      // 42 - valor temporário
    std::cout << answer << '\n'; // 42
}

Por que isso é permitido? Porque answer não pode modificar o temporário (é const), então é seguro.

Com int& isso não funciona:

int main() {
    // int& r = 42; // erro: não é possível ligar T& a um temporário
}

Extensão do tempo de vida do temporário

Vamos com cuidado: objetos temporários normalmente vivem pouco. Frequentemente, até o final da “full-expression” (em linguagem informal: até o fim da linha/expressão). Mas existe uma regra importante: se um temporário é ligado a uma variável do tipo const T&, o tempo de vida do temporário é estendido até o final do escopo dessa referência.

Essa regra é uma das razões principais pelas quais const T& é tão popular. E sim, há sutilezas e discussões no padrão (por exemplo, sobre “lifetime extension of references” em várias situações).

Exemplo: uma string temporária “vive mais” do que parece

#include <iostream>
#include <string>

int main() {
    const std::string& s = std::string("hello"); // temporário std::string
    std::cout << s << '\n'; // hello
}

Se não houvesse a regra de extensão, isso pareceria “referência a uma string que some”, mas aqui está tudo bem: o temporário vive enquanto s existir.

Esquema do tempo de vida

flowchart LR
    A["Criou temporário: std::string('hello')"] --> B["Vinculado a const std::string& s"]
    B --> C["temporário vive até o fim do scope da variável s"]
    C --> D["Saímos do scope: s é destruído, temporário também"]

Se o mermaid não for renderizado no seu ambiente — sem problema. A ideia é simples: a referência s como que “mantém” o temporário vivo, mas apenas dentro do seu escopo.

Temporários e parâmetros de função: onde fica a fronteira de segurança

Agora um esclarecimento honesto, para evitar o mito perigoso “qualquer const-reference torna tudo imortal”. Quando const T& é um parâmetro de função, o temporário passado na chamada vive o suficiente para que a função execute com segurança, mas isso não permite extrair a referência para fora e usá-la depois.

Na prática, memorize o modelo: “parâmetro const T& é seguro para passar temporários, porque o temporário sobrevive a chamada da função”. Depois (após a saída da função), não há garantias.

Aqui um exemplo seguro:

#include <iostream>
#include <string>

void print_line(const std::string& text) {
    std::cout << text << '\n';
}

int main() {
    print_line("Hi!"); // o literal de string vira um std::string temporário
}

Aqui está tudo certo: a string temporária existe durante a execução de print_line.

Mas se você, dentro de print_line, tentar “guardar a referência em algum lugar global”, rapidamente terá referências pendentes. Vamos tratar disso em aulas sobre retorno de referências e tempo de vida. Hoje é apenas uma introdução, sem mergulhar nas armadilhas mais perigosas.

3. Prática: parâmetros de funções e auto

const T& como parâmetro leitor principal

A situação real mais comum: você escreve uma função que deve ler uma string, um vetor ou seu modelo de dados, mas não deve modificá-lo. Se aceitar o parâmetro por valor, você criará uma cópia. Se aceitar T&, permitirá modificar o objeto (e proibirá passar temporários). Portanto a escolha típica é const T&.

Isso é tão comum que existe uma “regra popular” em C++: “se a função não deve modificar o objeto e o objeto é pesado — aceite const&”. Para int normalmente não faz sentido, mas para std::string e std::vector quase sempre é apropriado.

Exemplo: tamanho da string sem cópia

#include <iostream>
#include <string>

std::size_t len(const std::string& s) {
    return s.size();
}

int main() {
    std::string name = "Alice";
    std::cout << len(name) << '\n';    // 5
    std::cout << len("Hello") << '\n'; // 5
}

Note: len("Hello") funciona exatamente porque o parâmetro é const std::string&. Com std::string& isso não funcionaria.

Exemplo de aplicação: imprimir Task

Suponha que no curso já tenhamos um pequeno app de console “Lista de tarefas” (não porque todos devam fazer um TODO-list, mas porque é simples e não foge).

Temos o modelo da tarefa:

#include <string>

struct Task {
    int id = 0;
    std::string title;
    bool done = false;
};

Agora queremos imprimir a tarefa na tela. Imprimir é ler os campos, mas não modificar a tarefa. Isso é candidato direto a const Task&.

#include <iostream>
#include <string>

struct Task {
    int id = 0;
    std::string title;
    bool done = false;
};

void print_task(const Task& t) {
    std::cout << "#" << t.id << " " << t.title;
    std::cout << (t.done ? " [done]\n" : " [todo]\n");
}

int main() {
    Task a{1, "Read about const references", false};
    print_task(a); // #1 Read about const references [todo]
}

Por que isso é bom estilo?

  • Primeiro, não copiamos Task (e ela contém std::string, cuja cópia pode ser significativamente mais cara que a de um int).
  • Segundo, pela assinatura fica claro: a função não tem permissão para modificar a tarefa. Ou seja, se você acidentalmente escrever dentro algo como t .done = true;, o compilador vai te impedir.

Erro comum em tutoriais: aceitar Task por valor

void print_task(Task t) { // cópia!
    // ...
}

Não é proibido fazer isso, e em exemplos pequenos “funciona”. Mas é como ir ao mercado de caminhonete: possível, mas estranho.

Nuance com auto: como evitar cópias indesejadas

Às vezes você escreve de forma elegante:

auto x = something();

E pensa que x é “uma referência”. Mas auto sem & faz uma cópia se a expressão à direita for uma referência. Não é um erro da linguagem — você explicitamente pediu uma cópia.

No contexto de const T&, um bom hábito é: se você quer “apenas ler, sem cópias”, na maioria dos casos use const auto&.

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{10, 20, 30};

    const auto& first = v[0]; // lemos sem cópia (embora int seja barato)
    std::cout << first << '\n'; // 10
}

Sim, para int isso não é crítico. Mas para std::string e seus modelos — já faz diferença.

4. Erros típicos ao trabalhar com const T&

Erro #1: tentar modificar o objeto via const T& e ficar bravo com o compilador.
O compilador não está sendo maldoso — ele está apenas aplicando seu contrato. Se o parâmetro é const Task& t, você prometeu “não modificar”. Se a lógica exigir modificação, o contrato deve ser outro: Task& (modifico) ou Task* (modifico se o ponteiro não for nullptr), mas isso já é assunto de design de API.

Erro #2: pensar que const T& sempre estende a vida do temporário “para sempre”.
A extensão de tempo de vida funciona em cenários específicos, em particular quando o temporário é ligado a uma variável const T& na inicialização. A regra existe, mas não transforma temporários em imortais — eles ainda têm limites, só que às vezes maiores que “até o fim da linha”.

Erro #3: guardar uma const T& por mais tempo que o objeto referenciado vive.
const não torna a referência segura em termos de tempo de vida. Ela apenas proíbe modificação. Se o objeto for destruído, a referência vira pendente — e aí o problema não é “modificar ou não”, mas “o objeto não existe mais”. Tenha isso em mente especialmente ao guardar referências para elementos de containers ou para variáveis locais de outros escopos.

Erro #4: usar T& em todo lugar por hábito e depois estranhar que temporários não são aceitos.
Se a função não deve modificar o argumento — prefira já escrever const T&. Assim você poderá passar variáveis normais, objetos const e valores temporários. Isso torna a API mais amigável e simples de usar, especialmente para utilitários como impressão, checagem e formatação.

Erro #5: “otimizar” tudo com const&, inclusive tipos pequenos.
Para int, double, char, passar por valor geralmente é mais simples e não piora o desempenho. const& não é um botão mágico que acelera o programa; é uma ferramenta quando a cópia é significativa ou quando você quer aceitar temporários sem overloads. Às vezes equipes usam const& mesmo para tipos pequenos por consistência, mas isso é estilo e acordo do time.

1
Tarefa
C++ SELF, nível 40, lição 0
Bloqueado
Eco da mensagem
Eco da mensagem
1
Tarefa
C++ SELF, nível 40, lição 0
Bloqueado
Caixa sem cópias
Caixa sem cópias
1
Tarefa
C++ SELF, nível 40, lição 0
Bloqueado
Descrição da tarefa
Descrição da tarefa
1
Tarefa
C++ SELF, nível 40, lição 0
Bloqueado
Referência ao slogan
Referência ao slogan
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION