Hilo de C ++ con objeto de función, ¿cómo se llaman los destructores múltiples pero no los constructores?

15

Encuentre el fragmento de código a continuación:

class tFunc{
    int x;
    public:
    tFunc(){
        cout<<"Constructed : "<<this<<endl;
        x = 1;
    }
    ~tFunc(){
        cout<<"Destroyed : "<<this<<endl;
    }
    void operator()(){
        x += 10;
        cout<<"Thread running at : "<<x<<endl;
    }
    int getX(){ return x; }
};

int main()
{
    tFunc t;
    thread t1(t);
    if(t1.joinable())
    {
        cout<<"Thread is joining..."<<endl;
        t1.join();
    }
    cout<<"x : "<<t.getX()<<endl;
    return 0;
}

El resultado que obtengo es:

Constructed : 0x7ffe27d1b0a4
Destroyed : 0x7ffe27d1b06c
Thread is joining...
Thread running at : 11
Destroyed : 0x2029c28
x : 1
Destroyed : 0x7ffe27d1b0a4

Estoy confundido sobre cómo se llamaron los destructores con la dirección 0x7ffe27d1b06c y 0x2029c28 y no se llamó a los constructores. Mientras que el primer y el último constructor y destructor respectivamente son del objeto que creé.

SHAHBAZ
fuente
11
Defina e instrumente también su copiadora y movidora.
WhozCraig
Bien entendido. Como paso el objeto al que se llama el constructor de copia, ¿estoy en lo correcto? Pero, ¿cuándo se llama al constructor de movimiento?
SHAHBAZ

Respuestas:

18

Te falta instrumentar la copia de construcción y mover la construcción. Una modificación simple a su programa proporcionará evidencia de que es donde se están llevando a cabo las construcciones.

Copiar constructor

#include <iostream>
#include <thread>
#include <functional>
using namespace std;

class tFunc{
    int x;
public:
    tFunc(){
        cout<<"Constructed : "<<this<<endl;
        x = 1;
    }
    tFunc(tFunc const& obj) : x(obj.x)
    {
        cout<<"Copy constructed : "<<this<< " (source=" << &obj << ')' << endl;
    }

    ~tFunc(){
        cout<<"Destroyed : "<<this<<endl;
    }

    void operator()(){
        x += 10;
        cout<<"Thread running at : "<<x<<endl;
    }
    int getX() const { return x; }
};

int main()
{
    tFunc t;
    thread t1{t};
    if(t1.joinable())
    {
        cout<<"Thread is joining..."<<endl;
        t1.join();
    }
    cout<<"x : "<<t.getX()<<endl;
    return 0;
}

Salida (las direcciones varían)

Constructed : 0x104055020
Copy constructed : 0x104055160 (source=0x104055020)
Copy constructed : 0x602000008a38 (source=0x104055160)
Destroyed : 0x104055160
Thread running at : 11
Destroyed : 0x602000008a38
Thread is joining...
x : 1
Destroyed : 0x104055020

Copiar constructor y mover constructor

Si proporciona un movimiento, se preferirá al menos una de esas copias:

#include <iostream>
#include <thread>
#include <functional>
using namespace std;

class tFunc{
    int x;
public:
    tFunc(){
        cout<<"Constructed : "<<this<<endl;
        x = 1;
    }
    tFunc(tFunc const& obj) : x(obj.x)
    {
        cout<<"Copy constructed : "<<this<< " (source=" << &obj << ')' << endl;
    }

    tFunc(tFunc&& obj) : x(obj.x)
    {
        cout<<"Move constructed : "<<this<< " (source=" << &obj << ')' << endl;
        obj.x = 0;
    }

    ~tFunc(){
        cout<<"Destroyed : "<<this<<endl;
    }

    void operator()(){
        x += 10;
        cout<<"Thread running at : "<<x<<endl;
    }
    int getX() const { return x; }
};

int main()
{
    tFunc t;
    thread t1{t};
    if(t1.joinable())
    {
        cout<<"Thread is joining..."<<endl;
        t1.join();
    }
    cout<<"x : "<<t.getX()<<endl;
    return 0;
}

Salida (las direcciones varían)

Constructed : 0x104057020
Copy constructed : 0x104057160 (source=0x104057020)
Move constructed : 0x602000008a38 (source=0x104057160)
Destroyed : 0x104057160
Thread running at : 11
Destroyed : 0x602000008a38
Thread is joining...
x : 1
Destroyed : 0x104057020

Referencia envuelta

Si desea evitar esas copias, puede envolver su llamada en un contenedor de referencia ( std::ref). Dado que desea utilizar tdespués de que se realiza la parte de subprocesamiento, esto es viable para su situación. En la práctica, debe tener mucho cuidado al enhebrar contra referencias a objetos de llamada, ya que la vida útil del objeto debe extenderse al menos tanto como el hilo que utiliza la referencia.

#include <iostream>
#include <thread>
#include <functional>
using namespace std;

