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.
GO TO FULL VERSION