ser qué que puede programacion privado por omision ejemplo dinamico destructores copia constructores c++ iterator copy-constructor default-copy-constructor

qué - ¿El constructor de copia predeterminado de C++ es intrínsecamente inseguro? ¿Los iteradores son fundamentalmente inseguros también?



qué es un constructor en c++? (7)

¿Es este un problema bien conocido?

Bueno, se sabe, pero no diría que es bien conocido. Los punteros de hermanos no aparecen a menudo, y la mayoría de las implementaciones que he visto en la naturaleza se han roto de la misma manera que la tuya.

Creo que el problema es lo suficientemente infrecuente como para haber escapado al aviso de la mayoría de la gente; Curiosamente, ahora que sigo más Rust que C ++, aparece con bastante frecuencia debido a la rigurosidad del sistema tipográfico (es decir, el compilador rechaza esos programas, lo que provoca preguntas).

¿tiene una solución elegante / idiomática?

Hay muchos tipos de situaciones de punteros hermanos , por lo que realmente depende; sin embargo, conozco dos soluciones genéricas:

  • llaves
  • elementos compartidos

Vamos a revisarlos en orden.

Señalando a un miembro de clase, o apuntando a un contenedor indexable, entonces uno puede usar un desplazamiento o una clave en lugar de un iterador. Es un poco menos eficiente (y podría requerir una búsqueda) sin embargo, es una estrategia bastante simple. Lo he visto con gran efecto en la situación de memoria compartida (donde el uso de punteros es un no-no, ya que el área de memoria compartida se puede mapear en diferentes direcciones).

La otra solución es utilizada por Boost.MultiIndex, y consiste en un diseño de memoria alternativo. Se deriva del principio del contenedor intrusivo: en lugar de colocar el elemento en el contenedor (moviéndolo en la memoria), un contenedor intrusivo utiliza ganchos que ya están dentro del elemento para conectarlo al lugar correcto. A partir de ahí, es bastante fácil usar diferentes ganchos para conectar elementos individuales en contenedores múltiples, ¿verdad?

Bueno, Boost.MultiIndex lo impulsa dos pasos más:

  1. Utiliza la interfaz de contenedor tradicional (es decir, mueve su objeto), pero el nodo al que se mueve el objeto es un elemento con múltiples ganchos
  2. Utiliza varios ganchos / contenedores en una sola entidad

Puede verificar varios ejemplos y, en particular, el Ejemplo 5: Índices secuenciados se parece mucho a su propio código.

Solía ​​pensar que el modelo de objetos de C ++ es muy robusto cuando se siguen las mejores prácticas.
Hace solo unos minutos, sin embargo, me di cuenta de que no había tenido antes.

Considera este código:

class Foo { std::set<size_t> set; std::vector<std::set<size_t>::iterator> vector; // ... // (assume every method ensures p always points to a valid element of s) };

He escrito un código como este. Y hasta hoy, no había visto un problema con eso.

Pero, al pensarlo un poco más, me di cuenta de que esta clase está muy rota:
¡Su copy-constructor y copy-assignment copian los iteradores dentro del vector , lo que implica que seguirán apuntando al antiguo set ! ¡El nuevo no es una copia verdadera después de todo!

En otras palabras, debo implementar manualmente el copy-constructor aunque esta clase no esté administrando ningún recurso (¡no RAII)!

Esto me parece asombroso. Nunca me he encontrado con este problema, y ​​no conozco ninguna forma elegante de resolverlo. Pensando un poco más, me parece que la construcción de copias no es segura por defecto ; de hecho, me parece que las clases no deberían poder copiarse por defecto, porque cualquier tipo de acoplamiento entre sus variables de instancia corre el riesgo de renderizar la copia predeterminada -constructor inválido .

¿Los iteradores son fundamentalmente inseguros de almacenar? O, ¿deberían las clases ser realmente no copiables por defecto?

Las soluciones que puedo pensar a continuación son indeseables, ya que no me permiten aprovechar el constructor de copias generado automáticamente:

