2010-09-24 8 views
35

Acabo de entrar en una nueva empresa y gran parte de la base de código utiliza métodos de inicialización en lugar de constructores.¿Por qué usar un método de inicialización en lugar de un constructor?

struct MyFancyClass : theUberClass 
{ 
    MyFancyClass(); 
    ~MyFancyClass(); 
    resultType initMyFancyClass(fancyArgument arg1, classyArgument arg2, 
           redundantArgument arg3=TODO); 
    // several fancy methods... 
}; 

Me dijeron que esto tiene algo que ver con la sincronización. Que algunas cosas tienen que hacerse después de construcción que fallaría en el constructor. Pero la mayoría de los constructores están vacíos y realmente no veo ninguna razón para no usar constructores.

Así que me dirijo a ustedes, oh magos del C++: ¿por qué usarían un método init en lugar de un constructor?

+28

Si su propia compañía ni siquiera puede explicarlo, huelo mal código. – Mike

+0

Parece que falta un 'vacío'. – sellibitze

+2

Estoy de acuerdo con Mike. Comience a buscar un trabajo. – sbi

Respuesta

54

Como dicen "timing", supongo que es porque quieren que sus funciones init puedan invocar funciones virtuales en el objeto. Esto no siempre funciona en un constructor, porque en el constructor de la clase base, la parte de clase derivada del objeto "no existe todavía", y en particular no se puede acceder a las funciones virtuales definidas en la clase derivada. En cambio, se llama a la versión de clase base de la función, si está definida. Si no está definido (lo que implica que la función es puramente virtual), obtienes un comportamiento indefinido.

La otra razón común para las funciones init es el deseo de evitar excepciones, pero ese es un estilo de programación bastante antiguo (y si es una buena idea, es todo un argumento). No tiene nada que ver con cosas que no pueden funcionar en un constructor, sino con el hecho de que los constructores no pueden devolver un valor de error si algo falla. Entonces, en la medida en que sus colegas le hayan dado las verdaderas razones, sospecho que esto no es así.

+2

Solo una referencia de Msdn para su publicación: http://msdn.microsoft.com/en-us/library /ms182331%28VS.80%29.aspx – Tarik

+2

El primero es de hecho una razón válida. También se conoce una construcción en dos fases. Si necesito algo como esto, lo escondo en las entrañas de un objeto. Nunca expondría esto a los usuarios de mi clase. – sbi

+0

En cuanto al segundo motivo, el constructor todavía se puede usar con funciones auxiliares para administrar los recursos donde puede haber una excepción (si las excepciones están habilitadas). Si el constructor falla, no se invocará al destructor en el objeto parcialmente construido. Es por eso que a veces se ve el código de inicio/limpieza manual. –

-2

Utiliza un método de inicialización en lugar del constructor si el inicializador necesita llamarse DESPUÉS de que se haya creado la clase. Así que si la clase A fue creada como:

A *a = new A; 

y la initisalizer de la clase A requiere que ajustarse, entonces, evidentemente, necesita algo así como:

A *a = new A; 
a->init(); 
+0

No compro esto. Simplemente puede llamar a init de manera privada dentro de la última línea del constructor, entonces. (Sin embargo, no voté) – Mike

+7

Es una reformulación de la pregunta, no una respuesta. * ¿Por qué * necesitarías inicializar el objeto después de la construcción? –

+2

Si el constructor depende de la variable que se está configurando, me ha sucedido a mí. –

6

Dos razones que se me ocurre de la parte superior de mi cabeza:

  • Digamos que crear un objeto implica mucho y mucho trabajo tedioso que puede fallar de muchas y muchas maneras horribles y sutiles. Si usa un constructor corto para configurar cosas críticas que no fallarán, y luego le pide al usuario que llame a un método de inicialización para hacer el gran trabajo, al menos puede estar seguro de que tiene algún objeto creado incluso si el gran trabajo falla . Tal vez el objeto contenga información sobre la forma precisa en que falló el init, o quizás sea importante mantener objetos inicializados sin éxito por otros motivos.
  • A veces es posible que desee reinicializar un objeto mucho después de que se haya creado. De esta forma, solo se trata de llamar al método de inicialización nuevamente sin destruir y volver a crear el objeto.
+0

+1 Pensé en tu segundo argumento; de hecho, creo que esto, sin embargo, se considera una mala práctica en C++ y debería hacerse utilizando algunos objetos auxiliares. – mbq

+0

Su segunda viñeta es probablemente la razón más común por la que a veces uso las funciones de inicialización. Sí, el constructor podría llamar al init, pero eso solo hace que el código sea menos claro. – Jay

+0

Estas son las razones típicas para ver un init o una función de limpieza, pero entonces no sería una parte de la interfaz pública de clase, siendo una función de ayuda interna para evitar el código de eliminación de datos duplicados. Otras publicaciones explican las razones para hacer esto sistémicamente. –

25

Sí, puedo pensar en varios, pero en general no es una buena idea.

La mayoría de las veces, el motivo invocado es que solo informa errores a través de excepciones en un constructor (que es cierto) mientras que con un método clásico puede devolver un código de error.

