2010-03-25 14 views
25

En , durante un constructor de clase, comencé un nuevo subproceso con el puntero this como un parámetro que se usará ampliamente en el subproceso (por ejemplo, llamar funciones de miembro). ¿Es eso algo malo que hacer? ¿Por qué y cuáles son las consecuencias?C++ usando este puntero en los constructores

Mi proceso de inicio de subprocesos se encuentra al final del constructor.

Respuesta

18

La consecuencia es que el subproceso se puede iniciar y el código comenzará a ejecutar un objeto aún no completamente inicializado. Lo cual es suficientemente malo en sí mismo.

Si está considerando que 'bueno, será la última oración en el constructor, será casi lo más construido posible', piense de nuevo: puede derivar de esa clase, y el objeto derivado no será construido.

El compilador puede querer jugar con su código de alrededor y decidir que va a cambiar el orden de las instrucciones y que en realidad podría pasar el puntero this antes de ejecutar cualquier otra parte del código ... multihilo es complicado

+1

@gilbertc: Si B deriva de A, incluso si inicia el subproceso al final del constructor de A, todo iniciará/ejecutará desde B (vtable, variables miembro, código de constructor). ¿Podría ser esto un desagradable error de concurrencia? A primera vista, diría que sí, porque el objeto cambiará en el hilo de construcción, mientras que podría usarse en el hilo recién creado. – paercebal

1

depende de lo lo haces después de comenzar el hilo. Si realiza el trabajo de inicialización después de el hilo se ha iniciado, entonces podría utilizar datos que no se inicializaron correctamente.

Puede reducir los riesgos utilizando un método de fábrica que primero crea un objeto y luego inicia el hilo.

Pero creo que el mayor defecto en el diseño es que, al menos para mí, un constructor que hace más que "construir" parece bastante confuso.

0

Está bien, siempre y cuando pueda comenzar a usar ese puntero de inmediato. Si requiere que el resto del constructor complete la inicialización antes de que el nuevo subproceso pueda usar el puntero, entonces necesita hacer alguna sincronización.

4

La consecuencia principal es que el hilo puede comenzar a ejecutarse (y usar el puntero) antes de que el constructor haya finalizado, por lo que el objeto puede no estar en un estado definido/utilizable. Del mismo modo, dependiendo de cómo se detiene el subproceso, puede continuar ejecutándose después de que se haya iniciado el destructor y, por lo tanto, es posible que el objeto no esté en un estado utilizable.

Esto es especialmente problemático si su clase es una clase base, ya que el constructor de clase derivado ni siquiera comenzará a ejecutarse hasta que su constructor haya finalizado, y el destructor de clase derivado habrá finalizado antes de que se inicie el suyo. Además, las llamadas a funciones virtuales no hacen lo que se podría pensar antes de construir las clases derivadas y después de destruirlas: las llamadas virtuales "ignoran" las clases cuya parte del objeto no existe.

Ejemplo:

struct BaseThread { 
    MyThread() { 
     pthread_create(thread, attr, pthread_fn, static_cast<void*>(this)); 
    } 
    virtual ~MyThread() { 
     maybe stop thread somehow, reap it; 
    } 
    virtual void id() { std::cout << "base\n"; } 
}; 

struct DerivedThread : BaseThread { 
    virtual void id() { std::cout << "derived\n"; } 
}; 

void* thread_fn(void* input) { 
    (static_cast<BaseThread*>(input))->id(); 
    return 0; 
} 

Ahora bien, si se crea un DerivedThread, es una mejor una carrera entre el hilo que lo construye y el nuevo hilo, para determinar qué versión de id() es llamada. Podría ser que algo peor pueda pasar, necesitarías mirar muy de cerca tu API y compilador.

La forma habitual de no tener que preocuparse por esto es simplemente dar a su clase de subproceso una función start(), que el usuario llama después de construirla.

1

Puede ser potencialmente peligroso.

Durante la construcción de una clase base, las llamadas a funciones virtuales no se enviarán a anulaciones en más clases derivadas que aún no se han construido por completo; una vez que la construcción de las clases más derivadas cambia estos cambios.

Si el hilo que inicia llama a una función virtual y es indeterminado cuando esto sucede en relación con la finalización de la construcción de la clase, entonces es probable que tenga un comportamiento impredecible; tal vez un choque.

Sin funciones virtuales, si el subproceso solo usa métodos y datos de las partes de la clase que se han construido completamente, es probable que el comportamiento sea predecible.

1

