2010-10-11 16 views
37

Quiero preguntarle por sus mejores prácticas con respecto a los constructores en C++. No estoy seguro de qué debería hacer en un constructor y qué no.Qué (no) hacer en un constructor

¿Debo utilizarlo solo para inicializaciones de atributos, llamando a constructores principales, etc.? O incluso podría incluir funciones más complejas en ellas, como leer y analizar datos de configuración, configurar bibliotecas externas a.s.o.

¿O debo escribir funciones especiales para esto? Resp. init()/cleanup()?

¿Qué son los PRO y CON aquí?

Descubrí que, por ejemplo, puedo deshacerme de los punteros compartidos cuando uso init() y cleanup(). Puedo crear los objetos en la pila como atributos de clase e inicializarlos más tarde mientras está construido.

Si lo manejo en el constructor necesito crear una instancia durante el tiempo de ejecución. Entonces necesito un puntero.

Realmente no sé cómo decidir.

¿Quizás me pueda ayudar?

+6

http://gotw.ca/gotw/066.htm – DumbCoder

+13

Creo que es una mala idea agregar cosas como 'init()' y 'cleanup()' a las clases. Esto grita por errores. No puede estar seguro si una clase ha sido 'init()' ed. Deberías agregar funciones para verificar esto, pero esto lo hace aún más complejo. – jwueller

+0

@DumbCoder: Lo siento pero no lo entiendo del todo. El artículo que vinculó habla de excepciones, por lo que yo entiendo. – tyrondis

Respuesta

22

La lógica compleja y el constructor no siempre se combinan bien, y hay fuertes defensores contra el trabajo pesado en un constructor (por razones).

La regla cardinal es que el constructor debe producir un objeto completamente utilizable.

class Vector 
{ 
public: 
    Vector(): mSize(10), mData(new int[mSize]) {} 
private: 
    size_t mSize; 
    int mData[]; 
}; 

No significa un objeto totalmente inicializado, puede diferir parte de inicialización (piensa perezoso), siempre y cuando el usuario no tiene que pensar en ello.

class Vector 
{ 
public: 
    Vector(): mSize(0), mData(0) {} 

    // first call to access element should grab memory 

private: 
    size_t mSize; 
    int mData[]; 
}; 

Si hay trabajo pesado para ser hecho, es posible que decide proceder con un método constructor, que va a hacer el trabajo pesado antes de llamar al constructor. Por ejemplo, imagine recuperar configuraciones de una base de datos y crear un objeto de configuración.

// in the constructor 
Setting::Setting() 
{ 
    // connect 
    // retrieve settings 
    // close connection (wait, you used RAII right ?) 
    // initialize object 
} 

// Builder method 
Setting Setting::Build() 
{ 
    // connect 
    // retrieve settings 

    Setting setting; 
    // initialize object 
    return setting; 
} 

Este método de creación es útil si posponer la construcción del objeto produce un beneficio significativo. Por ejemplo, si los objetos toman mucha memoria, posponer la adquisición de memoria después de las tareas que probablemente fallen, puede no ser una mala idea.

Este método de construcción implica Constructor privado y Constructor público (o amigo). Tenga en cuenta que tener un constructor privado impone una serie de restricciones en los usos que se pueden hacer de una clase (no se puede almacenar en contenedores STL, por ejemplo), por lo que es posible que deba fusionarse en otros patrones. Por eso, este método solo debe usarse en circunstancias excepcionales.

Es posible que desee considerar cómo probar tales entidades también, si depende de algo externo (archivo/base de datos), piense en Inyección de dependencia, realmente ayuda con las pruebas unitarias.

+0

+1 para mantener mecanismos simples en constructores. Pero sugerir el uso de RAII podría valer la pena aunque no sea tan simple. –

+2

El problema con llamar a los métodos init() de forma perezosa es que debe asegurarse de que se llamen antes de su uso. –

+0