class tFunc{
    int x;
public:
    tFunc(){
        cout<<"Constructed : "<<this<<endl;
        x = 1;
    }
    tFunc(tFunc const& obj) : x(obj.x)
    {
        cout<<"Copy constructed : "<<this<< " (source=" << &obj << ')' << endl;
    }

    tFunc(tFunc&& obj) : x(obj.x)
    {
        cout<<"Move constructed : "<<this<< " (source=" << &obj << ')' << endl;
        obj.x = 0;
    }

    ~tFunc(){
        cout<<"Destroyed : "<<this<<endl;
    }

    void operator()(){
        x += 10;
        cout<<"Thread running at : "<<x<<endl;
    }
    int getX() const { return x; }
};

int main()
{
    tFunc t;
    thread t1{std::ref(t)}; // LOOK HERE
    if(t1.joinable())
    {
        cout<<"Thread is joining..."<<endl;
        t1.join();
    }
    cout<<"x : "<<t.getX()<<endl;
    return 0;
}

Salida (las direcciones varían)

Constructed : 0x104057020
Thread is joining...
Thread running at : 11
x : 11
Destroyed : 0x104057020

Tenga en cuenta que aunque mantuve las sobrecargas copy-ctor y move-ctor, ninguno de los dos fue llamado, ya que el contenedor de referencia ahora es lo que se copia / mueve; No es lo que hace referencia. Además, este enfoque final ofrece lo que probablemente estaba buscando; t.xDe vuelta en main, de hecho, se modifica a 11. No fue en los intentos anteriores. Sin embargo, no puedo enfatizar esto lo suficiente: tenga cuidado al hacer esto . La vida útil del objeto es crítica .


Muévete, y nada más

Finalmente, si no tiene interés en retener tcomo lo ha hecho en su ejemplo, puede usar la semántica de movimiento para enviar la instancia directamente al hilo, moviéndose en el camino.

#include <iostream>
#include <thread>
#include <functional>
using namespace std;

class tFunc{
    int x;
public:
    tFunc(){
        cout<<"Constructed : "<<this<<endl;
        x = 1;
    }
    tFunc(tFunc const& obj) : x(obj.x)
    {
        cout<<"Copy constructed : "<<this<< " (source=" << &obj << ')' << endl;
    }

    tFunc(tFunc&& obj) : x(obj.x)
    {
        cout<<"Move constructed : "<<this<< " (source=" << &obj << ')' << endl;
        obj.x = 0;
    }

    ~tFunc(){
        cout<<"Destroyed : "<<this<<endl;
    }

    void operator()(){
        x += 10;
        cout<<"Thread running at : "<<x<<endl;
    }
    int getX() const { return x; }
};

int main()
{
    thread t1{tFunc()}; // LOOK HERE
    if(t1.joinable())
    {
        cout<<"Thread is joining..."<<endl;
        t1.join();
    }
    return 0;
}

Salida (las direcciones varían)

Constructed : 0x104055040
Move constructed : 0x104055160 (source=0x104055040)
Move constructed : 0x602000008a38 (source=0x104055160)
Destroyed : 0x104055160
Destroyed : 0x104055040
Thread is joining...
Thread running at : 11
Destroyed : 0x602000008a38

Aquí puede ver que se crea el objeto, la referencia de valor de dicho mismo luego se envía directamente a std::thread::thread(), donde se mueve nuevamente a su lugar de descanso final, propiedad del hilo desde ese punto en adelante. No hay copiadores involucrados. Los dtors reales están contra dos proyectiles y el objeto concreto de destino final.

WhozCraig
fuente
5

En cuanto a su pregunta adicional publicada en los comentarios:

¿Cuándo se llama al constructor de movimiento?

El constructor de std::threadfirst crea una copia de su primer argumento (by decay_copy), que es donde se llama al constructor de copias . (Tenga en cuenta que en caso de un argumento rvalue , como thread t1{std::move(t)};o thread t1{tFunc{}};, en su lugar, se llamaría al constructor move ).

El resultado de decay_copyes un temporal que reside en la pila. Sin embargo, dado que decay_copylo realiza un subproceso de llamada , este temporal reside en su pila y se destruye al final del std::thread::threadconstructor. En consecuencia, el temporal en sí mismo no puede ser utilizado por un nuevo hilo creado directamente.

Para "pasar" el functor al nuevo hilo, se necesita crear un nuevo objeto en otro lugar , y aquí es donde se invoca el constructor de movimiento . (Si no existiera, se invocaría el constructor de copia).


Tenga en cuenta que podríamos preguntarnos por qué la materialización temporal diferida no se aplica aquí. Por ejemplo, en esta demostración en vivo , solo se invoca un constructor en lugar de dos. Creo que algunos detalles de implementación interna de la implementación de la biblioteca estándar de C ++ dificultan la optimización que se aplicará al std::threadconstructor.

Daniel Langr
fuente