Entonces, ¿en qué punto una clase se vuelve demasiado compleja para ser inmutable?
En mi opinión, no vale la pena molestarse en hacer que las clases pequeñas sean inmutables en idiomas como el que está mostrando. Estoy usando pequeños aquí y no complejos , porque incluso si agrega diez campos a esa clase y realmente les hace operaciones sofisticadas, dudo que tome kilobytes, mucho menos megabytes, mucho menos gigabytes, por lo que cualquier función que use instancias de su class simplemente puede hacer una copia barata de todo el objeto para evitar modificar el original si quiere evitar causar efectos secundarios externos.
Estructuras de datos persistentes
Donde encuentro uso personal para la inmutabilidad es para grandes estructuras de datos centrales que agregan un montón de datos pequeños como instancias de la clase que está mostrando, como una que almacena un millón NamedThings
. Al pertenecer a una estructura de datos persistente que es inmutable y estar detrás de una interfaz que solo permite acceso de solo lectura, los elementos que pertenecen al contenedor se vuelven inmutables sin que la clase de elemento ( NamedThing
) tenga que ocuparse de él.
Copias Baratas
La estructura de datos persistente permite que las regiones se transformen y se hagan únicas, evitando modificaciones al original sin tener que copiar la estructura de datos en su totalidad. Esa es la verdadera belleza de esto. Si quisiera escribir ingenuamente funciones que eviten los efectos secundarios que ingresan una estructura de datos que toma gigabytes de memoria y solo modifica el valor de la memoria de un megabyte, entonces tendría que copiar todo el asunto para evitar tocar la entrada y devolver un nuevo salida. Puede copiar gigabytes para evitar efectos secundarios o causar efectos secundarios en ese escenario, por lo que debe elegir entre dos opciones desagradables.
Con una estructura de datos persistente, le permite escribir dicha función y evitar hacer una copia de toda la estructura de datos, solo requiere aproximadamente un megabyte de memoria adicional para la salida si su función solo transformó el valor de la memoria de un megabyte.
Carga
En cuanto a la carga, hay una inmediata al menos en mi caso. Necesito esos constructores de los que la gente habla o "transitorios" como los llamo para poder expresar efectivamente transformaciones a esa estructura de datos masiva sin tocarla. Código como este:
void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
// Transform stuff in the range, [first, last).
for (; first != last; ++first)
transform(stuff[first]);
}
... entonces tiene que escribirse así:
ImmList<Stuff> transform_stuff(ImmList<Stuff> stuff, int first, int last)
{
// Grab a "transient" (builder) list we can modify:
TransientList<Stuff> transient(stuff);
// Transform stuff in the range, [first, last)
// for the transient list.
for (; first != last; ++first)
transform(transient[first]);
// Commit the modifications to get and return a new
// immutable list.
return stuff.commit(transient);
}
Pero a cambio de esas dos líneas de código adicionales, la función ahora es segura para llamar a través de subprocesos con la misma lista original, no causa efectos secundarios, etc. También hace que sea realmente fácil hacer que esta operación sea una acción que el usuario puede deshacer ya que deshacer puede almacenar una copia superficial barata de la lista anterior.
Excepción-Seguridad o recuperación de errores
No todos podrían beneficiarse tanto como yo de las estructuras de datos persistentes en contextos como estos (encontré mucho uso para ellos en sistemas de deshacer y edición no destructiva, que son conceptos centrales en mi dominio VFX), pero una cosa es aplicable a casi todos deben tener en cuenta la excepción de seguridad o recuperación de errores .
Si desea hacer que la función de mutación original sea segura para excepciones, entonces necesita una lógica de reversión, para lo cual la implementación más simple requiere copiar toda la lista:
void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
// Make a copy of the whole massive gigabyte-sized list
// in case we encounter an exception and need to rollback
// changes.
MutList<Stuff> old_stuff = stuff;
try
{
// Transform stuff in the range, [first, last).
for (; first != last; ++first)
transform(stuff[first]);
}
catch (...)
{
// If the operation failed and ran into an exception,
// swap the original list with the one we modified
// to "undo" our changes.
stuff.swap(old_stuff);
throw;
}
}
En este punto, la versión mutable segura para excepciones es aún más costosa desde el punto de vista computacional y podría decirse que es aún más difícil de escribir correctamente que la versión inmutable que utiliza un "generador". Y muchos desarrolladores de C ++ simplemente descuidan la seguridad de excepciones y tal vez eso esté bien para su dominio, pero en mi caso me gusta asegurarme de que mi código funcione correctamente incluso en el caso de una excepción (incluso escribir pruebas que arrojan excepciones deliberadamente a la excepción de prueba) seguridad), y eso hace que tenga que poder revertir cualquier efecto secundario que una función cause a la mitad de la función si algo arroja.
Cuando desee estar a salvo de excepciones y recuperarse de errores con gracia sin que su aplicación se bloquee y se queme, entonces debe revertir / deshacer cualquier efecto secundario que una función pueda causar en caso de error / excepción. Y allí el constructor puede ahorrar más tiempo de programador de lo que cuesta junto con el tiempo de cálculo porque: ...
¡No tiene que preocuparse por revertir los efectos secundarios en una función que no causa ninguno!
Volviendo a la pregunta fundamental:
¿En qué punto las clases inmutables se convierten en una carga?
Siempre son una carga en los idiomas que giran más en torno a la mutabilidad que a la inmutabilidad, por lo que creo que debería usarlos donde los beneficios superan significativamente los costos. Pero a un nivel lo suficientemente amplio para estructuras de datos lo suficientemente grandes, creo que hay muchos casos en los que es una compensación digna.
También en el mío, solo tengo unos pocos tipos de datos inmutables, y todas ellas son enormes estructuras de datos destinadas a almacenar grandes cantidades de elementos (píxeles de una imagen / textura, entidades y componentes de un ECS, y vértices / bordes / polígonos de una malla).