@David: estoy de acuerdo, y cuanto más simple, mejor. Lo sugerí principalmente para completarlo. –

2

Bueno, «constructor» viene de la construcción, construcción, configuración. Entonces ahí es donde ocurre toda la inicialización. Cada vez que instale una clase, use el constructor para asegurarse de que todo esté hecho para que el nuevo objeto pueda funcionar.

4

Se espera que un constructor cree un objeto que pueda usarse desde la palabra ir. Si por alguna razón no puede crear un objeto utilizable, debería arrojar una excepción y terminarlo. Por lo tanto, todos los métodos complementarios/funciones que son necesarias para un objeto que funcione correctamente debe ser llamado desde el constructor (a menos que usted quiere tener perezoso de carga como características)

15
  • no llame a delete this o el destructor en el constructor.
  • No use los miembros init()/cleanup(). Si tiene que llamar a init() cada vez que crea una instancia, todo en init() debe estar en el constructor. El constructor debe colocar la instancia en un estado coherente que permita llamar a cualquier miembro público con un comportamiento bien definido. Del mismo modo, para la limpieza(), más la limpieza() mata a RAII. (Sin embargo, cuando tiene múltiples constructores, a menudo es útil tener una función privada init() llamada por ellos.)
  • Hacer cosas más complejas en constructores está bien, dependiendo del uso previsto de las clases y su diseño general . Por ejemplo, no sería una buena idea leer un archivo en el constructor de algún tipo de clase Integer o Point; los usuarios esperan que sea barato de crear. También es importante considerar cómo los constructores de acceso a archivos afectarán su capacidad para escribir pruebas unitarias. La mejor solución suele ser tener un constructor que solo tome los datos que necesita para construir los miembros y escribir una función no miembro que realice el análisis del archivo y devuelva una instancia.
+2

-1: no, hacer cosas más complejas es ** no ** bien. El uso de polimorfismo en un constructor o en un destructor a menudo está obligado a ** error **. Vea mi ejemplo a continuación. –

+3

"No invoque' eliminar esto' en el constructor "- ¿no ha oído hablar de Adquisición de recursos es invalidación? – Potatoswatter

+0

+1. No estoy de acuerdo con "no' init() '", sin embargo. Eso esencialmente argumenta en contra de cualquier factorización de código en constructores. 'init' no debería reemplazar la lista de inicializadores de miembros, pero una de las características C++ 0x favoritas de Stroustrup, los inicializadores de miembros no estáticos, obtiene lo mejor de ambos mundos. – Potatoswatter

27

El error más común que hacer en un constructor, así como en un destructor, es el uso de polimorfismo. El polimorfismo a menudo no funciona en los constructores!

p. Ej.:

class A 
{ 
public: 
    A(){ doA();} 
    virtual void doA(){}; 
} 

class B : public A 
{ 
public: 
    virtual void doA(){ doB();}; 
    void doB(){}; 
} 


void testB() 
{ 
    B b; // this WON'T call doB(); 
} 

Esto es porque el objeto B aún no esta construido mientras se realiza el constructor de la clase madre ... A lo tanto imposible que se llama la versión overriden de void doA();

Ben, en los comentarios a continuación, me ha pedido un ejemplo donde el polimorfismo funcionará en los constructores.

por ejemplo:

class A 
{ 
public: 
    void callAPolymorphicBehaviour() 
    { 
     doOverridenBehaviour(); 
    } 

    virtual void doOverridenBehaviour() 
    { 
     doA(); 
    } 

    void doA(){} 
}; 

class B : public A 
{ 
public: 
    B() 
    { 
     callAPolymorphicBehaviour(); 
    } 

    virtual void doOverridenBehaviour() 
    { 
     doB() 
    } 

    void doB(){} 
}; 

void testB() 
{ 
    B b; // this WILL call doB(); 
} 

Esta vez, la razón detrás es: en el momento de la función doOverridenBehaviour()virtual se invoca, ya se inicializa el objeto B (pero todavía no construido), esto significa que su tabla virtual se inicializa, y por lo tanto puede realizar polimorfismo.

