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.

Você vai aprender
  • O que significa int preco() const e o que o compilador passa a conferir.
  • Por que um objeto const (ou uma referência const&) só pode chamar métodos const.
  • 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: const fora da classe, getters que devolvem const std::string& e mutable.

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:

TrechoSignificado
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 const fica 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() e p.preco() são métodos const, permitidos em produtos "só para consulta".
const Produto brinde{"chaveiro", 0};
Um objeto const: nasce e nunca mais muda. Pode chamar nome() e preco(); não pode chamar mudarPreco().

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)podepode
Objeto const (brinde)podeerro
Referência const Produto&podeerro
Ponteiro const Produto*podeerro

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âmetroCopia o objeto?A função pode mudar o original?Quando usar
Produto pSim: 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& pNãoSimQuando a função deve mudar o objeto (void reajustar(Produto& p)).
const Produto& pNãoNãoO 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 const com 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 ao nome_ 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 for usa const Produto& p.
valorTotal(vitrine)
Uma função const& pode passar o objeto adiante para outra função const&, 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() const sabe, sem abrir o corpo, que chamar preco() é seguro.
  • A classe funciona em todo lugar: com objetos const, com const&, dentro de containers passados por const&.

Para decidir, pergunte de cada método: "depois de chamar este método, o objeto está igual?"

Método do ProdutoO objeto fica igual?const?
nome(), preco(), estoque()SimSim
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ãoNão
Construtores—Nunca (construtor não leva const)
Comece pelo 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

Chamar um método que muda o objeto num objeto 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.

Método 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.

Esquecer o 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 { … }.

Getter 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

EscritaO 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() constGetter 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 qualifiersErro: chamou um método não-const num objeto/referência const.
assignment of member … in read-only objectErro: 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&:

int estoque() { return estoque_; }

2. O parâmetro

Complete o parâmetro da função que só lê um cliente, sem copiar e sem poder mudar:

void imprimir( c)

3. Leve e só de leitura

Complete o tipo de retorno do getter do nome, sem cópia e sem deixar mudar:

nome() const { return nome_; }

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: o const depois 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ências const& e ponteiros const* só chamam métodos const. 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 ser const.
  • Const-correctness: marque const em tudo o que não muda. Comece pelo const e tire só se precisar.
  • O const se repete na definição fora da classe. Getters const devolvem cópia ou const&. 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.