2010-07-17 2 views
11

Cuando creo un std :: vector de objetos, el constructor de estos objetos no siempre se llama.¿Cómo llamar al constructor de objetos contenidos en un std :: vector?

#include <iostream> 
#include <vector> 
using namespace std; 

struct C { 
    int id; 
    static int n; 
    C() { id = n++; } // not called 
// C() { id = 3; }  // ok, called 
}; 

int C::n = 0; 


int main() 
{ 
    vector<C> vc; 

    vc.resize(10); 

    cout << "C::n = " << C::n << endl; 

    for(int i = 0; i < vc.size(); ++i) 
     cout << i << ": " << vc[i].id << endl; 
} 

Ésta es la salida me sale:

C::n = 1 
0: 0 
1: 0 
2: 0 
... 

Esto es lo que me gustaría:

C::n = 10 
0: 0 
1: 1 
2: 2 
... 

En este ejemplo, ¿estoy obligado a cambiar el tamaño del vector y luego inicializar su elementos "manualmente"?
¿Podría ser que los elementos de un vector no se inicialicen de forma ordenada, desde el primero hasta el último, por lo que no puedo obtener un comportamiento determinista?

Lo que me gustaría hacer es contar fácilmente el número de objetos creados en un programa, en diferentes contenedores, en diferentes puntos del código, y darles una identificación única a cada uno de ellos.

¡Gracias!

+3

No aceptar la respuesta que tiene actualmente. Es un truco y hace que tu código sea desordenado e inutilizable. El copy-constructor necesita copiar. – GManNickG

+1

He agregado un ejemplo de cómo puede obtener los resultados que está buscando _en este caso_. Dicho eso, deberías expandirte en lo que quieres lograr al hacer esto. Los objetos de un tipo se crean y destruyen con frecuencia (por ejemplo, cuando tiene lugar una reasignación vectorial, se copian todos los objetos del vector). –

+0

Si ilumina sus objetivos, estaremos encantados de brindarle una solución de trabajo segura. Pero tal como está, tus objetivos son contradictorios: no puedes distinguir cada objeto mientras colocas esos objetos en contenedores estándar. – GManNickG

Respuesta

5

La razón es que vector::resize inserciones copias llamando al constructor de copia proporcionada de forma automática, en lugar de los le constructores ha definido en tu ejemplo.

Con el fin de obtener el resultado que desea, se puede definir el constructor de copia explícitamente:

struct C { 
//.... 
C(const C& other) { 
    id = n++; 
    // copy other data members 
} 
//.... 
}; 

Debido a la forma del vector :: cambiar el tamaño de las obras, aunque (que tiene un segundo argumento opcional que se utiliza como un 'prototipo 'para las copias que crea, con un valor predeterminado en su caso de C()), esto crea 11 objetos en su ejemplo (el' prototipo 'y 10 copias de este).

Editar (para incluir algunos de los buenos consejos de los muchos comentarios):

Hay varias desventajas a esta solución A tener en cuenta, así como algunas opciones y variantes que son propensos a producir más fácil de mantener y código sensible.

  • Este método agrega costos de mantenimiento y una cantidad de riesgo. Debe recordar modificar su constructor de copia siempre que agregue o elimine variables de miembros de la clase. No tiene que hacer eso si confía en el constructor de copia predeterminado. Una forma de combatir este problema es encapsular el contador en otra clase (like this), que también es posiblemente un mejor diseño de OO, pero por supuesto también debe tener en cuenta the many issues that can crop up with multiple inheritance.

  • Puede hacer que sea más difícil de entender para otras personas, porque una copia ya no es exactamente lo que la mayoría de la gente esperaría. Del mismo modo, otro código que trate con sus clases (incluidos los contenedores estándar) puede portarse mal. Una forma de combatir esto es definir un método operator== para su clase (y es may be argued que esta es una buena idea al anular el constructor de copias, incluso si no usa el método), para mantenerlo conceptualmente "sano" y también como un tipo de documentación interna. Si su clase obtiene mucho uso, probablemente también termine proporcionando un operator= para que pueda mantener la separación de su ID de instancia generada automáticamente de las asignaciones de miembros de la clase que deberían tener lugar bajo este operador. Y así sucesivamente;)

  • Podría eliminar la ambigüedad del problema de "diferentes valores de id para las copias" si tiene control suficiente sobre el programa para usar instancias creadas dinámicamente (a través de nuevas) y usar punteros a los contenedores internos. Esto significa que necesita 'inicializar elementos' manualmente '' hasta cierto punto, pero no es mucho trabajo escribir una función que le devuelva un vector lleno de punteros a nuevas instancias inicializadas. Si trata de forma consistente con punteros cuando usa contenedores estándar, no tendrá que preocuparse por los contenedores estándar que crean instancias "debajo de las cubiertas".

Si usted es consciente de todas estas cuestiones, y cree que puede hacer frente a las consecuencias (que es, por supuesto, depende de su contexto particular altamente), a continuación, anulando el constructor de copia es una opción viable. Después de todo, la función de idioma está ahí por una razón. Obviamente, no es tan simple como parece, y debes tener cuidado.

+3

Esto no funcionará. El copy-constructor necesita copiar; si no lo hace, la clase no se puede usar en un contenedor estándar. Estaría cansado de hacer un copia-constructor que no copia nada en realidad. – GManNickG

+0

