c++ const immutability const-cast

Manera idiomática de crear una clase inmutable y eficiente en C++



const immutability (8)

Estoy buscando hacer algo como esto (C #).

public final class ImmutableClass { public readonly int i; public readonly OtherImmutableClass o; public readonly ReadOnlyCollection<OtherImmutableClass> r; public ImmutableClass(int i, OtherImmutableClass o, ReadOnlyCollection<OtherImmutableClass> r) : i(i), o(o), r(r) {} }

Las posibles soluciones y sus problemas asociados que he encontrado son:

1. Usar const para los miembros de la clase , pero esto significa que se elimina el operador de asignación de copia predeterminado.

Solución 1:

struct OtherImmutableObject { const int i1; const int i2; OtherImmutableObject(int i1, int i2) : i1(i1), i2(i2) {} }

Problema 1:

OtherImmutableObject o1(1,2); OtherImmutableObject o2(2,3); o1 = o2; // error: use of deleted function ''OtherImmutableObject& OtherImmutableObject::operator=(const OtherImmutableObject&)`

EDITAR: Esto es importante ya que me gustaría almacenar objetos inmutables en un std::vector pero recibo un error: use of deleted function ''OtherImmutableObject& OtherImmutableObject::operator=(OtherImmutableObject&&)

2. Usar métodos get y devolver valores , pero esto significa que tendrían que copiar objetos grandes, lo cual es una ineficiencia que me gustaría saber cómo evitar. Este hilo sugiere la solución get, pero no aborda cómo manejar el paso de objetos no primitivos sin copiar el objeto original.

Solución 2:

class OtherImmutableObject { int i1; int i2; public: OtherImmutableObject(int i1, int i2) : i1(i1), i2(i2) {} int GetI1() { return i1; } int GetI2() { return i2; } } class ImmutableObject { int i1; OtherImmutableObject o; std::vector<OtherImmutableObject> v; public: ImmutableObject(int i1, OtherImmutableObject o, std::vector<OtherImmutableObject> v) : i1(i1), o(o), v(v) {} int GetI1() { return i1; } OtherImmutableObject GetO() { return o; } // Copies a value that should be immutable and therefore able to be safely used elsewhere. std::vector<OtherImmutableObject> GetV() { return v; } // Copies the vector. }

Problema 2: las copias innecesarias son ineficientes.

3. Usar métodos get y devolver referencias const o punteros const pero esto podría dejar referencias colgantes o punteros. Este hilo habla sobre los peligros de las referencias que quedan fuera del alcance de los retornos de funciones.

Solución 3:

class OtherImmutableObject { int i1; int i2; public: OtherImmutableObject(int i1, int i2) : i1(i1), i2(i2) {} int GetI1() { return i1; } int GetI2() { return i2; } } class ImmutableObject { int i1; OtherImmutableObject o; std::vector<OtherImmutableObject> v; public: ImmutableObject(int i1, OtherImmutableObject o, std::vector<OtherImmutableObject> v) : i1(i1), o(o), v(v) {} int GetI1() { return i1; } const OtherImmutableObject& GetO() { return o; } const std::vector<OtherImmutableObject>& GetV() { return v; } }

Problema 3:

ImmutableObject immutable_object(1,o,v); // elsewhere in code... OtherImmutableObject& other_immutable_object = immutable_object.GetO(); // Somewhere else immutable_object goes out of scope, but not other_immutable_object // ...and then... other_immutable_object.GetI1(); // The previous line is undefined behaviour as immutable_object.o will have been deleted with immutable_object going out of scope

Se puede producir un comportamiento indefinido debido a que devuelve una referencia de cualquiera de los métodos Get .


  1. Realmente desea objetos inmutables de algún tipo más semántica de valor (ya que le importa el rendimiento en tiempo de ejecución y desea evitar el montón). Simplemente defina una struct con todos los miembros de datos public .

    struct Immutable { const std::string str; const int i; };

    Puede crear instancias y copiarlos, leer miembros de datos, pero eso es todo. Mover-construir una instancia a partir de una referencia rvalue de otra todavía se copia.

    Immutable obj1{"...", 42}; Immutable obj2 = obj1; Immutable obj3 = std::move(obj1); // Copies, too obj3 = obj2; // Error, cannot assign

    De esta manera, realmente se asegura de que cada uso de su clase respete la inmutabilidad (suponiendo que nadie haga cosas malas const_cast ). Se puede proporcionar funcionalidad adicional a través de funciones gratuitas, no tiene sentido agregar funciones miembro a una agregación de miembros de datos de solo lectura.

  2. Desea 1., todavía con una semántica de valor, pero un poco relajado (de modo que los objetos ya no sean realmente inmutables) y también le preocupa que necesite la construcción de movimientos en aras del rendimiento en tiempo de ejecución. No hay forma de evitar private miembros private datos y las funciones de miembros getter:

    class Immutable { public: Immutable(std::string str, int i) : str{std::move(str)}, i{i} {} const std::string& getStr() const { return str; } int getI() const { return i; } private: std::string str; int i; };

    El uso es el mismo, pero la construcción de movimiento realmente se mueve.

    Immutable obj1{"...", 42}; Immutable obj2 = obj1; Immutable obj3 = std::move(obj1); // Ok, does move-construct members

    Si desea que se permita la asignación o no, ahora está bajo su control. Simplemente = delete los operadores de asignación si no lo desea, de lo contrario, vaya con el generado por el compilador o implemente el suyo.

    obj3 = obj2; // Ok if not manually disabled

  3. No le importa la semántica de valor y / o los incrementos de conteo de referencia atómica están bien en su escenario. Use la solución representada en la respuesta de @ NathanOliver .


Básicamente, puede obtener lo que desea al aprovechar std::unique_ptr o std::shared_ptr . Si solo desea uno de estos objetos, pero permite que se mueva, puede usar std::unique_ptr . Si desea permitir que varios objetos ("copias") tengan el mismo valor, puede usar un std::shared_Ptr . Use un alias para acortar el nombre y proporcionar una función de fábrica y se vuelve bastante indoloro. Eso haría que su código se vea así:

class ImmutableClassImpl { public: const int i; const OtherImmutableClass o; const ReadOnlyCollection<OtherImmutableClass> r; public ImmutableClassImpl(int i, OtherImmutableClass o, ReadOnlyCollection<OtherImmutableClass> r) : i(i), o(o), r(r) {} } using Immutable = std::unique_ptr<ImmutableClassImpl>; template<typename... Args> Immutable make_immutable(Args&&... args) { return std::make_unique<ImmutableClassImpl>(std::forward<Args>(args)...); } int main() { auto first = make_immutable(...); // first points to a unique object now // can be accessed like std::cout << first->i; auto second = make_immutable(...); // now we have another object that is separate from first // we can''t do // second = first; // but we can transfer like second = std::move(first); // which leaves first in an empty state where you can give it a new object to point to }

Si el código se cambia para usar un shared_ptr en shared_ptr lugar, entonces podría hacer

second = first;

y luego ambos objetos apuntan al mismo objeto, pero ninguno puede modificarlo.


C ++ no tiene la capacidad de predefinir una clase como inmutable o constante.

Y en algún momento probablemente llegarás a la conclusión de que no deberías usar const para los miembros de la clase en C ++. Simplemente no vale la pena las molestias, y honestamente puedes prescindir de ellas.

Como solución práctica, intentaría:

typedef class _some_SUPER_obtuse_CLASS_NAME_PLEASE_DONT_USE_THIS { } const Immutable;

para disuadir a cualquiera de usar cualquier cosa menos Immutable en su código.


En C ++ simplemente no hay * necesidad de hacer esto:

class ImmutableObject { const int i1; const int i2; } ImmutableObject o1: ImmutableObject o2; o1 = o2; // Doesn''t compile because immutable objects are not mutable.

Si desea una referencia mutable a un objeto inmutable / const, utilice un puntero, un puntero inteligente o un reference_wrapper . A menos que realmente desee tener una clase cuyo contenido pueda ser cambiado por cualquier persona en cualquier momento, que es lo opuesto a una clase inmutable.

* Por supuesto, C ++ es un lenguaje donde "no" no existe. En esas preciosas pocas circunstancias realmente excepcionales, puede usar const_cast .


La inmutabilidad en C ++ no se puede comparar directamente con la inmutabilidad en la mayoría de los otros lenguajes populares debido a la semántica de valor universal de C ++. Tienes que descubrir qué quieres decir con "inmutable".

Desea poder asignar nuevos valores a variables de tipo OtherImmutableObject . Eso tiene sentido, ya que puede hacerlo con variables de tipo ImmutableObject en C #.

En ese caso, la forma más sencilla de obtener la semántica que desea es

struct OtherImmutableObject { int i1; int i2; };

Puede parecer que esto es mutable. Después de todo, puedes escribir

OtherImmutableObject x{1, 2}; x.i1 = 3;

Pero el efecto de esa segunda línea es (ignorando la concurrencia ...) exactamente el mismo que el efecto de

x = OtherImmutableObject{3, x.i2};

así que si desea permitir la asignación a variables de tipo OtherImmutableObject entonces no tiene sentido rechazar la asignación directa a los miembros, ya que no proporciona ninguna garantía semántica adicional; todo lo que hace es hacer que el código para la misma operación abstracta sea más lento. (En este caso, la mayoría de los compiladores de optimización probablemente generarán el mismo código para ambas expresiones, pero si uno de los miembros fuera std::string podría no ser lo suficientemente inteligente como para hacerlo).

Tenga en cuenta que este es el comportamiento de básicamente todos los tipos estándar en C ++, incluidos int , std::complex , std::string , etc. Todos son mutables en el sentido de que puede asignarles nuevos valores, y todos inmutables en el tiene la sensación de que lo único que puede hacer (de manera abstracta) para cambiarlos es asignarles nuevos valores, al igual que los tipos de referencia inmutables en C #.

Si no quieres esa semántica, tu única otra opción es prohibir la asignación. Aconsejaría hacerlo declarando que sus variables son const , no declarando que todos los miembros del tipo sean const , ya que le brinda más opciones sobre cómo puede usar la clase. Por ejemplo, puede crear una instancia inicialmente mutable de la clase, construir un valor en ella y luego "congelarla" utilizando solo referencias const a ella a partir de entonces, como convertir un StringBuilder en una string , pero sin la sobrecarga de copiarlo.

(Una posible razón para declarar que todos los miembros son const podría ser que permite una mejor optimización en algunos casos. Por ejemplo, si una función obtiene un OtherImmutableObject const& , y el compilador no puede ver el sitio de la llamada, no es seguro almacenar en caché los valores de los miembros a través de llamadas a otro código desconocido, ya que el objeto subyacente puede no tener el calificador const . Pero si los miembros reales se declaran const , entonces creo que sería seguro almacenar en caché los valores).


Los objetos inmutables funcionan mucho mejor con la semántica de puntero. Entonces escriba un puntero inteligente inmutable:

struct immu_tag_t {}; template<class T> struct immu:std::shared_ptr<T const> { using base = std::shared_ptr<T const>; immu():base( std::make_shared<T const>() ) {} template<class A0, class...Args, std::enable_if_t< !std::is_base_of< immu_tag_t, std::decay_t<A0> >{}, bool > = true, std::enable_if_t< std::is_construtible< T const, A0&&, Args&&... >{}, bool > = true > immu(A0&& a0, Args&&...args): base( std::make_shared<T const>( std::forward<A0>(a0), std::forward<Args>(args)... ) ) {} template<class A0, class...Args, std::enable_if_t< std::is_construtible< T const, std::initializer_list<A0>, Args&&... >{}, bool > = true > immu(std::initializer_list<A0> a0, Args&&...args): base( std::make_shared<T const>( a0, std::forward<Args>(args)... ) ) {} immu( immu_tag_t, std::shared_ptr<T const> ptr ):base(std::move(ptr)) {} immu(immu&&)=default; immu(immu const&)=default; immu& operator=(immu&&)=default; immu& operator=(immu const&)=default; template<class F> immu modify( F&& f ) const { std::shared_ptr<T> ptr; if (!*this) { ptr = std::make_shared<T>(); } else { ptr = std::make_shared<T>(**this); } std::forward<F>(f)(*ptr); return {immu_tag_t{}, std::move(ptr)}; } };

Esto aprovecha shared_ptr para la mayor parte de su implementación; La mayoría de las desventajas de shared_ptr no son un problema con objetos inmutables.

A diferencia de ptr compartido, le permite crear el objeto directamente y, de forma predeterminada, crea un estado no nulo. Todavía puede alcanzar un estado nulo al ser movido desde. Puede crear uno en un estado nulo haciendo:

immu<int> immu_null_int{ immu_tag_t{}, {} };

y un int no nulo vía:

immu<int> immu_int;

o

immu<int> immu_int = 7;

Agregué un útil método de utilidad llamado modify . Modificar le proporciona una instancia mutable de la T para pasar a un lambda para modificar antes de que se devuelva empaquetado en un immu<T> .

El uso concreto se ve así:

struct data; using immu_data = immu<data>; struct data { int i; other_immutable_class o; std::vector<other_immutable_class> r; data( int i_in, other_immutable_class o_in, std::vector<other_immutable_class> r_in ): i(i_in), o(std::move(o_in)), r( std::move(r_in)) {} };

Luego usa immu_data .

El acceso a los miembros requiere -> no . , y debe verificar si hay immu_data nulos si se los pasa.

Así es como usas .modify :

immu_data a( 7, other_immutable_class{}, {} ); immu_data b = a.modify([&](auto& b){ ++b.i; b.r.emplace_back() });

Esto crea una b cuyo valor es igual a a , excepto que i se incrementa en 1, y hay una otra other_immutable_class extra en br (construido por defecto). Tenga en cuenta que a no se modifica creando b .

Probablemente hay errores tipográficos arriba, pero he usado el diseño.

Si quieres ponerte elegante, puedes hacer que immu compatible con copiar en escritura o modificar en el lugar si es único. Sin embargo, es más difícil de lo que parece.


Para responder a su pregunta, no crea estructuras de datos inmutables en C ++ porque las referencias const a todo el objeto hacen el truco. Las violaciones de la regla se hacen visibles por la presencia de const_cast s.

Si puedo referirme a "Pensar fuera del cuadrante de sincronización" de Kevlin Henney, hay dos preguntas que hacer sobre los datos:

  • ¿Es una estructura inmutable o mutable?
  • ¿Es compartido o no compartido?

Estas preguntas se pueden organizar en una bonita mesa de 2x2 con 4 cuadrantes. En un contexto concurrente, solo un cuadrante necesita sincronización: datos mutables compartidos.

De hecho, los datos inmutables no necesitan sincronizarse porque no puede escribir en ellos, y las lecturas concurrentes están bien. Los datos no compartidos no necesitan sincronizarse, ya que solo el propietario de los datos puede escribir o leer en ellos.

Por lo tanto, está bien que una estructura de datos sea mutable en un contexto no compartido, y los beneficios de la inmutabilidad solo se producen en un contexto compartido.

En mi opinión, la solución que le brinda la mayor libertad es definir su clase para la mutabilidad y la inmutabilidad, utilizando la constidad solo donde tenga sentido (los datos que se inicializan y luego nunca cambian):

/* const-correct */ class C { int f1_; int f2_; const int f3_; // Semantic constness : initialized and never changed. };

Luego puede usar instancias de su clase C como mutables o inmutables, en beneficio de la constness-donde-tiene-sentido en cualquier caso.

Si ahora desea compartir su objeto, puede empacarlo en un puntero inteligente para const :

shared_ptr<const C> ptr = make_shared<const C>(f1, f2, f3);

Usando esta estrategia, su libertad abarca los 3 quandrantes no sincronizados mientras permanece fuera del cuadrante de sincronización. (por lo tanto, limitando la necesidad de hacer que su estructura sea inmutable)


Yo diría que la forma más idiomática sería eso:

struct OtherImmutable { int i1; int i2; OtherImmutable(int i1, int i2) : i1(i1), i2(i2) {} };

Pero ... eso no es inmutable?

De hecho, pero puede pasarlo como un valor:

void frob1() { OtherImmutable oi; oi = frob2(oi); } auto frob2(OtherImmutable oi) -> OtherImmutable { // cannot affect frob1 oi, since it''s a copy }

Aún mejor, los lugares que no necesitan mutar localmente pueden definir sus variables locales como const:

auto frob2(OtherImmutable const oi) -> OtherImmutable { return OtherImmutable{oi.i1 + 1, oi.i2}; }