  1. Implementar manualmente un constructor de copia para cada clase no trivial que escribo. Esto no solo es propenso a errores, sino también doloroso escribir para una clase complicada.
  2. Nunca almacene iteradores como variables miembro. Esto parece ser severamente limitante.
  3. Inhabilitar la copia por defecto en todas las clases que escribo, a menos que pueda demostrar explícitamente que son correctas. Esto parece funcionar completamente en contra del diseño de C ++, que para la mayoría de los tipos tiene semántica de valores y, por lo tanto, se puede copiar.

¿Es este un problema bien conocido? De ser así, ¿tiene una solución elegante / idiomática?


Es este un problema bien conocido

Sí. Cada vez que tenga una clase que contenga punteros o datos tipo puntero como un iterador, debe implementar su propio constructor de copias y el operador de asignaciones para asegurarse de que el nuevo objeto tenga punteros / iteradores válidos.

y si es así, ¿tiene una solución elegante / idiomática?

Quizás no sea tan elegante como te gustaría, y probablemente no sea el mejor en rendimiento (pero, a veces, las copias no lo son, por lo que C ++ 11 agregó semántica de movimientos), pero tal vez algo así te funcione (suponiendo que std::vector contiene iteradores en el std::set del mismo objeto principal):

class Foo { private: std::set<size_t> s; std::vector<std::set<size_t>::iterator> v; struct findAndPushIterator { Foo &foo; findAndPushIterator(Foo &f) : foo(f) {} void operator()(const std::set<size_t>::iterator &iter) { std::set<size_t>::iterator found = foo.s.find(*iter); if (found != foo.s.end()) foo.v.push_back(found); } }; public: Foo() {} Foo(const Foo &src) { *this = src; } Foo& operator=(const Foo &rhs) { v.clear(); s = rhs.s; v.reserve(rhs.v.size()); std::for_each(rhs.v.begin(), rhs.v.end(), findAndPushIterator(*this)); return *this; } //... };

O, si usa C ++ 11:

class Foo { private: std::set<size_t> s; std::vector<std::set<size_t>::iterator> v; public: Foo() {} Foo(const Foo &src) { *this = src; } Foo& operator=(const Foo &rhs) { v.clear(); s = rhs.s; v.reserve(rhs.v.size()); std::for_each(rhs.v.begin(), rhs.v.end(), [this](const std::set<size_t>::iterator &iter) { std::set<size_t>::iterator found = s.find(*iter); if (found != s.end()) v.push_back(found); } ); return *this; } //... };


"Inherentemente inseguro"

No, las características que mencionas no son inherentemente inseguras; el hecho de que hayas pensado en tres posibles soluciones seguras al problema es evidencia de que no hay una falta de seguridad "inherente" aquí, aunque creas que las soluciones son indeseables.

Y sí, hay RAII aquí: los contenedores ( set y vector ) son recursos de gestión. Creo que su punto es que el RAII "ya está siendo atendido" por los contenedores std . Pero debe considerar las instancias del contenedor como "recursos", y de hecho su clase los está administrando . Tiene razón al decir que no administra directamente la memoria dinámica , porque la biblioteca estándar se ocupa de este aspecto del problema de gestión. Pero hay más en el problema de gestión, del que hablaré un poco más a continuación.

Comportamiento predeterminado "mágico"

El problema es que aparentemente esperas que puedas confiar en que el constructor de copia predeterminado "hará lo correcto" en un caso no trivial como este. No estoy seguro de por qué esperaba el comportamiento correcto, quizás esté esperando que la memorización de reglas empíricas como la "regla del 3" sea una forma robusta de asegurarse de no dispararse en el pie. ? Ciertamente sería agradable (y, como se señala en otra respuesta, Rust va mucho más allá que otros lenguajes de bajo nivel para hacer que disparar a pie sea mucho más difícil), pero C ++ simplemente no está diseñado para un diseño de clase "irreflexivo" de ese tipo , ni debería ser

Conceptualizando el comportamiento del constructor

No voy a tratar de abordar la cuestión de si este es un "problema bien conocido", porque realmente no sé cuán bien caracterizado está el problema de los datos "hermanos" y el almacenamiento de iteradores. Pero espero poder convencerlo de que, si se toma el tiempo para pensar en el comportamiento de copiado-constructor para cada clase que escriba que pueda copiarse, esto no debería ser un problema sorprendente .

En particular, cuando decida usar el constructor de copia predeterminado, debe pensar en lo que hará el constructor de copia predeterminado: es decir, llamará al constructor de copia de cada miembro no primitivo y no sindical (es decir, miembros que tener copia-constructores) y bitwise-copy el resto.

Al copiar su vector de iteradores, ¿qué hace el constructor de copias de std::vector ? Realiza una "copia profunda", es decir, los datos dentro del vector se copian. Ahora, si el vector contiene iteradores, ¿cómo afecta eso la situación? Bueno, es simple: los iteradores son los datos almacenados por el vector, por lo que los iteradores se copiarán. ¿Qué hace un copy-constructor de un iterador? No voy a buscar esto realmente, porque no necesito saber los detalles: solo necesito saber que los iteradores son como punteros en esto (y en otros aspectos), y al copiar un puntero simplemente copia el puntero , no los datos apuntados . Es decir, los iteradores y punteros no tienen una copia profunda por defecto.

Tenga en cuenta que esto no es sorprendente: por supuesto los iteradores no hacen copias profundas por defecto. Si lo hicieran, obtendrías un nuevo conjunto diferente para cada iterador que se está copiando. Y esto tiene aún menos sentido de lo que parece inicialmente: por ejemplo, ¿qué significaría realmente si los iteradores unidireccionales hicieran copias en profundidad de sus datos? Presumiblemente obtendría una copia parcial , es decir, todos los datos restantes que todavía están "en frente" de la posición actual del iterador, más un nuevo iterador que apunta al "frente" de la nueva estructura de datos.

Ahora considere que no hay forma de que un constructor de copias conozca el contexto en el que se llama. Por ejemplo, considere el siguiente código:

using iter = std::set<size_t>::iterator; // use typedef pre-C++11 std::vector<iter> foo = getIters(); // get a vector of iterators useIters(foo); // pass vector by value

Cuando se llama a getIters , el valor de retorno puede moverse, pero también puede ser copiado. La asignación a foo también invoca un copia-constructor, aunque esto también puede ser eliminado. Y a menos que useIters tome su argumento por referencia, entonces también tiene una llamada de constructor de copia allí.

En cualquiera de estos casos, ¿esperaría que el constructor de copia cambie a qué std::set apuntan los iteradores contenidos en std::vector<iter> ? ¡Por supuesto no! Entonces, naturalmente, el constructor de copias de std::vector no puede diseñarse para modificar los iteradores de esa manera particular, y de hecho el constructor de copias de std::vector es exactamente lo que necesita en la mayoría de los casos donde realmente será usado.

Sin embargo, supongamos que std::vector podría funcionar así: supongamos que tiene una sobrecarga especial para "vector-de-iteradores" que podría volver a colocar los iteradores, y que el compilador podría de alguna manera ser "contado" solo para invocar a este constructor especial cuando los iteradores realmente necesitan ser reubicados. (Tenga en cuenta que la solución de "solo invocar la sobrecarga especial cuando se genera un constructor predeterminado para una clase contenedora que también contiene una instancia del tipo de datos subyacente de los iteradores" no funcionaría, y si los iteradores std::vector en su caso estaban apuntando a un conjunto de estándares diferente , y estaban siendo tratados simplemente como una referencia a datos administrados por alguna otra clase? Diablos, ¿cómo se supone que el compilador sabe si todos los iteradores apuntan al mismo std::set ?) Ignorando este problema de cómo el compilador sabría cuándo invocar este constructor especial, ¿cómo sería el código del constructor? _Ctnr<T>::iterator , usando _Ctnr<T>::iterator como nuestro tipo de iterador (usaré C ++ 11 / 14isms y seré un poco descuidado, pero el punto general debería ser claro):

template <typename T, typename _Ctnr> std::vector< _Ctnr<T>::iterator> (const std::vector< _Ctnr<T>::iterator>& rhs) : _data{ /* ... */ } // initialize underlying data... { for (auto i& : rhs) { _data.emplace_back( /* ... */ ); // What do we put here? } }

De acuerdo, entonces queremos que cada nuevo iterador copiado sea ​​reubicado para referirse a una instancia diferente de _Ctnr<T> . Pero, ¿de dónde vendría esta información ? Tenga en cuenta que el constructor de copias no puede tomar el nuevo _Ctnr<T> como argumento: entonces ya no sería un constructor de copias. Y, en cualquier caso, ¿cómo sabría el compilador qué _Ctnr<T> proporcionar? (Tenga en cuenta también que para muchos contenedores, encontrar el "iterador correspondiente" para el nuevo contenedor puede no ser trivial).

Gestión de recursos con std:: containers

Esto no es solo una cuestión de que el compilador no sea tan "inteligente" como podría o debería ser. Esta es una instancia en la que usted, el programador, tiene un diseño específico en mente que requiere una solución específica. En particular, como se mencionó anteriormente, tiene dos recursos, ambos std:: contenedores. Y tienes una relación entre ellos . Aquí llegamos a algo que la mayoría de las otras respuestas han establecido, y que a estas alturas debería ser muy, muy claro: los miembros de la clase relacionados requieren un cuidado especial, ya que C ++ no administra este acoplamiento por defecto. Pero lo que espero también sea claro en este punto es que no se debe pensar que el problema surge específicamente debido al acoplamiento de los miembros de datos; el problema es simplemente que la construcción predeterminada no es mágica, y el programador debe conocer los requisitos para copiar correctamente una clase antes de decidir dejar que el constructor generado implícitamente maneje la copia.

La solución elegante

... Y ahora llegamos a la estética y las opiniones. Parece que le resulta poco elegante obligarse a escribir un constructor de copias cuando no tiene ningún puntero o matriz en su clase que deba gestionarse manualmente.

Pero los constructores de copia definidos por el usuario son elegantes; permitiéndole escribirlos es la elegante solución de C ++ al problema de escribir clases correctas no triviales.

Es cierto que esto parece un caso en el que la "regla de 3" no se aplica del todo, ya que hay una clara necesidad de =delete el constructor de copias o escribirlo usted mismo, pero no hay una necesidad clara (todavía) para un usuario. destructor definido Pero, una vez más, no puede simplemente programar según las reglas generales y esperar que todo funcione correctamente, especialmente en un lenguaje de bajo nivel como C ++; debe conocer los detalles de (1) lo que realmente desea y (2) cómo se puede lograr eso.

Entonces, dado que el acoplamiento entre std::set y std::vector realmente crea un problema no trivial, resolver el problema envolviéndolos en una clase que implemente correctamente (o simplemente elimine) el constructor de copia es en realidad una solución muy elegante (e idiomática).

Definición explícita versus eliminación

Usted menciona una posible "nueva regla" a seguir en sus prácticas de codificación: "Inhabilitar la copia por defecto en todas las clases que escribo, a menos que pueda demostrar explícitamente que son correctas". Si bien esta podría ser una regla práctica más segura (al menos en este caso) que la "regla de 3" (especialmente cuando su criterio para "¿Necesito implementar el 3?" Es para verificar si se requiere un eliminador), mi anterior precaución contra confiar en las reglas generales todavía se aplica.

Pero creo que la solución aquí es en realidad más simple que la regla general propuesta. No es necesario que demuestres formalmente la corrección del método predeterminado; simplemente necesita tener una idea básica de lo que haría y de lo que necesita hacer.

Arriba, en mi análisis de su caso particular, entré en muchos detalles, por ejemplo, mencioné la posibilidad de "iteradores de copia profunda". No necesita entrar en tantos detalles para determinar si el constructor de copia predeterminado funcionará correctamente o no. En su lugar, simplemente imagine cómo se verá el constructor de copia creado manualmente; debería ser capaz de decir con bastante rapidez qué tan similar es su constructor imaginario explícitamente definido al que el compilador generaría.

Por ejemplo, un Foo clase que contenga data vectoriales únicos tendrá un constructor de copia que se verá así:

Foo::Foo(const Foo& rhs) : data{rhs.data} {}

Sin siquiera escribir eso, sabes que puedes confiar en el generado implícitamente, porque es exactamente lo mismo que lo que escribiste arriba.

Ahora, considere el constructor para su clase Foo :

Foo::Foo(const Foo& rhs) : set{rhs.set} , vector{ /* somehow use both rhs.set AND rhs.vector */ } // ...???? {}

Inmediatamente, dado que simplemente copiar miembros del vector no funcionará, puede decir que el constructor predeterminado no funcionará. Entonces ahora debe decidir si su clase necesita ser copiable o no.


C ++ copy / move ctor / assign son seguros para tipos de valores regulares. Los tipos de valores regulares se comportan como enteros u otros valores "regulares".

También son seguros para los tipos semánticos del puntero, siempre que la operación no cambie a lo que el puntero "debería" apuntar. Señalar algo "dentro de usted" u otro miembro es un ejemplo de dónde falla.

Son algo seguros para los tipos semánticos de referencia, pero mezclar semántica de puntero / referencia / valor en la misma clase tiende a ser inseguro / con errores / peligroso en la práctica.

La regla de cero es que crea clases que se comportan como tipos de valores regulares o como tipos semánticos de punteros que no necesitan volverse a sentar al copiar / mover. Entonces no tiene que escribir ctors / mover ctors.

Los iteradores siguen la semántica del puntero.

Lo idiomático / elegante a este respecto es acoplar estrechamente el contenedor del iterador con el contenedor señalado, y bloquear o escribir el controlador de copia allí. No son realmente cosas separadas una vez que uno contiene punteros en el otro.


La afirmación de que Foo no está gestionando ningún recurso es falsa.

Copiar el constructor a un lado, si se elimina un elemento del set , debe haber un código en Foo que administre el vector para que se elimine el respectivo iterador.

Creo que la solución idiomática es simplemente usar un contenedor, un vector<size_t> , y verificar que el recuento de un elemento sea cero antes de insertarlo. Entonces los valores predeterminados de copiar y mover están bien.


Sí, este es un "problema" bien conocido: cada vez que guarde punteros en un objeto, probablemente necesite algún tipo de constructor de copia personalizada y operador de asignación para asegurarse de que los punteros sean todos válidos y señalen las cosas esperadas .

Como los iteradores son solo una abstracción de punteros de elementos de colección, tienen el mismo problema.


Sí, por supuesto es un problema bien conocido.

Si su clase almacena punteros, como desarrollador experimentado, intuitivamente sabrá que los comportamientos de copia predeterminados pueden no ser suficientes para esa clase.

Su clase almacena iteradores y, dado que también son "identificadores" de datos almacenados en otros lugares, se aplica la misma lógica.

Esto no es "asombroso".