+1

+1 ¡No se puede enfatizar lo suficiente! – helpermethod

+0

¿Querías heredar B de A como en 'clase B: public A'? – Arun

+0

@ArunSaha, sí, tienes razón ;-), gracias por la observación, he editado. –

4

yo preferiría pregunto:

What all to do in the constructor? 

y nada no especificado anteriormente es la respuesta a la pregunta del PO.

creo que el único propósito del constructor es a

  1. inicializar todas las variables miembro a un estado conocido, y

  2. asignar recursos (si es aplicable).

El artículo # 1 suena tan simple, pero veo que al olvido/ignorados de manera regular y sólo avisen por una herramienta de análisis estático. Nunca subestimes esto (juego de palabras intencionado).

9

Respuesta simple: depende.

Al diseñar su software, es posible que desee programar a través del principio RAII ("Adquisición de recursos es inicialización"). Esto significa (entre otras cosas) que el objeto en sí es responsable de sus recursos, y no del que llama. Además, es posible que desee familiarizarse con exception safety (en sus diferentes grados).

Por ejemplo, considere:

 
void func() { 
    MyFile f("myfile.dat"); 
    doSomething(f); 
} 

Si el diseño de la clase MyFile en el camino, que puede estar seguro antes de que doSomething(f)f se inicializa, se ahorra una gran cantidad de problemas para la comprobación de eso. Además, si libera los recursos mantenidos por f en el destructor, es decir, cierra el identificador del archivo, está en el lado seguro y es fácil de usar.

En este caso específico, puede utilizar las propiedades especiales de los constructores:

  • Si se lanza una excepción desde el constructor de su mundo exterior, el objeto no se creará. Esto significa que no se llamará al destructor y la memoria se liberará de inmediato.
  • Se debe llamar a un constructor . No puede forzar al usuario a usar cualquier otra función (excepto el destructor), solo por convención. Entonces, si desea obligar al usuario a inicializar su objeto, ¿por qué no a través del constructor?
  • Si tiene virtual métodos, no debe llamar a los desde dentro del constructor, a menos que sepa lo que está haciendo - usted (o usuarios posteriores) pueden sorprenderse por qué no se llama al método de anulación virtual. Lo mejor es no confundir a nadie.

Un constructor debe dejar el objeto en un estado utilizable . Y como siempre es hacer que sea difícil utilizar su API incorrectamente, lo mejor que puede hacer es hacer que sea fácil de usar correctamente (sic a Scott Meyers). Hacer la inicialización dentro del constructor debería ser su estrategia predeterminada, pero siempre hay excepciones, por supuesto.

Entonces: Es una buena forma de usar constructores para la inicialización. No siempre es posible, por ejemplo, los marcos de GUI a menudo necesitan ser construidos, luego inicializados. Pero si diseña su software completamente en esta dirección, puede ahorrar muchos problemas.

4

PUEDE arrojar desde un constructor, y a menudo es la mejor opción que crear un objeto zombie, es decir, un objeto que tiene un estado "fallido".

Sin embargo, nunca debe arrojar desde un destructor.

El compilador sabrá en qué orden se construyen los objetos miembros, el orden en que aparecen en el encabezado.Sin embargo, no se invocará el destructor como dijiste, lo que significa que si llamas nuevo varias veces dentro de un constructor no puedes confiar en que tu destructor invoque las eliminaciones por ti. Si los coloca en objetos de puntero inteligente, eso no es un problema, ya que estos objetos se eliminarán. Si los quiere como punteros sin formato, póngalos temporalmente en objetos auto_ptr hasta que sepa que su constructor ya no lanzará, luego llame a release() en todos sus auto_ptrs.

1

