Const-correctness

Español


Something I’ve learned the first day of my current job. I was supposed to know about it, but clearly I didn’t (just like many other things related to coding ‘professionally’ and not just for Academic projects).

Though, luck was on my side, because tha’ boss was kind enough to give me a full lesson about it, and enforced me to use it.

What is const-correctness then?

It’s a tool to do better, maintainable and more readable code. (It’s existent on C/C++, but there are some similar/related concepts on other languages like java, Objective-C, C# and D).

The idea of the const-correctness comes from the reserved word const from C/C++ that shows and enforce the mutability of variables, pointers, and methods (OOP).At compile time the checks are done to avoid the modification of things that are marked as const (a vague definition, yes… but some examples will help me).

First I’m going to explain what happens on each case of declaring things as const (by the way, take a look at the wikipedia article, which is very detailed) and then by extension what means const-correctness.
For purposes of simplify the examples, I will not use headers, but please don’t do that on production code.

Const variable
int main()
{
    const int a = 1;
}

What means that? It means that the variable a is non-mutable and therefore any further attempt to do something like that:

int main()
{
    const int a = 1;
    a = 3; //Forbidden at compile.
}

Will not be possible and will throw this error at compile time:

$ g++ ./constCorrectness.cpp
constCorrectness.cpp: In function ‘int main()’:
constCorrectness.cpp:4:6: error: assignment of read-only variable ‘a’

Means you will not be able to assign or modify any variable declared as const. That also includes your own classes, if a variable is declared as const, you will only be able to do const behavior over it (keep reading)

Const pointers

Pointers are more or less the same example, but there is a subtle difference, which is that isn’t the same to declare a const pointer than a pointer to a const object than a const pointer to a const object:

class ClassB
{
public:
	int b;
	ClassB(int b) { this->b = b; }
};

void constAndPointers()
{
	ClassB i(0);
	ClassB j(1);

	/*const pointer*/
	ClassB* const constPtr = &i;
	constPtr->b = 4; //OK
//	constPtr = &j; //Forbidden.

	/*pointer to const*/
	const ClassB* ptrToConst = &i;
//	*ptrToConst = 5; //Forbidden.
	ptrToConst = &j; //OK

	/*const pointer to const*/
	const ClassB* const cPtC = &i;
//	*cPtC = 4; //Forbidden.
//	cPtc = &j  //Also forbidden.

	const int constVar = 3;
//	int* ptrToConstVar = &constVar; //Forbidden.
	const int* anotherPtrToConstVar = &constVar;
//	int* const yetAnotherPtr = &constVar; //Forbidden.
	const int* const andOneMorePtr = &constVar;
}

What’s failing on each case?

  • First, we see a const pointer, means we can modify the object we’re pointing to, but we can’t modify the pointer itself making it point to another place.
  • Second, we have a pointer to a const object, meaning we can assing that pointer to any direction, but the object we are pointing to, will be const (with all that means)
  • Third, we have a const pointer to a const object, let’s say its a complete read-only variable, no further assignments neither modifications to the pointed object.
  • I the fourth case, we have a slight different constraint, the original variable is already const. If we try to compile that code the 2 forbidden lines will fail, because of a conversion issue, it’s not possible to use a pointer to a non const object to point to a const one:

$ g++ constCorrectness.cpp
constCorrectness.cpp: In function ‘void constAndPointers()’:
constCorrectness.cpp:32:24: error: invalid conversion from ‘const int’ to ‘int*’ [-fpermissive]
constCorrectness.cpp:34:30: error: invalid conversion from ‘const int’ to ‘int*’ [-fpermissive]

Const methods & const on method-related code

At last we come to the const word for methods:

class ClassB
{
public:
        int b;
        ClassB(int b) { this->b = b; }

        int getCopyOfB() const { return b; }
//      int* getPtrToB() const { return &b; }
        const int* getConstPtrToB() const { return &b; }
};

In this new version of ClassB we see three types of getters for the member variable b, the first one can be marked as const because the return value will be a copy of b and not the member itself.

The third one instead returns a pointer to a “const int” object, meaning that we can safely mark that as const and we also will be sure that anyone calling that method will not modify the state of this object by accident (a little hint of the strength of const-correctness).

The second one will not compile because we cant return a pointer to a member of the object inside a method marked as const, because methods marked as const cannot modify the inner state of the object (well, you can take a look a the mutable reserved word, but thats a freedom that one usually do not take).

Const members

Const can also be applied to members of a class defining them once and never modifying them over the object lifecycle:

class ClassB
{
public:
        int b;
        const int a;
        ClassB(int b) : a(100) { this->b = b; }
        ClassB(int a, int b) : a(a), b(b) {}
}

Here we’ve added a new member a that is a const member. Therefore we need to instantiate it on the constructor because past that moment we will not be able to modify it (and thats the idea of the : for the constructor, otherwise, if we waited to the code inside the brackets a will not be modifiable)

If we tried to use this constructor

ClassB() { a = 100; this->b = 100; }

the compilation will fail:

$ g++ constCorrectness.cpp
constCorrectness.cpp: In constructor ‘ClassB::ClassB()’:
constCorrectness.cpp:10:2: error: uninitialized member ‘ClassB::a’ with ‘const’ type ‘const int’ [-fpermissive]
constCorrectness.cpp:10:17: error: assignment of read-only member ‘ClassB::a’

Every concept described here about const can be applied to arguments of functions and methods too.

Well, very pretty, but what’s all this for? To apply const-correctness to our projects:

To write code that sets constraints before reaching unexpected behaviors. Things that are not supposed to be modified will not be. And the biggest friend of the programmer, our compiler, will let us know if something might get wrong way before we reach that particular place.

To give the chance to others programmers to write simpler code because, if you use a library that applies code-correctness, and you have objects to pass to that library you know when to do a copy if needed or not because the library will explicitly said “I will not touch your object in any way”.

To have implicit preconditions and therefore reduce the number of different behaviors that might appear.

To write code that is simpler to understand by others, because you put very clear with signs what is supposed to be variable and what not, and most of the times only putting that clear, lets the other person understand why!

Great, and what was const-correctness anyway?

The act of putting const in every place that it should be written 🙂
To think and understand what behaviors are supposed to avoid modification of objects, when a variable needs to be re-assigned, when do you want modification or just information.

A simple example of lack of const-correctness that might be annoying:

Class Vector
{
private:
	double x,y,z;
public:
	Vector(double aX, double aY, double aZ) : x(aX), y(aY), z(aZ) {}
	lenSqr() { return x*x + y*y + z*z};
};

bool foo(const Vector& aV)
{
	return (aV.lenSqr() > 100.);
}

int main()
{
	Vector myVec(1.,1.,1.);
	std::cout << foo(myVec) ? "It is." : "It is not."<< std::endl;
}

That code will fail and it will be because of the lack of const-correctness. The problem resides in the fact that lenSqr isn’t a const method even when no modification is done to the object. Therefore on foo() we can’t access that method because the reference to aV is const but that method isn’t. Now this isn’t code that you or I will create but, if you have to deal with some libraries (i.e. a mathematical one what provides you a Vector impl.) this might happen to you and you will have to overcome a problem either by a special cast (const_cast) or by adjusting your code to the library.

So please, try to do your code adjusting to the const-correctness and so you might make the life of another person easier, and you will increase the robustness of your own code in the mean time!

Some good sources:

Wikipedia
CProgramming
Back to Top




Algo que aprendí en el primer día de mi actual trabajo. Se suponía que debía saber de ello, pero claramente no (como tantas otras cosas relacionadas con programar ‘profesionalmente’ y no solamente para proyectos académicos).

Pero, con suerte de mi lado, ‘el jefe’ fue un buen tipo y me dió una clase completa de const-correctness y me dijo que lo usara.

¿Qué es const-correctness entonces?

Es una herramienta para hacer código mejor, ‘mantenible’ y más legible. (Existe en C/C++, aunque existen contrapartes similares o relacionadas en otros lenguajes como java, Objective-C, C# o D).

La idea viene de la palabra reservada const de C/C++ que muestra y fuerza la inmutabilidad/mutabilidad de variables, punteros y métodos (OOP). En tiempo de compilación es donde se realizan los chequeos para evitar la modificacion de las cosas marcadas como const (definición vaga, pero con algunos ejemplos mejora).

Primer, qué casos se pueden econtrar de const (pueden mirar el artículo de wikipedia, que está bastante detallado) y luego por extensión qué significa const-correctness.
Con el propósito de simplificar los ejemplos, no voy a usar headers, pero por favor no hagan eso en código de producción.

Variable const
int main()
{
    const int a = 1;
}

Eso? Significa que la variable es inmutable y por lo tanto cualquier intento de hacer algo como esto:

int main()
{
    const int a = 1;
    a = 3; //Prohibido en tiempo de compilación
}

No va a ser posible y va a lanzar un error de compilación

$ g++ ./constCorrectness.cpp
constCorrectness.cpp: In function ‘int main()’:
constCorrectness.cpp:4:6: error: assignment of read-only variable ‘a’

Que vendría a decir que no podes asignar o modificar variables declaradas como const. Eso incluye objetos de clases propias, si una variable es declarada const, solo vas a poder utilizar su comportamiento const (más detalle abajo).

Punteros const

Los punteros son más o menos el mismo ejemplo, con una pequeña diferencia, no es lo mismo declarar un puntero const, un puntero a un objeto const o un puntero const a un objeto const:

class ClassB
{
public:
	int b;
	ClassB(int b) { this->b = b; }
};

void constAndPointers()
{
	ClassB i(0);
	ClassB j(1);

	/*puntero const*/
	ClassB* const constPtr = &i;
	constPtr->b = 4; //OK
//	constPtr = &j; //Error.

	/*puntero a const*/
	const ClassB* ptrToConst = &i;
//	*ptrToConst = 5; //Error.
	ptrToConst = &j; //OK

	/*puntero const a const*/
	const ClassB* const cPtC = &i;
//	*cPtC = 4; //Error.
//	cPtc = &j  //Error.

	const int constVar = 3;
//	int* ptrToConstVar = &constVar; //Error.
	const int* anotherPtrToConstVar = &constVar;
//	int* const yetAnotherPtr = &constVar; //Error.
	const int* const andOneMorePtr = &constVar;
}

¿Qué está fallando en cada caso?

  • Primero, tenemos un puntero const, lo que implica que es posible modificar el objeto al cual apunta pero no se puede aputar a ningun otro objeto.
  • Segundo, tenemos un puntero a un objeto const, por lo que es posible cambiar el puntero para que apunte a otra cosa, pero cada objeto apuntado va a ser const (con todo lo que ello signifique)
  • Tercero, un puntero const a un objeto const, digamos que es un casos de variable completamente read-only, no se puede cambiar el puntero ni tampoco se puede modificar el objeto apuntado.
  • En el cuarto caso hay una condición ligeramente diferente, la variable original ya es const. Si se intenta compilar el código con dichas dos líneas descomentadas, va a fallar, por un intento fallido de conversión (no es posible pasar de un puntero a objeto no-const a un puntero a objeto const):

$ g++ constCorrectness.cpp
constCorrectness.cpp: In function ‘void constAndPointers()’:
constCorrectness.cpp:32:24: error: invalid conversion from ‘const int’ to ‘int*’ [-fpermissive]
constCorrectness.cpp:34:30: error: invalid conversion from ‘const int’ to ‘int*’ [-fpermissive]

Métodos const & const en código relacionado a métodos

Al final, const para métodos:

class ClassB
{
public:
        int b;
        ClassB(int b) { this->b = b; }

        int getCopyOfB() const { return b; }
//      int* getPtrToB() const { return &b; }
        const int* getConstPtrToB() const { return &b; }
};

En esta nueva versión de ClassB, vemos 3 tipos de getters para la propiedad b, el primer getter se puede marcar como const porque retornará una copia y no el valor original.

El tercero sin embargo retorna un puntero, pero a un “const int”, por lo que se puede marcar como const, además de que nadie podrá modificar el estado interno de un objeto ClassB por este método (una pista del poder de const-correctness).

El segundo no compilará porque se está intentando retornar un puntero no-const a un miembro/propiedad del objeto dentro de una función marcada como const. No es posible modificar, digamos que, ni por acción ni por omisión, un objeto a través de un método const (bueno, pueden ver algo sobre la palabra reservada mutable, pero es una libertad que usualmente uno no toma).

Miembros const

Const también puede aplicarse a miembros de clase, definiendolos una vez y nunca modificándolos a lo largo del ciclo de vida del objeto.

class ClassB
{
public:
        int b;
        const int a;
        ClassB(int b) : a(100) { this->b = b; }
        ClassB(int a, int b) : a(a), b(b) {}
}

Aquí añadimos un nuevo miebro a que es const. Por lo tanto es necesario instanciarlo en el constructor, dado que pasado ese momento no será posible modificarlo (esta es la idea de : para el constructor, de otro modo, si se espera hasta el código dentro de las llaves, a ya no será modificable)

Intentando usar este constructor

ClassB() { a = 100; this->b = 100; }

La compilación fallará:

$ g++ constCorrectness.cpp
constCorrectness.cpp: In constructor ‘ClassB::ClassB()’:
constCorrectness.cpp:10:2: error: uninitialized member ‘ClassB::a’ with ‘const’ type ‘const int’ [-fpermissive]
constCorrectness.cpp:10:17: error: assignment of read-only member ‘ClassB::a’

Todos los conceptos descritos hasta acá acerca de const, pueden ser aplicados a argumentos de funciones y métodos también.

Bien, bonito, pero ¿para qué sirve esto? Para aplicar const-correctness a nuestros proyectos:

Para escribir código que crea restricciones antes de llegar a comportamientos inesperados. Cosas que no se supone pueden ser modificadas, no van a serlo. Y el mejor amigo del programador, el compilador, aivsará si algo no termina de cerrar mucho antes de que se llegue a ese caso particular.

Para darle la chance a otros programadores de escribir código más simple, dado que una librería que aplica const-conrrectness nos permite obviar la copia/resguardo de objetos cuando explícitamente nos indica que “no va a tocar de ningún modo” el objeto que le pasemos.

Para tener precondiciones implícitas y por ende reducir el número de diferentes comportamientos que puedan aparecer.

Para escribir código que es más simple de entender por otros, porque indica claramente y con señales qué se supone que es una variable y que no, y la mayoría de las veces tener eso claro ayuda a los demás a entender ¡porqué las cosas son como son!

Bien, y ¿qué era entocnes const-correctness?

El acto de poner const en cada lugar donde se debe poner const. 🙂
Pensar y entender qué comportamientos son los esperados, o en cuáles se espera o no la modificación de objetos; cuándo una variable va a ser re-asignada; cuándo se requiere o no modificar un estado o simplemente información sobre el mismo.

Un ejemplo simple de falta de const-correctness que puede ser molesto:

Class Vector
{
private:
	double x,y,z;
public:
	Vector(double aX, double aY, double aZ) : x(aX), y(aY), z(aZ) {}
	lenSqr() { return x*x + y*y + z*z};
};

bool foo(const Vector& aV)
{
	return (aV.lenSqr() > 100.);
}

int main()
{
	Vector myVec(1.,1.,1.);
	std::cout << foo(myVec) ? "It is." : "It is not."<< std::endl;
}

Ese código va a fallar por la falta de const-correctness. EL problema reside en el hecho de que lenSqr no es un método const, aún cuando no modifica el estado del objeto. Por lo tanto en foo() no es posible acceder al método porque aV se recibe como const y ese método no lo es. Esto no es código que vos o yo podemos crear pero, si tenés que trabajar con librerías (p.e. una librería matemática que provea una implementación de vector) esto te puede pasar y vas a tener que sobrellevar un problema bien usando un cast especial (const_cast) o bien ajustando tu código en función de la librería.

Así que por favor, tratá de que tu código se ajuste a const-correctness, así podés simplificar la vida de otros, y ¡además mejoras tu propio código al mismo tiempo!.

Un par de buenos recursos (en inglés):

Wikipedia
CProgramming
Inicio

Advertisements

2 thoughts on “Const-correctness

  1. Muy interesante!!

    Es cuestión de incorporar la costumbre, pero creo que el esfuerzo queda más que justificado por las razones que comentas.

    Abrazo,
    Martín.-

    PD: espero ver desactivada la moderación preventiva!

  2. Sí, es cierto, pero la costumbre se te pega enseguida! Y después que el compilador te de una mano, ahorra bastantes dolores de cabeza y te deja con más confianza sobre lo que hacés.

    La moderación preventiva está desactivada desde el post anterior, no podés ver tu comentario?

    Salú y gracias!

Leave a Reply (Deja una respuesta)

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s