GMan: digamos que la clase tiene algunos datos que necesitan ser copiados, y una variable miembro que tiene que ser única (la identificación). ¿No es correcto, en este caso, escribir un constructor de copia que copia justo lo que puede, y le asigna un nuevo valor único a la identificación? Si esta es una mala manera de proceder, ¿hay alguna alternativa? – Pietro

+0

@Pietro: generalmente, después de invocar el constructor de copia, la copia debe ser la misma que la original (es decir, 'original == copy' debe ser verdadero). Si no, puede terminar con problemas extraños que pueden ser difíciles de depurar o su código será difícil de entender. Es raro que cada objeto de un tipo necesite un identificador absolutamente único (y si lo hace, a menudo su dirección puede usarse para tal fin). ¿Cuál es su caso de uso que requiere este identificador único? –

21

el constructor de estos objetos no siempre se llama.

Sí, lo es, pero no es el constructor lo que cree. La función miembro resize() es en realidad declarado como esto:

void resize(size_type sz, T c = T()); 

El segundo parámetro es el objeto a copia en cada uno de los elementos recién insertados del vector. Si omite el segundo parámetro, construye por defecto un objeto de tipo T y luego copia ese objeto en cada uno de los nuevos elementos.

En su código, se construye un C temporal y se llama al constructor predeterminado; id se establece en 0. El constructor de copia declarado implícitamente se llama diez veces (para insertar diez elementos en el vector), y todos los elementos en el vector tienen la misma identificación.

[Nota para aquellos que estén interesados: en C++ 03, el segundo parámetro de resize() (c) se toma por valor; en C++ 0x es tomado por const lvalue reference (ver LWG Defect 679)].

En este ejemplo, ¿estoy obligado a cambiar el tamaño del vector y luego inicializar sus elementos "manualmente"?

Usted puede (y probablemente debería) insertar los elementos en el vector de forma individual, por ejemplo,

std::vector<C> vc; 
for (unsigned i(0); i < 10; ++i) 
    vc.push_back(C()); 
3

El vector está utilizando el constructor de copias generado por C++ sin pedirlo. Una "C" se instancia, el resto se copia del prototipo.

0

@James: Digamos que tengo que ser capaz de distinguir cada objeto, incluso si más de uno puede (temporalmente) tener el mismo valor. Su dirección no es algo en lo que confíe tanto, debido a las reasignaciones de vectores. Además, diferentes objetos pueden estar en contenedores diferentes. ¿Los problemas que menciona están relacionados con las convenciones seguidas o pueden existir problemas técnicos reales con dicho código? La prueba que hice funciona bien.
Esto es lo que quiero decir:

#include <iostream> 
#include <vector> 
#include <deque> 
using namespace std; 

struct C { 
    int id; 
    static int n; 
    int data; 

    C() {    // not called from vector 
     id = n++; 
     data = 123; 
    } 

    C(const C& other) { 
     id = n++; 
     data = other.data; 
    } 

    bool operator== (const C& other) const { 
     if(data == other.data)  // ignore id 
      return true; 
     return false; 
    } 
}; 

int C::n = 0; 


int main() 
{ 
    vector<C> vc; 
    deque<C> dc; 

    vc.resize(10); 

    dc.resize(8); 

    cout << "C::n = " << C::n << endl; 

    for(int i = 0; i < vc.size(); ++i) 
     cout << "[vector] " << i << ": " << vc[i].id << "; data = " << vc[i].data << endl; 

    for(int i = 0; i < dc.size(); ++i) 
     cout << "[deque] " << i << ": " << dc[i].id << "; data = " << dc[i].data << endl; 
} 

Salida:

C::n = 20 
[vector] 0: 1; data = 123 
[vector] 1: 2; data = 123 
[vector] 2: 3; data = 123 
[vector] 3: 4; data = 123 
[vector] 4: 5; data = 123 
[vector] 5: 6; data = 123 
[vector] 6: 7; data = 123 
[vector] 7: 8; data = 123 
[vector] 8: 9; data = 123 
[vector] 9: 10; data = 123 
[deque] 0: 12; data = 123 
[deque] 1: 13; data = 123 
[deque] 2: 14; data = 123 
[deque] 3: 15; data = 123 
[deque] 4: 16; data = 123 
[deque] 5: 17; data = 123 
[deque] 6: 18; data = 123 
[deque] 7: 19; data = 123 
+0

@Pietro - en primer lugar, disculpe el drama que mi sugerencia parece haber causado :) Tiene razón en que la dirección no es completamente confiable, ya que si una instancia se destruye y otra se crea, existe la posibilidad de que la segunda tener la misma dirección que el primero.Me parece que anular el constructor de copia no es un método inapropiado para su situación. Por lo general, como se señaló, no es la mejor idea, pero hay una excepción para cada regla;) – sje397

+1

@sje: Deja de dar consejos, estás equivocado. Sobrescribir el constructor de copia * es un hack *. Es un mal diseño y solo "funciona" en esta pequeña prueba, rompiendo la practicidad de la clase en cualquier otro caso. @Pietro: No * hagas esto, es un código terrible. El código de sje hace que su clase no se pueda usar en contenedores estándar. – GManNickG

+1

_Tengo que ser capaz de distinguir cada objeto, incluso si más de uno puede (temporalmente) tener el mismo valor._ Aquí tienes que definir tu noción de temporal vs. persistente. Puede usar un identificador incremental para identificar objetos, pero luego los objetos no deben ser copiados o las copias deben tener la misma identificación. Si una copia tiene una identificación diferente, entonces no es una copia. –

Cuestiones relacionadas