Dominando o Qualificador `const` em C++

Em C++, o qualificador const é uma ferramenta poderosa para expressar a intenção de que um valor não deve ser alterado. Ele é uma promessa ao compilador que, se mantida, pode levar a um código mais seguro, otimizado e legível. Este artigo explora o uso do const em diferentes contextos, desde variáveis simples até classes e ponteiros complexos.

Variáveis Constantes

Quando declaramos uma variável com const, estamos garantindo que seu valor não poderá ser modificado após a inicialização. Tentar reatribuir um valor a uma variável const resultará em um erro de compilação.

#include <iostream>

int main() {
    // Variável comum, pode ser alterada
    int contador = 10;
    std::cout << "Contador inicial: " << contador << std::endl;
    contador = 20; // OK
    std::cout << "Contador alterado: " << contador << std::endl;

    // Variável constante, não pode ser alterada
    const double PI_APROXIMADO = 3.14;
    // PI_APROXIMADO = 3.14159; // ERRO: atribuição a uma variável somente leitura
    std::cout << "PI Aproximado: " << PI_APROXIMADO << std::endl;

    return 0;
}

const com Ponteiros

O uso de const com ponteiros pode ser um pouco mais complexo, pois ele pode qualificar o dado para o qual o ponteiro aponta, o próprio ponteiro, ou ambos.

Ponteiro para Dado Constante (const Tipo\* ou Tipo const\*)

Neste caso, o ponteiro aponta para um dado que não pode ser modificado através desse ponteiro. O ponteiro em si pode ser reatribuído para apontar para outro local de memória.

#include <iostream>

int main() {
    int valor_original = 100;
    const int VALOR_FIXO = 50;

    const int* ponteiro_para_const = &valor_original; // Pode apontar para valor_original
    std::cout << "Valor apontado (original): " << *ponteiro_para_const << std::endl;

    // *ponteiro_para_const = 120; // ERRO: não pode modificar o dado apontado
    ponteiro_para_const = &VALOR_FIXO; // OK: ponteiro pode ser reatribuído
    std::cout << "Valor apontado (fixo): " << *ponteiro_para_const << std::endl;

    return 0;
}

Ponteiro Constante para Dado (Tipo\* const)

Aqui, o ponteiro não pode ser reatribuído para apontar para outro local de memória após a inicialização, mas o dado para o qual ele aponta pode ser modificado através desse ponteiro.

#include <iostream>

int main() {
    int vida_jogador = 100;
    int pontuacao = 0;

    int* const ponteiro_constante = &vida_jogador; // Ponteiro é constante, deve ser inicializado
    std::cout << "Vida inicial: " << *ponteiro_constante << std::endl;

    *ponteiro_constante = 75; // OK: pode modificar o dado apontado
    std::cout << "Vida após dano: " << *ponteiro_constante << std::endl;

    // ponteiro_constante = &pontuacao; // ERRO: não pode reatribuir o ponteiro
    
    return 0;
}

Ponteiro Constante para Dado Constante (const Tipo\* const)

Nesta combinação, nem o dado apontado nem o próprio ponteiro podem ser modificados.

#include <iostream>

int main() {
    const int NUMERO_MAGICO = 7;
    
    const int* const ponteiro_totalmente_const = &NUMERO_MAGICO; // Ambos são constantes
    std::cout << "Número Mágico: " << *ponteiro_totalmente_const << std::endl;

    // *ponteiro_totalmente_const = 8; // ERRO: não pode modificar o dado
    // ponteiro_totalmente_const = nullptr; // ERRO: não pode reatribuir o ponteiro

    return 0;
}

Removendo a Constância com const\_cast

Embora o const seja uma promessa, C++ fornece o operador const\_cast para remover a qualificação const de um ponteiro ou referência. Isso é geralmente necessário ao interagir com APIs de C legadas ou em cenários muito específicos, mas deve ser usado com extrema cautela, pois pode levar a comportamento indefinido se usado para modificar um objeto que foi originalmente declraado como const.

#include <iostream>

void ImprimirEModificar(int* val) {
    std::cout << "Valor original (dentro da função): " << *val << std::endl;
    *val += 10;
    std::cout << "Valor modificado (dentro da função): " << *val << std::endl;
}

int main() {
    const int configuracao_padrao = 100;
    std::cout << "Configuração padrão antes: " << configuracao_padrao << std::endl;

    // const_cast permite remover a constância de um ponteiro
    // Mas modificar 'configuracao_padrao' através do ponteiro resultante é comportamento indefinido
    // se 'configuracao_padrao' foi definida como const desde o início.
    // É mais seguro usar const_cast com dados não-const que foram passados como const.
    int* ptr_modificavel = const_cast<int>(&configuracao_padrao);
    
    // Isto é comportamento indefinido se 'configuracao_padrao' é realmente const.
    // Demonstrando apenas a capacidade do const_cast, não seu uso seguro.
    *ptr_modificavel = 150; 
    std::cout << "Configuração padrão após (via const_cast): " << configuracao_padrao << std::endl;

    // Exemplo de uso mais seguro:
    int var_nao_const = 200;
    const int* ptr_para_var_nao_const = &var_nao_const;
    // Podemos remover const para passar para uma função não-const
    ImprimirEModificar(const_cast<int>(ptr_para_var_nao_const));
    std::cout << "Variável não-const após função: " << var_nao_const << std::endl;

    return 0;
}
</int></int>