Sin embargo, en código OO correctamente diseñado, el constructor es responsable de establecer las invariantes de clase. Al permitir un constructor predeterminado, permite una clase vacía, por lo tanto, debe modificar las invariantes para que se acepte tanto la clase "nula" como la clase "significativa" ...y cada uso de la clase primero debe garantizar que el objeto se haya construido correctamente ... es grosero.

Así que ahora, vamos a desenmascarar las "razones":

  • Tengo que utilizar un método virtual: utilizar el lenguaje Constructor virtual.
  • Hay mucho trabajo por hacer: ¿y qué, el trabajo se llevará a cabo de todos modos, sólo lo hacen en el constructor
  • La instalación puede fallar: lanzar una excepción
  • Quiero mantener la parte inicializado objeto: use un try/catch dentro del constructor y establezca la causa del error en un campo de objeto, no se olvide de assert al comienzo de cada método público para asegurarse de que el objeto se pueda utilizar antes de intentar usarlo.
  • quiero reiniciar mi objetivo: invocar el método de inicialización del constructor, evitará código duplicado sin dejar de tener un objeto totalmente inicializado
  • quiero reiniciar mi objeto (2): utilizar operator= (y ponerlo en práctica utilizando el modismo de copiar y cambiar si la versión generada por el compilador no se ajusta a sus necesidades).

Como dije, en general, mala idea. Si realmente quieres tener un constructor "vacío", hazlos private y usa los métodos de Builder. Es tan eficiente con NRVO ... y puede devolver boost::optional<FancyObject> en caso de que la construcción falle.

+2

me gustaría ir para el debunkment alternativa, "Quiero la posibilidad de reinicializar mi objetivo: implementar una copia-y-canje' 'operador =" –

+0

pensaba en ello, entonces se dieron cuenta de que puede haber oportunidades de optimización. Por ejemplo, pensar en el método 'assign' en un objeto' vECTOR': mediante el uso de 'assign' que vuelva a utilizar el almacenamiento ya asignados y por lo tanto no requieren una asignación de memoria falsa. Sin embargo, es complicado hacerlo correctamente, así que estoy de acuerdo en que se debe usar copiar y cambiar en el caso general. –

+0

de acuerdo, no está mal tener una función de reinicialización, simplemente no es con lo que comenzaría. Si nada más, para "oportunidad de optimización", lea "garantía de excepción básica". –

15

Otros han enumerado muchos motivos posibles (y explicaciones adecuadas de por qué la mayoría de estos no son generalmente una buena idea). Permítanme publicar un ejemplo de un (más o menos) uso válido de los métodos init, que en realidad tiene que ver con el tiempo.

En un proyecto anterior, teníamos muchas clases de servicio y objetos, cada uno de los cuales formaba parte de una jerarquía, y se cruzaban las referencias de varias maneras. Por lo general, para crear un ServiceA, necesitaba un objeto de servicio principal, que a su vez necesitaba un contenedor de servicio, que ya dependía de la presencia de algunos servicios específicos (posiblemente incluyendo ServiceA mismo) en el momento de la inicialización. El motivo fue que durante la inicialización, la mayoría de los servicios se registraron con otros servicios como detectores de eventos específicos y/o notificaron a otros servicios sobre el evento de una inicialización exitosa. Si el otro servicio no existía en el momento de la notificación, el registro no se realizó, por lo que este servicio no recibiría mensajes importantes más tarde, durante el uso de la aplicación. Con el fin de romper la cadena de dependencias circulares, tuvimos que utilizar métodos de inicialización explícitos separados de los constructores, por lo tanto efectivamente haciendo que la inicialización del servicio global sea un proceso de dos fases.

Por lo tanto, aunque este modismo no se debe seguir en general, en mi humilde opinión tiene algunos usos válidos. Sin embargo, es mejor limitar su uso al mínimo, utilizando constructores siempre que sea posible. En nuestro caso, este fue un proyecto heredado, y todavía no comprendemos completamente su arquitectura. Al menos el uso de los métodos init se limitaba a las clases de servicio: las clases regulares se inicializaban mediante constructores. Creo que podría haber una manera de refactorizar esa arquitectura para eliminar la necesidad de métodos de init de servicio, pero al menos no veía cómo hacerlo (y para ser sincero, teníamos problemas más urgentes que tratar en el momento en que estaba parte del proyecto).

+0

Parece que estaba usando singletons. (No hagas eso). –

+0

@Roger, no, estos no eran Singletons. De hecho, podría tener múltiples llamados "modelos" abiertos al mismo tiempo, cada uno con una única instancia dedicada de cada tipo de servicio. Soy muy consciente de los problemas con Singleton, pero gracias sin embargo :-) –

+2

Es sólo 2 fases de inicialización si se tiene en cuenta que el registro de los oyentes es parte de la inicialización. En una vista dinámica del mundo, donde los servicios aparecen y desaparecen, el registro de oyentes es parte de la vida del sistema. –

1

y también me gusta para fijar un ejemplo de código para responder # 1 -

Desde También MSDN dice:

Cuando se llama a un método virtual, el tipo real que ejecuta el método no se selecciona hasta el tiempo de ejecución. Cuando un constructor llama a un método virtual, es posible que el constructor para no se haya ejecutado la instancia que invoca el método .

Ejemplo: El siguiente ejemplo demuestra el efecto de la violación de esta regla. La aplicación de prueba crea una instancia de DerivedType, que hace que se ejecute su constructor de clase base (BadlyConstructedType). El constructor de BadlyConstructedType llama incorrectamente al método virtual DoSomething. Como muestra el resultado, DerivedType.DoSomething() se ejecuta y lo hace antes de que se ejecute el constructor de DerivedType.

using System; 

namespace UsageLibrary 
{ 
    public class BadlyConstructedType 
    { 
     protected string initialized = "No"; 

     public BadlyConstructedType() 
     { 
      Console.WriteLine("Calling base ctor."); 
      // Violates rule: DoNotCallOverridableMethodsInConstructors. 
      DoSomething(); 
     } 
     // This will be overridden in the derived type. 
     public virtual void DoSomething() 
     { 
      Console.WriteLine ("Base DoSomething"); 
     } 
    } 

    public class DerivedType : BadlyConstructedType 
    { 
     public DerivedType() 
     { 
      Console.WriteLine("Calling derived ctor."); 
      initialized = "Yes"; 
     } 
     public override void DoSomething() 
     { 
      Console.WriteLine("Derived DoSomething is called - initialized ? {0}", initialized); 
     } 
    } 

    public class TestBadlyConstructedType 
    { 
     public static void Main() 
     { 
      DerivedType derivedInstance = new DerivedType(); 
     } 
    } 
} 

Salida:

Calling ctor base.

Derivado ¿Se llama DoSomething - initialized? No

Llamada derivada ctor.

1

Más de un caso especial: si crea un oyente, es posible que desee hacerlo registrar en algún lugar (como con un singleton o GUI). Si lo hace durante su constructor, filtra un puntero/referencia a sí mismo que aún no es seguro, ya que el constructor no se ha completado (e incluso puede fallar por completo). Suponga el singleton que recopila todos los oyentes y les envía eventos cuando suceden eventos y eventos, y luego recorre su lista de oyentes (uno de ellos es la instancia de la que estamos hablando), para enviarles un mensaje a cada uno. Pero esta instancia todavía está a mitad de camino en su constructor, por lo que la llamada puede fallar en todo tipo de malas maneras. En este caso, tiene sentido tener el registro en una función separada, que obviamente hace no llamada desde el propio constructor (que vencería el propósito por completo), sino desde el objeto principal, después de que la construcción se haya completado.

Pero ese es un caso específico, no el general.

4

Un uso más de dicha inicialización puede estar en el grupo de objetos. Básicamente, solo solicitas el objeto del grupo. El grupo ya tendrá algunos N objetos creados que están en blanco. Es la persona que llama ahora la que puede llamar a cualquier método que le guste para configurar a los miembros. Una vez que la persona que llama ha terminado con el objeto, le dirá al grupo que lo elimine. La ventaja es que hasta que el objeto se utilice, la memoria se guardará y el llamador puede usar su propio método de miembro adecuado para inicializar el objeto. Un objeto puede estar sirviendo para muchos propósitos, pero la persona que llama puede no necesitarlo todo, y también puede no necesitar inicializar todos los miembros de los objetos.

Por lo general, piense en las conexiones de bases de datos. Una agrupación puede tener un montón de objetos de conexión, y la persona que llama puede completar el nombre de usuario, la contraseña, etc.

1

Es útil para la gestión de recursos. Supongamos que tiene clases con destructores para desasignar recursos automáticamente cuando finaliza la vida del objeto.Supongamos que también tiene una clase que contiene estas clases de recursos y las inicia en el constructor de esta clase superior. ¿Qué sucede cuando usas el operador de asignación para iniciar esta clase superior? Una vez que se copian los contenidos, la clase superior anterior queda fuera de contexto, y los destructores son llamados para todas las clases de recursos. Si estas clases de recursos tienen punteros que se copiaron durante la asignación, todos estos punteros ahora son punteros malos. Si, en su lugar, inicia las clases de recursos en una función de inicio separada en la clase superior, omite por completo el destructor de la clase de recurso, ya que el operador de asignación nunca tiene que crear y eliminar estas clases. Creo que esto es lo que significaba el requisito de "tiempo".

5

La función init() es buena cuando su compilador no admite excepciones, o su aplicación de destino no puede usar un montón (las excepciones se implementan usualmente usando un montón para crearlas y destruirlas).

Las rutinas init() también son útiles cuando debe definirse el orden de construcción. Es decir, si globalmente se asignan objetos, no se define el orden en que se invoca el constructor. Por ejemplo:

[file1.cpp] 
some_class instance1; //global instance 

[file2.cpp] 
other_class must_construct_before_instance1; //global instance 

La norma proporciona ninguna garantía de que must_construct_before_instance1 's constructor será invocado antes instancia1' s constructor. Cuando está ligado al hardware, el orden en que las cosas se inicializan puede ser crucial.

Cuestiones relacionadas