sobrecargar sobrecarga relacionales poo operadores operador funciones asignacion c++ const undefined-behavior assignment-operator

relacionales - sobrecarga del operador<< c++



Miembro de const y operador de asignación. ¿Cómo evitar el comportamiento indefinido? (6)

¿Cómo se puede asignar a una A si tiene un miembro const? Estás tratando de lograr algo que es fundamentalmente imposible. Su solución no tiene un comportamiento nuevo sobre el original, que no es necesariamente UB, pero el suyo definitivamente lo es.

El simple hecho es que estás cambiando un miembro const. O bien, debe anular la constricción de su miembro o deshacerse del operador de asignación. No hay solución para su problema, es una contradicción total.

Editar para más claridad:

Const cast no siempre introduce un comportamiento indefinido. Tú, sin embargo, ciertamente lo hiciste. Aparte de cualquier otra cosa, no está definido no llamar a todos los destructores, y ni siquiera llamaste al correcto, antes de colocarte en él a menos que supieras con certeza que T es una clase POD. Además, hay comportamientos indefinidos de tiempo libre relacionados con varias formas de herencia.

Si invoca un comportamiento indefinido, puede evitarlo si no intenta asignar un objeto const.

answered la pregunta sobre std :: vector of objects y const-correctness y obtuve un voto negativo y un comentario sobre un comportamiento indefinido. No estoy de acuerdo y por eso tengo una pregunta.

Considere la clase con el miembro const:

class A { public: const int c; // must not be modified! A(int c) : c(c) {} A(const A& copy) : c(copy.c) { } // No assignment operator };

Quiero tener un operador de asignación pero no quiero usar const_cast como en el siguiente código de una de las respuestas:

A& operator=(const A& assign) { *const_cast<int*> (&c)= assign.c; // very very bad, IMHO, it is UB return *this; }

Mi solucion es

A& operator=(const A& right) { if (this == &right) return *this; this->~A() new (this) A(right); return *this; }

¿Tengo un comportamiento indefinido?

Por favor, su solución sin UB.


En ausencia de otros miembros (no const ), esto no tiene ningún sentido en absoluto, independientemente de un comportamiento indefinido o no.

A& operator=(const A& assign) { *const_cast<int*> (&c)= assign.c; // very very bad, IMHO, it is UB return *this; }

AFAIK, esto no es un comportamiento indefinido que ocurre aquí porque c no es una instancia de static const , o no pudo invocar al operador de asignación de copia. Sin embargo, const_cast debe sonar y decirle que algo está mal. const_cast fue diseñado principalmente para funcionar alrededor de API no correctas, y no parece ser el caso aquí.

Además, en el siguiente fragmento de código:

A& operator=(const A& right) { if (this == &right) return *this; this->~A() new (this) A(right); return *this; }

Tienes dos riesgos principales , el primero de los cuales ya ha sido señalado.

  1. En presencia tanto de una instancia de clase derivada de A como de un destructor virtual, esto conducirá a una reconstrucción parcial de la instancia original.
  2. Si el constructor llama en new(this) A(right); lanza una excepción, tu objeto será destruido dos veces. En este caso particular, no será un problema, pero si tiene una limpieza significativa, lo lamentará.

Edición : si su clase tiene este miembro const que no se considera "estado" en su objeto (es decir, es un tipo de ID utilizado para rastrear instancias y no es parte de las comparaciones en el operator== y similares), entonces lo siguiente podría tener sentido:

A& operator=(const A& assign) { // Copy all but `const` member `c`. // ... return *this; }


Lea este enlace:

http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=368

En particular...

Este truco supuestamente evita la reduplicación de código. Sin embargo, tiene algunos defectos graves. Para que funcione, el destructor de C debe asignar NULLify a todos los punteros que ha eliminado porque la posterior llamada del constructor de copia podría eliminar los mismos punteros nuevamente cuando reasigne un nuevo valor a las matrices de caracteres.


Primero: cuando creas un miembro de datos const , le dices al compilador y a todo el mundo que este miembro de datos nunca cambia . Por supuesto, entonces no puede asignárselo y, ciertamente , no debe engañar al compilador para que acepte el código que lo hace, no importa cuán inteligente sea el truco.
Puede tener un miembro de datos const o un operador de asignación asignando a todos los miembros de datos. No puedes tener los dos.

En cuanto a su "solución" al problema:
Supongo que llamar al destructor en un objeto dentro de una función miembro invocada para esos objetos invocaría UB de inmediato. Invocar a un constructor en datos en bruto sin inicializar para crear un objeto desde dentro de una función miembro que se ha invocado para un objeto que residía donde ahora se invoca al constructor en datos en bruto ... también me suena como UB . (Demonios, solo el deletrear esto hace que mis uñas de los pies se enrosquen). Y, no, no tengo el capítulo y el verso de la norma para eso. Odio leer la norma. Creo que no puedo soportar su metro.

Sin embargo, aparte de los aspectos técnicos, admito que puede salirse con la suya "solución" en casi todas las plataformas , siempre y cuando el código sea tan simple como en su ejemplo . Sin embargo, esto no hace que sea una buena solución. De hecho, diría que ni siquiera es una solución aceptable , porque el código IME nunca es tan simple como eso. Con el paso de los años, se ampliará, cambiará, mutará y torcerá, y luego fallará silenciosamente y requerirá un cambio de 36 horas de depuración para encontrar el problema. No sé sobre usted, pero cada vez que encuentro un fragmento de código como este responsable de 36 horas de diversión de depuración, quiero estrangular al miserable imbécil que me hizo esto.

Herb Sutter, en su GotW # 23 , analiza esta idea pieza por pieza y finalmente concluye que "está llena de trampas , a menudo está mal , y hace de la vida un infierno para los autores de clases derivadas ... nunca use el truco" de implementar la asignación de copias en términos de construcción de copias mediante el uso de un destructor explícito seguido de una nueva ubicación , aunque este truco aparece cada tres meses en los grupos de noticias "(enfatice el mío).


Si definitivamente quieres tener un miembro inmutable (pero asignable), entonces sin UB puedes poner las cosas así:

#include <iostream> class ConstC { int c; protected: ConstC(int n): c(n) {} int get() const { return c; } }; class A: private ConstC { public: A(int n): ConstC(n) {} friend std::ostream& operator<< (std::ostream& os, const A& a) { return os << a.get(); } }; int main() { A first(10); A second(20); std::cout << first << '' '' << second << ''/n''; first = second; std::cout << first << '' '' << second << ''/n''; }


Tu código provoca un comportamiento indefinido.

No solo "indefinido si A se usa como una clase base y esto, eso o lo otro". En realidad indefinido, siempre. return *this ya es UB, porque no se garantiza que haga referencia al nuevo objeto.

Específicamente, considere 3.8 / 7:

Si, una vez que finaliza la vida útil de un objeto y antes de que se reutilice o libere el almacenamiento que ocupó el objeto ocupado, se creará un nuevo objeto en la ubicación de almacenamiento que ocupaba el objeto original, un puntero que apuntaba al objeto original, una referencia que referido al objeto original, o el nombre del objeto original se referirá automáticamente al nuevo objeto y, una vez que ha comenzado la vida útil del nuevo objeto, puede usarse para manipular el nuevo objeto si:

...

- el tipo del objeto original no está calificado de forma constante y, si un tipo de clase, no contiene ningún miembro de datos no estáticos cuyo tipo esté calificado de forma constante o un tipo de referencia,

Ahora, "después de que finalice la vida útil de un objeto y antes de que el almacenamiento que el objeto ocupado se reutiliza o libere, se crea un nuevo objeto en la ubicación de almacenamiento que ocupó el objeto original" es exactamente lo que está haciendo.

Su objeto es de tipo de clase, y contiene un miembro de datos no estáticos cuyo tipo es const-calificado. Por lo tanto, después de que su operador de asignación haya ejecutado, no se garantiza que los punteros, referencias y nombres que se refieren al objeto anterior se refieran al nuevo objeto y puedan usarse para manipularlo.

Como ejemplo concreto de lo que podría salir mal, considere:

A x(1); B y(2); std::cout << x.c << "/n"; x = y; std::cout << x.c << "/n";

Espera esta salida?

1 2

¡Incorrecto! Es plausible que pueda obtener esa salida, pero la razón por la que los miembros const son una excepción a la regla establecida en 3.8 / 7 es que el compilador puede tratar a xc como el objeto const que dice ser. En otras palabras, el compilador puede tratar este código como si fuera:

A x(1); B y(2); int tmp = x.c std::cout << tmp << "/n"; x = y; std::cout << tmp << "/n";

Porque los objetos const (informalmente) no cambian sus valores . El valor potencial de esta garantía al optimizar código que involucra objetos const debe ser obvio. Para que haya alguna forma de modificar xc sin invocar a UB, esta garantía debería eliminarse. Entonces, mientras los escritores estándar hayan hecho su trabajo sin errores, no hay forma de hacer lo que quieres.

[*] De hecho, tengo mis dudas sobre el uso de this como el argumento para la ubicación nueva; posiblemente deberías haberlo copiado primero en un void* y usarlo. Pero no me molesta si eso es específicamente UB, ya que no guardaría la función como un todo.