Puede hacer lo que quiera hacer, pero use constructor para tal fin para lo que se llama - crear objeto. y si necesita llamar a otros métodos, está bien. simplemente siga una regla - no la haga más compleja de lo que necesita. Una buena práctica es hacer un constructor lo más simple posible, pero eso no significa que solo necesite inicializar miembros.

+1

Una respuesta muy vaga, no me puedo convertir ni en votos a favor ni en negativos. –

1

¡Creo que lo más importante es un poco de sentido común! Se habla mucho de lo que se debe hacer y lo que no se hace, todo está muy bien, pero el punto clave a considerar es cómo se usará su objeto. Por ejemplo,

  1. ¿cuántas instancias de este objeto se crearán? (¿Están en contenedores, por ejemplo?)
  2. ¿con qué frecuencia se crean y se destruyen? (¿bucles?)
  3. ¿Qué tan grande es?

Si este objeto es una instancia única, que se construye desde el principio y la construcción no está en la ruta crítica, ¿por qué no hacer el trabajo pesado en el constructor (siempre que use excepciones adecuadamente, puede incluso tiene más sentido)? Si, por otro lado, es un objeto muy liviano que se crea y destruye en un ciclo por un corto período de tiempo: intente hacer lo menos posible (aparte de inicializar los miembros, por ejemplo) (los funtores son un muy buen ejemplo) de esto ...)

Existen ventajas de tener una carga de dos fases (o lo que sea), pero la principal desventaja es olvidarse de llamarlo, ¿cuántos de nosotros hemos hecho eso? :)

Por lo tanto, mi tuppence es, no se adhieren a una regla dura y rápida, mire cuidadosamente cómo se va a usar su objeto, y luego ¡diseñe a su gusto!

3

Un constructor se utiliza para construir un objeto, nada más y nada menos. Debe hacer lo que sea necesario para establecer las invariantes de clase dentro de un constructor y cuán complejo es que realmente depende del tipo de objeto que se inicializa.

Las funciones separadas de init() son una buena idea solo si por alguna razón no puede usar excepciones.

+0

+1 para "si por alguna razón no puede usar excepciones", eso no es poco común en entornos incrustados en tiempo real, donde otros beneficios de C++ sobre C hacen de C++ la primera opción, a pesar de la falta de excepciones. –

6

De El C++ Programming Language:

El uso de funciones tales como init() para proporcionar la inicialización de objetos de clase es poco elegante y errorprone. Debido a que no se indica en ninguna parte que se debe inicializar un objeto, un programador puede olvidarse de hacerlo, o hacerlo dos veces (a menudo con resultados igualmente desastrosos). Un mejor enfoque es permitir que el programador declare una función con el propósito explícito de inicializar objetos. Como tal función construye valores de un tipo dado, se llama un constructor.

que suelen tener en cuenta la siguiente regla en el diseño de una clase: Debo ser capaz de utilizar cualquier método de la clase segura después del constructor ha ejecutado. Seguro aquí significa que siempre puede lanzar excepciones si no se ha llamado al método init() del objeto, pero prefiero tener algo que realmente se pueda usar.

Por ejemplo, class std::string podría no asignar ninguna memoria cuando utiliza el constructor por defecto, porque la mayoría de los métodos (es decir begin() y end()) funcionarían correctamente si ambos devuelven punteros nulos, y c_str() no necesariamente devolver el búfer en uso por otras razones de diseño , por lo tanto, debe estar preparado para asignar memoria en cualquier momento. No asignar memoria en este caso todavía conduce a una instancia de cadena perfectamente utilizable.

Por el contrario, el uso de RAII en resguardos con cobertura para bloqueos mutex es un ejemplo de un constructor que puede ejecutarse durante un tiempo arbitrariamente prolongado (hasta que el propietario del candado lo libera) pero que todavía se acepta como buena práctica.

En cualquier caso, la inicialización diferida se puede realizar de formas más seguras que utilizando un método init(). Una forma es usar alguna clase intermedia que capture todos los parámetros para el constructor. Otra es usar el patrón del constructor.

Cuestiones relacionadas