C++ Métodos const
Desde a aula Métodos, você viu que há dois tipos de método: os que só olham o objeto (preco(), mostrar()) e os que mudam o objeto (vender(), mudarPreco()). Até agora, essa diferença só existia na sua cabeça. Nesta aula você vai escrevê-la no código, com a palavra const depois dos parênteses, e ver por que isso não é enfeite: sem ela, o seu código simplesmente não compila em situações muito comuns.
- O que significa
int preco() conste o que o compilador passa a conferir. - Por que um objeto
const(ou uma referênciaconst&) só pode chamar métodosconst. - A ler o erro
passing ‘const Produto’ as ‘this’ argument discards qualifiers. - Por que objetos vão para funções como
const Produto&. - O que é const-correctness e como aplicá-la a uma classe inteira.
- Os detalhes:
constfora da classe, getters que devolvemconst std::string&emutable.
Esta aula usa o que você viu em Constantes, const com Ponteiros e Referências, public e private e this. Os programas foram compilados e executados na máquina fictícia da Ana, na pasta ~/cpp/const, com o g++ 15.2 e o comando padrão g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.
A ideia: "só para consulta"
A Ana deixa no balcão uma pasta com as fichas dos produtos, com uma etiqueta: "só para consulta". O cliente pode folhear, ler preços, conferir o estoque. Não pode riscar nem apagar nada. Agora imagine as instruções do verso das fichas: "ler o preço" pode ser feito numa ficha "só para consulta"; "vender: tire do estoque" não pode.
Em C++, a pasta "só para consulta" é um objeto const (ou uma referência const&). A marca nas instruções é o const depois dos parênteses do método: "este método pode ser usado em fichas só para consulta, porque ele não risca nada". O compilador confere as duas pontas: o método marcado não pode mudar o objeto, e o objeto "só para consulta" só aceita métodos marcados.
1. O problema: a função que só queria ler
Esta é a classe Produto da aula public e private, com getters sem const, e uma função que mostra um produto. A função recebe o produto como const Produto&, como você aprendeu: por referência, para não copiar, e const, porque só vai ler.
#include <iostream>
#include <string>
class Produto {
public:
Produto(std::string nome, int preco) : nome_(nome), preco_(preco) {
}
std::string nome() { return nome_; }
int preco() { return preco_; }
private:
std::string nome_;
int preco_;
};
void mostrar(const Produto& p) {
std::cout << p.nome() << ": R$ " << p.preco() << "\n";
}
int main() {
Produto caneta{"caneta", 3};
mostrar(caneta);
}
$ g++ -std=c++23 -Wall -Wextra problema.cpp -o problema
problema.cpp: In function ‘void mostrar(const Produto&)’:
problema.cpp:18:24: error: passing ‘const Produto’ as ‘this’ argument discards qualifiers [-fpermissive]
18 | std::cout << p.nome() << ": R$ " << p.preco() << "\n";
| ~~~~~~^~
problema.cpp:9:17: note: in call to ‘std::string Produto::nome()’
9 | std::string nome() { return nome_; }
| ^~~~
problema.cpp:18:48: error: passing ‘const Produto’ as ‘this’ argument discards qualifiers [-fpermissive]
18 | std::cout << p.nome() << ": R$ " << p.preco() << "\n";
| ~~~~~~~^~
problema.cpp:10:9: note: in call to ‘int Produto::preco()’
10 | int preco() { return preco_; }
| ^~~~~
A função só lê, os getters só leem, e mesmo assim não compila. Esta é a mensagem mais famosa desta aula; leia por partes:
| Trecho | Significado |
|---|---|
passing ‘const Produto’ | "Passando um const Produto…": o p é uma referência const, um produto "só para consulta". |
as ‘this’ argument | "…como o argumento this…": lembre da aula this. Chamar p.nome() passa o endereço de p para o parâmetro escondido this do método. |
discards qualifiers | "…descarta qualificadores": o this de nome() é um Produto* comum, que permite mudar o objeto. Para passar o produto "só para consulta" ali, o compilador teria de jogar fora o const (o "qualificador"). Ele se recusa. |
[-fpermissive] | A opção que faria o g++ aceitar mesmo assim. Nunca use: ela desliga a proteção em vez de consertar o código. |
note: in call to ‘std::string Produto::nome()’ | Aponta o método culpado: repare que a assinatura dele não tem const no fim. |
Em resumo: o compilador não lê o corpo de nome() para ver se ele muda algo. Ele só confia no que está declarado. Sem const na declaração, para ele, nome() pode mudar o produto, e um produto "só para consulta" não pode correr esse risco.
No editor do guia, o Clang explica com outras palavras, até mais diretas: 'this' argument to member function 'nome' has type 'const Produto', but function is not marked const ("o argumento this do método nome é const Produto, mas o método não está marcado como const").
2. A solução: const depois dos parênteses
#include <iostream>
#include <string>
class Produto {
public:
Produto(std::string nome, int preco) : nome_(nome), preco_(preco) {
}
std::string nome() const { return nome_; }
int preco() const { return preco_; }
void mudarPreco(int novo) {
if (novo >= 0) {
preco_ = novo;
}
}
private:
std::string nome_;
int preco_;
};
void mostrar(const Produto& p) {
std::cout << p.nome() << ": R$ " << p.preco() << "\n";
}
int main() {
Produto caneta{"caneta", 3};
mostrar(caneta);
caneta.mudarPreco(4);
mostrar(caneta);
const Produto brinde{"chaveiro", 0};
std::cout << "Brinde: " << brinde.nome() << "\n";
}
$ g++ -std=c++23 -Wall -Wextra certo.cpp -o certo $ ./certo caneta: R$ 3 caneta: R$ 4 Brinde: chaveiro
- int preco() const
- O
constfica depois dos parênteses (e antes das chaves). Ele promete: "este método não muda o objeto". Lê-se "precoé um método const". - { return preco_; }
- O corpo é o mesmo de antes. O que mudou é que agora o compilador confere a promessa: se o corpo tentar mudar um atributo, dá erro (seção 3).
- void mudarPreco(int novo)
- Sem
const: este método muda o objeto, e está certo não marcá-lo. - void mostrar(const Produto& p)
- Agora a função compila:
p.nome()ep.preco()são métodosconst, permitidos em produtos "só para consulta". - const Produto brinde{"chaveiro", 0};
- Um objeto
const: nasce e nunca mais muda. Pode chamarnome()epreco(); não pode chamarmudarPreco().
Por dentro, o const muda o tipo do this: num método comum, this é Produto*; num método const, é const Produto* ("ponteiro para produto que não pode ser mudado"). É por isso que a mensagem fala do argumento this.
Método const (preco()) | Método comum (mudarPreco()) | |
|---|---|---|
Objeto comum (caneta) | pode | pode |
Objeto const (brinde) | pode | erro |
Referência const Produto& | pode | erro |
Ponteiro const Produto* | pode | erro |
Missão: rode e veja o erro do Clang. Depois conserte só a classe, acrescentando const aos dois getters, sem mexer na função mostrar. A saída deve ser caneta: R$ 3.
#include <iostream>
#include <string>
class Produto {
public:
Produto(std::string nome, int preco) : nome_(nome), preco_(preco) {
}
std::string nome() { return nome_; }
int preco() { return preco_; }
private:
std::string nome_;
int preco_;
};
void mostrar(const Produto& p) {
std::cout << p.nome() << ": R$ " << p.preco() << "\n";
}
int main() {
Produto caneta{"caneta", 3};
mostrar(caneta);
}
#include <iostream>
#include <string>
class Produto {
public:
Produto(std::string nome, int preco) : nome_(nome), preco_(preco) {
}
std::string nome() const { return nome_; }
int preco() const { return preco_; }
private:
std::string nome_;
int preco_;
};
void mostrar(const Produto& p) {
std::cout << p.nome() << ": R$ " << p.preco() << "\n";
}
int main() {
Produto caneta{"caneta", 3};
mostrar(caneta);
}
3. O compilador confere a promessa
Marcar um método como const não é só uma etiqueta. Se o corpo tentar mudar um atributo, o compilador recusa:
#include <iostream>
#include <string>
class Produto {
public:
Produto(std::string nome, int estoque) : nome_(nome), estoque_(estoque) {
}
int estoque() const {
estoque_ = estoque_ - 1;
return estoque_;
}
private:
std::string nome_;
int estoque_;
};
int main() {
Produto caneta{"caneta", 50};
std::cout << caneta.estoque() << "\n";
}
$ g++ -std=c++23 -Wall -Wextra mexer.cpp -o mexer
mexer.cpp: In member function ‘int Produto::estoque() const’:
mexer.cpp:10:18: error: assignment of member ‘Produto::estoque_’ in read-only object
10 | estoque_ = estoque_ - 1;
| ~~~~~~~~~^~~~~~~~~~~~~~
"Atribuição ao membro Produto::estoque_ num objeto só de leitura". Dentro de um método const, o objeto inteiro é tratado como só de leitura. Um getter que, por engano, mudasse o estoque seria pego aqui, na compilação, e não semanas depois por uma conta errada no caixa. No Clang: cannot assign to non-static data member within const member function 'estoque'.
A regra vale também para chamar outros métodos: dentro de um método const, só se podem chamar outros métodos const do mesmo objeto. Veja em Quando dá errado.
4. Objetos vão para funções como const&
A seção 1 mostrou que o problema aparece justamente quando você faz a coisa certa ao passar objetos para funções. Recordando as três formas (aula Por Valor × Por Referência):
| Parâmetro | Copia o objeto? | A função pode mudar o original? | Quando usar |
|---|---|---|---|
Produto p | Sim: copia nome, preço, estoque… | Não (muda a cópia) | Quando a função precisa de uma cópia própria para mexer. |
Produto& p | Não | Sim | Quando a função deve mudar o objeto (void reajustar(Produto& p)). |
const Produto& p | Não | Não | O padrão para objetos que a função só lê. |
Copiar um int é instantâneo, mas um objeto pode carregar textos longos, vetores com milhares de itens… Passar por const& evita a cópia e ainda garante, a quem chama, que o objeto volta intacto. Por isso essa é a forma mais comum de receber objetos em C++, e por isso todo método que só lê precisa ser const: senão, ninguém consegue usá-lo pela const&.
Um exemplo maior, com uma vitrine inteira passada por const& para duas funções:
#include <iostream>
#include <string>
#include <vector>
class Produto {
public:
Produto(std::string nome, int preco, int estoque)
: nome_(nome), preco_(preco), estoque_(estoque) {
}
const std::string& nome() const { return nome_; }
int preco() const { return preco_; }
int estoque() const { return estoque_; }
int valorEmEstoque() const { return preco_ * estoque_; }
void vender(int quantidade) {
if (quantidade > 0 && quantidade <= estoque_) {
estoque_ -= quantidade;
}
}
private:
std::string nome_;
int preco_;
int estoque_;
};
int valorTotal(const std::vector<Produto>& vitrine) {
int total = 0;
for (const Produto& p : vitrine) {
total += p.valorEmEstoque();
}
return total;
}
void relatorio(const std::vector<Produto>& vitrine) {
for (const Produto& p : vitrine) {
std::cout << p.nome() << ": " << p.estoque() << " un. x R$ " << p.preco() << "\n";
}
std::cout << "Total: R$ " << valorTotal(vitrine) << "\n";
}
int main() {
std::vector<Produto> vitrine{{"caneta", 3, 50}, {"caderno", 12, 8}};
vitrine[0].vender(10);
relatorio(vitrine);
}
$ g++ -std=c++23 -Wall -Wextra lista.cpp -o lista $ ./lista caneta: 40 un. x R$ 3 caderno: 8 un. x R$ 12 Total: R$ 216
- const std::string& nome() const
- Dois
constcom papéis diferentes. O do fim: "o método não muda o objeto". O do começo: o método devolve uma referência só de leitura aonome_interno, sem copiar o texto. Quem chama pode ler, não pode mudar. - int valorEmEstoque() const
- Faz uma conta com os atributos, sem mudá-los: é
const. - const std::vector<Produto>& vitrine
- O vetor inteiro chega "só para consulta". Cada elemento também fica só de leitura, por isso o
forusaconst Produto& p. - valorTotal(vitrine)
- Uma função
const&pode passar o objeto adiante para outra funçãoconst&, sem cópia nenhuma.
5. Const-correctness: marcar tudo direito
Const-correctness ("correção de const") é o hábito de marcar const em tudo o que não muda: métodos que só leem, parâmetros que só são lidos, variáveis que não mudam depois de criadas. Os ganhos:
- O compilador pega erros: um getter que muda o objeto sem querer, uma função "de relatório" que vende um produto por engano.
- O código se explica: quem lê
int preco() constsabe, sem abrir o corpo, que chamarpreco()é seguro. - A classe funciona em todo lugar: com objetos
const, comconst&, dentro de containers passados porconst&.
Para decidir, pergunte de cada método: "depois de chamar este método, o objeto está igual?"
Método do Produto | O objeto fica igual? | const? |
|---|---|---|
nome(), preco(), estoque() | Sim | Sim |
valorEmEstoque(), temEstoque(int) | Sim (só calcula) | Sim |
mostrar() | Sim (só escreve na tela) | Sim |
maisCaroQue(const Produto&) | Sim (só compara) | Sim |
vender(int), repor(int), mudarPreco(int) | Não | Não |
| Construtores | — | Nunca (construtor não leva const) |
const. Ao escrever um método novo, coloque const de saída. Se o compilador reclamar que o método muda algo, aí você decide: ou o método realmente muda o objeto (tire o const), ou há um engano no corpo (conserte o corpo). Acrescentar const depois, numa classe grande, é bem mais trabalhoso, porque um método não-const "contamina" quem o chama.A exceção controlada: mutable
Às vezes um atributo é só um detalhe interno que não faz parte do "estado" que importa, como um contador de quantas vezes o preço foi consultado. A palavra mutable ("mutável") deixa esse atributo mudar até dentro de métodos const:
class Produto {
public:
Produto(std::string nome, int preco) : nome_(nome), preco_(preco) {
}
int preco() const {
++consultas_;
return preco_;
}
int consultas() const { return consultas_; }
private:
std::string nome_;
int preco_;
mutable int consultas_ = 0;
};
int main() {
const Produto caneta{"caneta", 3};
caneta.preco();
caneta.preco();
caneta.preco();
std::cout << "Preço consultado " << caneta.consultas() << " vezes\n";
}
$ g++ -std=c++23 -Wall -Wextra mutable.cpp -o mutable $ ./mutable Preço consultado 3 vezes
Mesmo com a caneta const, o contador subiu. Use mutable com muita parcimônia, só para detalhes que quem usa a classe não percebe (contadores, memória de cálculos já feitos). Se você está usando mutable para driblar o const num dado de verdade, o método não deveria ser const.
Quando dá errado
const
int main() {
const Produto brinde{"chaveiro", 0};
std::cout << brinde.preco() << "\n";
brinde.mudarPreco(5);
}
$ g++ -std=c++23 -Wall -Wextra brinde.cpp -o brinde
brinde.cpp: In function ‘int main()’:
brinde.cpp:26:22: error: passing ‘const Produto’ as ‘this’ argument discards qualifiers [-fpermissive]
26 | brinde.mudarPreco(5);
| ~~~~~~~~~~~~~~~~~^~~
brinde.cpp:12:10: note: in call to ‘void Produto::mudarPreco(int)’
12 | void mudarPreco(int novo) {
| ^~~~~~~~~~
O que aconteceu: a classe é a de certo.cpp, já com os getters const. O brinde.preco() passou; o mudarPreco não, porque muda o objeto e o brinde é const. Aqui o erro está certo: o problema é a linha do main, não a classe.
Como resolver: descubra qual dos dois está errado. Se o brinde precisa mudar de preço, ele não deveria ser const. Se o método não muda nada de verdade, marque-o const. Nunca use -fpermissive nem const_cast para calar o erro.
const chamando método não-const
class Produto {
public:
Produto(std::string nome, int preco, int estoque)
: nome_(nome), preco_(preco), estoque_(estoque) {
}
int valorEmEstoque() {
return preco_ * estoque_;
}
void mostrar() const {
std::cout << nome_ << ": R$ " << valorEmEstoque() << " em estoque\n";
}
private:
std::string nome_;
int preco_;
int estoque_;
};
$ g++ -std=c++23 -Wall -Wextra chamaoutro.cpp -o chamaoutro
chamaoutro.cpp: In member function ‘void Produto::mostrar() const’:
chamaoutro.cpp:15:56: error: passing ‘const Produto’ as ‘this’ argument discards qualifiers [-fpermissive]
15 | std::cout << nome_ << ": R$ " << valorEmEstoque() << " em estoque\n";
| ~~~~~~~~~~~~~~^~
chamaoutro.cpp:10:9: note: in call to ‘int Produto::valorEmEstoque()’
10 | int valorEmEstoque() {
| ^~~~~~~~~~~~~~
O que aconteceu: dentro de mostrar() const, o this é const Produto*. Chamar valorEmEstoque() passa esse this adiante, e valorEmEstoque não prometeu não mudar nada. É o mesmo erro, agora dentro da classe (repare em In member function ‘void Produto::mostrar() const’).
Como resolver: valorEmEstoque só calcula, então merece const: int valorEmEstoque() const. Esse "efeito cascata" é normal: marcar um método const costuma revelar outros que também deveriam ser.
const na definição fora da classe
#include <iostream>
#include <string>
class Produto {
public:
Produto(int preco) : preco_(preco) {
}
int preco() const;
private:
int preco_;
};
int Produto::preco() {
return preco_;
}
int main() {
Produto caneta{3};
std::cout << caneta.preco() << "\n";
}
$ g++ -std=c++23 -Wall -Wextra fora.cpp -o fora
fora.cpp:15:5: error: no declaration matches ‘int Produto::preco()’
15 | int Produto::preco() {
| ^~~~~~~
fora.cpp:9:9: note: candidate is: ‘int Produto::preco() const’
9 | int preco() const;
| ^~~~~
fora.cpp:4:7: note: ‘class Produto’ defined here
4 | class Produto {
| ^~~~~~~
O que aconteceu: o const faz parte da assinatura do método. preco() e preco() const são, para o compilador, métodos diferentes (dá até para ter os dois). A definição sem const não corresponde a nenhuma declaração, e o g++ mostra o candidato que quase serviu.
Como resolver: repita o const na definição: int Produto::preco() const { … }.
const devolvendo referência comum
class Produto {
public:
Produto(std::string nome) : nome_(nome) {
}
std::string& nome() const { return nome_; }
private:
std::string nome_;
};
$ g++ -std=c++23 -Wall -Wextra referencia.cpp -o referencia
referencia.cpp: In member function ‘std::string& Produto::nome() const’:
referencia.cpp:9:40: error: binding reference of type ‘std::string&’ {aka ‘std::__cxx11::basic_string<char>&’} to ‘const std::string’ {aka ‘const std::__cxx11::basic_string<char>’} discards qualifiers
9 | std::string& nome() const { return nome_; }
| ^~~~~
O que aconteceu: dentro do método const, nome_ é const std::string. Devolver um std::string& comum daria a quem chama uma porta para mudar o nome por fora (caneta.nome() = "x";), furando a promessa. O compilador não deixa: "ligar uma referência std::string& a um const std::string descarta qualificadores". (O {aka …} é o nome completo, "também conhecido como", do tipo std::string por dentro da biblioteca.)
Como resolver: devolva const std::string& (sem cópia, só leitura) ou std::string (uma cópia).
Referência rápida
| Escrita | O que faz |
|---|---|
int preco() const { … } | Método que promete não mudar o objeto; o compilador confere. |
int Produto::preco() const { … } | Definição fora da classe: o const se repete. |
const Produto brinde{…}; | Objeto que nunca muda: só chama métodos const. |
void f(const Produto& p) | Recebe sem copiar e sem poder mudar: só chama métodos const. |
const std::string& nome() const | Getter que devolve o texto interno sem cópia, só para leitura. |
mutable int consultas_; | Atributo que pode mudar até em métodos const (use pouco). |
passing ‘const X’ as ‘this’ argument discards qualifiers | Erro: chamou um método não-const num objeto/referência const. |
assignment of member … in read-only object | Erro: um método const tentou mudar um atributo. |
Exercícios
1. Onde vai o const
Complete o getter para que ele possa ser chamado num const Produto&:
2. O parâmetro
Complete o parâmetro da função que só lê um cliente, sem copiar e sem poder mudar:
3. Leve e só de leitura
Complete o tipo de retorno do getter do nome, sem cópia e sem deixar mudar:
4. Qual destes métodos não deve ser const?
5. O g++ diz passing ‘const Produto’ as ‘this’ argument discards qualifiers numa linha p.mostrar();, dentro de void f(const Produto& p). O que fazer, se mostrar só escreve na tela?
6. Dentro de um método const de Produto, qual é o tipo de this?
Missão final: a ficha do cliente VIP
Missão: o programa não compila. Acrescente const só nos métodos que não mudam o cliente (o ganhar muda, então fica sem). Não mexa no main nem em mostrar. A saída deve ser:
Bia: 15 pontos VIP Caio: 100 pontos
#include <iostream>
#include <string>
class Cliente {
public:
Cliente(std::string nome, int pontos) : nome_(nome), pontos_(pontos) {
}
const std::string& nome() { return nome_; }
int pontos() { return pontos_; }
void ganhar(int quantidade) {
if (quantidade > 0) {
pontos_ += quantidade;
}
}
private:
std::string nome_;
int pontos_;
};
void mostrar(const Cliente& c) {
std::cout << c.nome() << ": " << c.pontos() << " pontos\n";
}
int main() {
Cliente bia{"Bia", 10};
bia.ganhar(5);
mostrar(bia);
const Cliente vip{"Caio", 100};
std::cout << "VIP ";
mostrar(vip);
}
#include <iostream>
#include <string>
class Cliente {
public:
Cliente(std::string nome, int pontos) : nome_(nome), pontos_(pontos) {
}
const std::string& nome() const { return nome_; }
int pontos() const { return pontos_; }
void ganhar(int quantidade) {
if (quantidade > 0) {
pontos_ += quantidade;
}
}
private:
std::string nome_;
int pontos_;
};
void mostrar(const Cliente& c) {
std::cout << c.nome() << ": " << c.pontos() << " pontos\n";
}
int main() {
Cliente bia{"Bia", 10};
bia.ganhar(5);
mostrar(bia);
const Cliente vip{"Caio", 100};
std::cout << "VIP ";
mostrar(vip);
}
Resumo
int preco() const: oconstdepois dos parênteses promete que o método não muda o objeto, e o compilador confere (assignment of member … in read-only object).- Objetos
const, referênciasconst&e ponteirosconst*só chamam métodosconst. Senão:passing ‘const Produto’ as ‘this’ argument discards qualifiers. - O compilador confia na declaração, não no corpo: um getter sem
consté tratado como se mudasse o objeto. - Objetos vão para funções como
const Produto&quando só são lidos: sem cópia e sem risco. Por isso, todo método que só lê deve serconst. - Const-correctness: marque
constem tudo o que não muda. Comece peloconste tire só se precisar. - O
constse repete na definição fora da classe. Gettersconstdevolvem cópia ouconst&.mutableé a exceção, para detalhes internos. - Próxima aula: dados e métodos que pertencem à classe, e não a cada objeto, em Membros static.