const em Métodos de Classes

Ao aplicar const a um método de classe, estamos declarando que esse método não modificará o estado do objeto (ou seja, seus membros de dados não-mutable). Isso permite que o método seja chamado em objetos const.

#include <iostream>
#include <string>

class Produto {
private:
    std::string nome;
    double preco;
    int quantidade;

public:
    Produto(const std::string& n, double p, int q) 
        : nome(n), preco(p), quantidade(q) {}

    // Método const: não modifica o estado do objeto
    std::string GetNome() const {
        // nome = "Novo Nome"; // ERRO: não pode modificar membros em um método const
        return nome;
    }

    double GetPreco() const {
        return preco;
    }

    // Método não-const: pode modificar o estado do objeto
    void AjustarQuantidade(int delta) {
        quantidade += delta;
    }

    int GetQuantidade() const {
        return quantidade;
    }
};

void ExibirDetalhes(const Produto& p) { // Recebe uma referência const
    std::cout << "Produto: " << p.GetNome() 
              << ", Preço: R$" << p.GetPreco() 
              << ", Qtd: " << p.GetQuantidade() << std::endl;
    // p.AjustarQuantidade(1); // ERRO: não pode chamar método não-const em objeto const
}

int main() {
    Produto item1("Laptop", 1500.0, 5);
    const Produto item2("Monitor", 800.0, 3); // Objeto const

    ExibirDetalhes(item1);
    ExibirDetalhes(item2);

    item1.AjustarQuantidade(2); // OK: item1 não é const
    // item2.AjustarQuantidade(1); // ERRO: item2 é const
    
    std::cout << "Nova quantidade Laptop: " << item1.GetQuantidade() << std::endl;

    return 0;
}

Sobrecarga de Métodos const e Não-const

É possível ter duas versões de um método, uma const e outra não-const. O compilador escolherá a versão apropriada dependendo se o objeto que chama o método é const ou não.

#include <iostream>

class Dados {
private:
    int valor;
public:
    Dados(int v) : valor(v) {}

    // Versão não-const: retorna uma referência modificável
    int& GetValor() {
        std::cout << "Chamado GetValor() não-const" << std::endl;
        return valor;
    }

    // Versão const: retorna uma referência const, não modificável
    const int& GetValor() const {
        std::cout << "Chamado GetValor() const" << std::endl;
        return valor;
    }
};

int main() {
    Dados d_mutavel(10);
    const Dados d_imutavel(20);

    d_mutavel.GetValor() = 15; // Chama a versão não-const
    // d_imutavel.GetValor() = 25; // ERRO: Chama a versão const, referência const

    std::cout << "Valor mutável: " << d_mutavel.GetValor() << std::endl;
    std::cout << "Valor imutável: " << d_imutavel.GetValor() << std::endl;

    return 0;
}

O Qualificador mutable

O mutable é um qualificador de membro de classe que permite que um membro de dados seja modificado mesmo dentro de um método const. Isso é útil para otimizações (como caching) onde a modificação de um membro não altera o estado lógico ou observável do objeto.

#include <iostream>
#include <string>

class ElementoGrafico {
private:
    std::string nome;
    int posicao_x;
    int posicao_y;
    mutable int cache_calculado_hash; // Pode ser modificado em métodos const
    mutable bool cache_valido;

    void RecalcularHash() const {
        // Simula um cálculo caro
        cache_calculado_hash = posicao_x * 31 + posicao_y; // OK por causa de 'mutable'
        cache_valido = true;
        std::cout << "Hash recalculado." << std::endl;
    }

public:
    ElementoGrafico(const std::string& n, int x, int y) 
        : nome(n), posicao_x(x), posicao_y(y), cache_calculado_hash(0), cache_valido(false) {}

    // Método const que pode modificar membros 'mutable'
    int ObterHash() const {
        if (!cache_valido) {
            RecalcularHash();
        }
        return cache_calculado_hash;
    }

    void Mover(int novo_x, int novo_y) {
        posicao_x = novo_x;
        posicao_y = novo_y;
        cache_valido = false; // Invalida o cache ao modificar o estado
    }
};

int main() {
    ElementoGrafico circulo("Círculo", 10, 20);
    const ElementoGrafico quadrado("Quadrado", 5, 5); // Objeto const

    std::cout << "Hash do Círculo: " << circulo.ObterHash() << std::endl; // Recalcula
    std::cout << "Hash do Círculo (novamente): " << circulo.ObterHash() << std::endl; // Usa cache

    std::cout << "Hash do Quadrado: " << quadrado.ObterHash() << std::endl; // Recalcula (mesmo sendo const)
    quadrado.ObterHash(); // Usa cache

    circulo.Mover(30, 40); // Modifica o objeto, invalidando o cache
    std::cout << "Hash do Círculo após mover: " << circulo.ObterHash() << std::endl; // Recalcula

    return 0;
}

Tags: C++ const ponteiros classes Imutabilidade

Publicado em 7-1 16:12