Yo diría que, como regla general, debe evitar hacer esto. Pero ciertamente puede salirse con la suya en muchas circunstancias. Creo que básicamente hay dos cosas que pueden ir mal:

  1. El nuevo hilo podría intentar acceder al objeto antes de que el constructor termine de inicializarlo. Puede solucionar esto asegurándose de que se completa la inicialización antes de iniciar el hilo. Pero, ¿y si alguien hereda de tu clase? No tienes control sobre lo que hará su constructor.
  2. ¿Qué sucede si el hilo no se inicia? Realmente no hay una manera limpia de manejar errores en un constructor. Puede lanzar una excepción, pero esto es peligroso, ya que significa que no se llamará al destructor de su objeto. Si eliges no lanzar una excepción, entonces estás atrapado escribiendo código en tus diversos métodos para verificar si las cosas se inicializaron correctamente.

En general, si tiene una inicialización compleja y propensa a errores, entonces es mejor hacerlo en un método que en el constructor.

+0

El hecho de que no se le solicite al controlador la falla en la construcción solo es peligroso si su clase maneja directamente los recursos sin procesar, lo cual no debería hacerse de todos modos. Como las excepciones son la única forma de informar las fallas de construcción, ciertamente no deberían ser prohibidas para permitir que las clases implementadas de manera incorrecta no se filtren. Aunque estoy de acuerdo con todo lo demás que dices. – sbi

+0

No dije que debería ser prohibido, solo que es peligroso. Se necesita cierto grado de cuidado para garantizar que todo se limpie adecuadamente frente a una excepción. Esto puede ser especialmente difícil cuando tu clase depende de un código que no escribiste y no controlas. –

1

Básicamente, lo que necesita es la construcción de dos fases: Usted desea iniciar su único hilo después el objeto está totalmente construido. John Dibling answered una pregunta similar (no duplicada) ayer discutiendo exhaustivamente la construcción de dos fases. Es posible que desee echarle un vistazo.

Tenga en cuenta, sin embargo, que esto deja el problema de que el subproceso podría iniciarse antes de que se realice un constructor de clase derivado. (. Constructores clases derivadas se llaman después de las de sus clases base)

Así que al final lo más seguro es probable que iniciar manualmente el hilo:

class Thread { 
    public: 
    Thread(); 
    virtual ~Thread(); 
    void start(); 
    // ... 
}; 

class MyThread : public Thread { 
    public: 
    MyThread() : Thread() {} 
    // ... 
}; 

void f() 
{ 
    MyThread thrd; 
    thrd.start(); 
    // ... 
} 
0

Algunas personas sienten que no hay que usar el this puntero en un constructor porque el objeto aún no está completamente formado. Sin embargo, puede usar esto en el constructor (en el {cuerpo} e incluso en la lista de inicialización) si tiene cuidado.

Aquí hay algo que siempre funciona: el {body} de un constructor (o una función llamada desde el constructor) puede acceder de manera confiable a los miembros de datos declarados en una clase base y/o los miembros de datos declarados en la propia clase del constructor. Esto se debe a que se garantiza que todos esos miembros de datos se construyeron por completo cuando el {cuerpo} del constructor comienza a ejecutarse.

Aquí hay algo que nunca funciona: el {cuerpo} de un constructor (o una función llamada desde el constructor) no puede llegar a una clase derivada al llamar a una función de virtualmember anulada en la clase derivada.Si su objetivo era llegar a la función anulada en la clase derivada, no obtendrá lo que desea. Tenga en cuenta que no obtendrá la anulación en la clase derivada independientemente de cómo llame a la función de miembro virtual: utilizando explícitamente este puntero (por ejemplo, este-> método()), utilizando implícitamente este puntero (por ejemplo, método ()), o incluso llamando a alguna otra función que llame a la función de miembro virtual en su objeto. La conclusión es la siguiente: incluso si el llamador está construyendo un objeto de una clase derivada, durante el constructor de la clase base, su objeto aún no es de esa clase derivada. Usted ha sido advertido.

Aquí hay algo que a veces funciona: si transfiere alguno de los miembros de datos en este objeto al inicializador de otro miembro de datos, debe asegurarse de que el otro miembro de datos ya se haya inicializado. La buena noticia es que puede determinar si el otro miembro de datos se ha inicializado (o no) utilizando algunas reglas de lenguaje sencillas que son independientes del compilador particular que está utilizando. La mala noticia es que debe conocer esas reglas de lenguaje (p. Ej., Los sub-objetos de la clase base se inicializan primero (busque el orden si tiene herencia múltiple y/o virtual), luego los miembros de datos definidos en la clase se inicializan en el orden en que aparecen en la declaración de clase). Si no conoce estas reglas, ¡no pase ningún miembro de datos desde este objeto (independientemente de si usa o no explícitamente esta palabra clave) al inicializador de cualquier otro miembro de datos! Y si conoce las reglas, tenga cuidado.