c++ copy-constructor circular-dependency rule-of-three

c++ - ¿Excepción a la Regla de los Tres?



copy-constructor circular-dependency (3)

He leído mucho sobre la Regla de Tres de C ++. Muchas personas lo juran. Pero cuando se establece la regla, casi siempre incluye una palabra como "generalmente", "probable" o "probablemente", lo que indica que hay excepciones. No he visto mucha discusión sobre cuáles podrían ser estos casos excepcionales, casos donde la Regla de los Tres no se cumple, o al menos donde adherirse a ella no ofrece ninguna ventaja.

Mi pregunta es si mi situación es una excepción legítima a la Regla de Tres. Creo que en la situación que describo a continuación, son necesarios un constructor de copia definido explícitamente y un operador de asignación de copia, pero el destructor predeterminado (generado implícitamente) funcionará bien. Aquí está mi situación:

Tengo dos clases, A y B. La que se trata aquí es A. B es amiga de A. A contiene un objeto B. B contiene un puntero A destinado a apuntar al objeto A que posee el objeto B. B usa este puntero para manipular miembros privados del objeto A. B nunca se crea una instancia excepto en el constructor A. Me gusta esto:

// A.h #include "B.h" class A { private: B b; int x; public: friend class B; A( int i = 0 ) : b( this ) { x = i; }; };

y...

// B.h #ifndef B_H // preprocessor escape to avoid infinite #include loop #define B_H class A; // forward declaration class B { private: A * ap; int y; public: B( A * a_ptr = 0 ) { ap = a_ptr; y = 1; }; void init( A * a_ptr ) { ap = a_ptr; }; void f(); // this method has to be defined below // because members of A can''t be accessed here }; #include "A.h" void B::f() { ap->x += y; y++; } #endif

¿Por qué iba a configurar mis clases de esa manera? Lo prometo, tengo buenas razones. Estas clases realmente hacen mucho más de lo que he incluido aquí.

Así que el resto es fácil, ¿verdad? No hay gestión de recursos, no hay Big Three, no hay problema. ¡Incorrecto! El constructor de copia predeterminado (implícito) para A no será suficiente. Si hacemos esto:

A a1; A a2(a1);

obtenemos un nuevo objeto A a2 que es idéntico a a1 , lo que significa que a2.b es idéntico a a1.b , lo que significa que a2.b.ap sigue apuntando a a1 ! Esto no es lo que queremos. Debemos definir un constructor de copia para A que duplique la funcionalidad del constructor de copia predeterminado y luego configure el nuevo A::b.ap para que apunte al nuevo objeto A. Añadimos este código a la class A :

public: A( const A & other ) { // first we duplicate the functionality of a default copy constructor x = other.x; b = other.b; // b.y has been copied over correctly // b.ap has been copied over and therefore points to ''other'' b.init( this ); // this extra step is necessary };

Un operador de asignación de copia es necesario por la misma razón y se implementaría utilizando el mismo proceso de duplicación de la funcionalidad del operador de asignación de copia predeterminado y luego llamando a b.init( this ); .

Pero no hay necesidad de un destructor explícito; ergo esta situación es una excepción a la Regla de Tres. Estoy en lo cierto?


No te preocupes tanto por la "Regla de los Tres". Las reglas no están ahí para ser obedecidas a ciegas; Están ahí para hacerte pensar. Usted ha pensado Y has concluido que el destructor no lo haría. Así que no escribas uno. La regla existe para que no se olvide de escribir el destructor, perdiendo recursos.

De todos modos, este diseño crea un potencial para que B :: ap esté equivocado. Esa es una clase completa de posibles errores que podrían eliminarse si se tratara de una sola clase, o si se vincularan de alguna manera más sólida.


Parece que B está fuertemente acoplado a A , y siempre debería usar la instancia A que lo contiene. ¿Y que A siempre contiene una instancia B ? Y acceden a los miembros privados de cada uno a través de la amistad.

Por lo tanto, uno se pregunta por qué son clases separadas.

Pero asumiendo que necesita dos clases por alguna otra razón, aquí hay una solución directa que elimina toda la confusión de su constructor / destructor:

class A; class B { A* findMyA(); // replaces B::ap }; class A : /* private */ B { friend class B; }; A* B::findMyA() { return static_cast<A*>(this); }

Aún podría usar la contención y encontrar la instancia de A from B es this puntero utilizando la macro offsetof . Pero eso es más complicado que usar static_cast y static_cast el compilador a la matemática del puntero para usted.


Voy con @dspeyer. Piensas y decides. En realidad, alguien ya ha llegado a la conclusión de que la regla de tres por lo general (si toma las decisiones correctas durante su diseño) se reduce a la regla de dos: haga que sus recursos sean administrados por objetos de la biblioteca (como los punteros inteligentes mencionados anteriormente) y generalmente puede deshacerse de incinerador de basuras. Si tiene la suerte de deshacerse de todo y confiar en el compilador para generar el código para usted.

En la nota al margen: su constructor de copia NO duplica el compilador generado por uno. Utiliza la asignación de copia dentro de ella mientras que el compilador usaría constructores de copia. Deshágase de las asignaciones en el cuerpo de su constructor y use la lista de inicializadores. Será más rápido y más limpio.

Buena pregunta, buena respuesta de Ben (otro truco para confundir a mis colegas en el trabajo) y estoy feliz de darles a ustedes dos votos positivos.