Toda clase que especialice las operaciones de mover debería ser construible por defecto
Publicado el 21 de septiembre de 2020
La idea detrás del diseño de RAII es que los recursos pertenecen a objetos. Un recurso es todo aquello que hay que pedir a un sistema y devolver cuando se deje de usar. Por supuesto el sistema operativo maneja recursos, como la memoria o los archivos, pero también puede ser algo más abstracto como el tiempo de uso de un mutex, e incluso podemos diseñar nuestros propios recursos que abstraigan y representen el comportamiento de nuestros sistemas. Un objeto puede adquirir recursos en su constructor o a lo largo de su vida, y debe liberarlos en el destructor. En RAII hay tres operaciones esenciales: el destructor, que tiene como función liberar los recursos; la copia, que crea un objeto cuyo valor observable es igual al de otro siendo ambos objetos separados e independientes; y mover, que transfiere los recursos de un objeto a otro, dejando al primero vacío. El estándar define que todo objeto del que se ha movido queda en un estado válido pero no especificado. En otras palabras, mover de un objeto no lo destruye, y un objeto del que se ha movido tiene que seguir pudiendo usarse. Por lo general un objeto del que se ha movido se queda en un estado vacío, en el que no es propietario de ningún recurso. Eso significa que para toda clase que implementa operaciones de movimiento, existe un estado válido en el que el objeto está vacío. Éste es también el estado en el que el destructor no hace nada, y es único. Para un tipo que implementa operaciones de movimiento, existe un solo estado en el que no es propietario de ningún recurso.
Este estado es un muy buen candidato para el estado al que inicializar con el constructor por defecto. En primer lugar, porque es intuitivo. El constructor por defecto no coge ningún argumento, por lo que debería inicializar siempre al mismo estado. Para la mayoría de los tipos, el único estado el que un objeto que es propietario de recursos puede estar que es intuitivo al ser construido sin ningún parámetro, es el estado vacío. Por lo tanto, es lo que el usuario esperaría obtener al crear un objeto sin pasar ningún parámetro al constructor, y deberíamos tener en cuenta las expectativas de los usuarios al diseñar nuestras interfaces.
Además de esto, el estado vacío es muy barato de alcanzar. Por lo general el coste debería rondar un
memset
a 0 a los bytes de la estructura. Es importante tener en cuenta que el constructor por defecto es una de esas funciones que se ejecutan muchas veces y de forma implícita, así que nunca queremos meter cálculos pesados ahí. Recordemos que se va a ejecutar no sólo al crear una variable de este tipo sin pasar parámetros al constructor, sino también cuando se instancia otra clase que tenga una variable de este tipo. Queremos que el coste en nuestros programas sea explícito y obvio ya que esto facilita muchísimo el poder razonar sobre él, el poder optimizar y el poder escribir por defecto sin tener que pensar demasiado programas que son razonablemente eficientes. Por eso, el estado vacío, siendo prácticamente gratuito de alcanzar, es un fantástico candidato para el constructor por defecto.
Por último, esta regla de diseño está bastante aceptada y es la que encontramos, por ejemplo, en todos los tipos de la biblioteca estándar en los que es aplicable. Los contenedores por defecto están vacíos,
unique_ptr
y
shared_ptr
son nulos, los
fstream
s no abren ningún archivo... La biblioteca estándar es también muy consistente en lo contrario. Tipos que implementar un destructor pero no se pueden mover, como
lock_guard
, no son construibles por defecto, mientras que la alternativa que la biblioteca nos brinda que sí que es movible,
unique_lock
, sí que tiene un constructor por defecto que crea un objeto sin ningún mutex asociado. Estó se une de nuevo a lo de ser intuitivo. Si muchos te los tipos que usamos siguen esta regla, es de esperar que el resto lo hagan también.
En resumen, esta convención de tener que todo tipo que especialice las operaciones de mover sea construible por defecto y se inicialice al estado vacío en el que se queda al ser movido es un patrón de diseño muy sólido e intuitivo que ya está en uso por gran parte del código que usamos y deberíamos seguirla al implementar nuevos tipos que gestionen recursos.
Logo of RSS.