Uso de const con punteros y la semántica de inmutabilidad

La interacción entre el calificador const y los punteros define qué partes de una operación son inmutables: el dato al que apunta el puntero o la dirección de memoria que el puntero contiene. Esta distinción es fundamental para la seguridad de tipos y la semántica de una API. Un puntero es, en esencia, una variable que almacena una dirección; por tanto, como cualquier variable, puede ser constante o no.

Cuando combinas const con punteros, te encuentras con cuatro escenarios posibles. Si el const está a la izquierda del asterisco (o es el tipo mismo, como en const T*), el contenido es constante (no puedes cambiar lo que hay en esa dirección). Si el const está a la derecha del asterisco (como en T* const), el puntero es constante (no puedes cambiar la dirección almacenada). La combinación de ambos te permite ser extremadamente preciso con las promesas de inmutabilidad que le haces al compilador.

¿Cuándo deberías usar cada uno? Usarás const T* (puntero a dato constante) principalmente en parámetros de función para garantizar que la función no altere el objeto pasado. Usarás T* const (puntero constante) cuando tengas un puntero que debe ser un ancla fija hacia un recurso. Usarás const T* const para máxima restricción. Si aplicas mal estas reglas, lo más común es que el compilador te detenga con un error, pero si intentas “engañar” al sistema con const_cast sobre un objeto originalmente constante, entrarás en el terreno del comportamiento indefinido (UB).

#include <iostream>
#include <vector>

// Estructura de datos simple
struct Sensor {
    int reading;
    // El uso de 'mutable' permite modificar este miembro incluso si 
    // el objeto Sensor es tratado como 'const'. Útil para caché o contadores.
    mutable int access_count = 0; 
};

class TelemetrySystem {
public:
    // Recibe un puntero a dato constante. 
    // Promesa: No modificarás el valor de 'reading' del sensor.
    void log_sensor(const Sensor* s) const {
        if (s) {
            std::cout << "Sensor reading: " << s->reading << "\n";
            // s->reading = 0; // Error de compilación: el dato es constante.
            s->access_count++; // OK: 'access_count' es 'mutable'.
        }
    }

    // Devuelve un puntero constante a un dato constante.
    // Protege totalmente la integridad del objeto interno.
    const Sensor* const get_locked_sensor(const Sensor& s) const {
        return &s;
    }
};

int main() {
    Sensor s{100};
    TelemetrySystem system;

    // 1. Puntero mutable a dato mutable (T* p)
    // Podemos cambiar la dirección y cambiar el valor.
    Sensor* p1 = &s;
    p1->reading = 110; 
    p1 = &s; 

    // 2. Puntero mutable a dato constante (const T* p)
    // Podemos mover el puntero, pero no cambiar el valor en la dirección.
    const Sensor* p2 = &s;
    // p2->reading = 120; // Error: el contenido es constante.
    p2 = &s;            // OK: el puntero es mutable.

    // 3. Puntero constante a dato mutable (T* const p)
    // Podemos cambiar el valor, pero el puntero siempre apunta a la misma dirección.
    Sensor* const p3 = &s;
    p3->reading = 130; // OK: el contenido es mutable.
    // p3 = &s;       // Error: el puntero es constante.

    // 4. Puntero constante a dato constante (const T* const p)
    // Ni la dirección ni el contenido pueden cambiar.
    const Sensor* const p4 = &s;
    // p4->reading = 140; // Error
    // p4 = &s;          // Error

    system.log_sensor(&s);

    return 0;
}

Desglose del código

En el ejemplo, hemos definido una estructura Sensor con un miembro reading y un miembro mutable access_count. La palabra clave mutable es crucial aquí: permite que, aunque un método sea const (como log_sensor), pueda modificar ese miembro específico. Esto es vital cuando necesitas mantener estados internos como contadores de acceso o semáforos sin comprometer la semántica de inmutabilidad del objeto principal.

Al analizar los punteros:
p1 es un Sensor*. Es el caso más permisivo: puedes cambiar s.reading a través de él y puedes reasignar p1 para que apunte a otra variable Sensor.
p2 es un const Sensor*. El compilador ha marcado la memoria a la que apunta como de “solo lectura” a través de este puntero. Sin embargo, p2 mismo es una variable, por lo que p2 = &s es una operación válida.
p3 es un Sensor* const. Aquí, el valor de p3 (la dirección de memoria) se ha grabado en el binario como constante. Puedes modificar p3->reading, pero no puedes hacer que p3 apunte a otra variable.
p4 es un const Sensor* const. Es la restricción total. El compilador bloqueará cualquier intento de modificar el dato o la dirección.

En la función log_sensor, observamos la propagación de const. Al recibir un const Sensor* s, el método también debe ser const (indicado por const al final de la firma) para poder operar con el puntero sin comprometer el estado del objeto TelemetrySystem.

El error frecuente

Un error clásico ocurre al intentar usar const_cast para “saltarse” la protección de const. Si bien const_cast [C++11] es una herramienta legítima para eliminar la cualidad de const en un puntero o referencia, solo es seguro si el objeto original no fue declarado como const.

// ERROR: Comportamiento indefinido (UB)
const int valor_real = 42; 
int* hack = const_cast<int*>(&valor_real);
*hack = 100; // UB: Estás intentando modificar un objeto que reside en memoria de solo lectura.

Si compilas esto con -fsanitize=undefined, el detector te avisará. Si el objeto fue declarado const, el compilador puede haberlo colocado en una sección de memoria de solo lectura del binario, y el intento de escritura provocará un error de segmentación (segmentation fault) o un comportamiento impredecible. Si el objeto es mutable pero lo estás tratando como const (por ejemplo, mediante una referencia), el const_cast es una práctica aceptable pero generalmente indica un mal diseño de la API.

27

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio