Fil C ++ utilisant un objet fonction, comment sont appelés les destructeurs multiples mais pas les constructeurs?

15

Veuillez trouver l'extrait de code ci-dessous:

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;
}

La sortie que j'obtiens est:

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

Je suis confus comment les destructeurs avec l'adresse 0x7ffe27d1b06c et 0x2029c28 ont été appelés et aucun constructeur n'a été appelé? Alors que le premier et le dernier constructeur et destructeur sont respectivement de l'objet que j'ai créé.

SHAHBAZ
la source
11
Définissez et instrumentez également votre copy-ctor et move-ctor.
WhozCraig
Bien compris. Puisque je passe l'objet, le constructeur de copie étant appelé, ai-je raison? Mais, quand le constructeur de mouvement est-il appelé?
SHAHBAZ

Réponses:

18

Il vous manque une construction de copie et de déplacement de l'instrumentation. Une simple modification de votre programme fournira la preuve que c'est là que les constructions ont lieu.

Copier le constructeur

#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;
}

Sortie (les adresses varient)

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

Copier le constructeur et déplacer le constructeur

Si vous fournissez un ctor de déplacement, il sera préférable pour au moins une de ces copies:

#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;
}

Sortie (les adresses varient)

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

Référence enveloppé

Si vous voulez éviter ces copies, vous pouvez envelopper votre callable dans un wrapper de référence ( std::ref). Étant donné que vous souhaitez utiliser une tfois la partie de filetage terminée, cela est viable pour votre situation. Dans la pratique, vous devez être très prudent lors du filetage par rapport aux références à des objets d'appel, car la durée de vie de l'objet doit s'étendre au moins aussi longtemps que le fil utilisant la référence.

#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;
}

Sortie (les adresses varient)

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

Notez que même si j'ai conservé les surcharges copy-ctor et move-ctor, aucune n'a été appelée, car le wrapper de référence est maintenant la chose copiée / déplacée; pas la chose à laquelle il fait référence. De plus, cette approche finale fournit ce que vous recherchiez probablement; t.xretour mainest, en fait, modifié en 11. Ce n'était pas dans les tentatives précédentes. Je ne saurais trop insister sur cela, cependant: soyez prudent . La durée de vie des objets est critique .


Bouge, et rien que

Enfin, si vous n'avez aucun intérêt à conserver tcomme vous l'avez fait dans votre exemple, vous pouvez utiliser la sémantique de déplacement pour envoyer l'instance directement au thread, en cours de route.

#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;
}

Sortie (les adresses varient)

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

Ici, vous pouvez voir que l'objet est créé, la référence rvalue à ladite-même puis envoyée directement à std::thread::thread(), où il est à nouveau déplacé vers son lieu de repos final, propriété du thread à partir de ce point. Aucun copieur n'est impliqué. Les dtors réels sont contre deux obus et l'objet concret de destination finale.

WhozCraig
la source
5

Quant à votre question supplémentaire publiée dans les commentaires:

Quand le constructeur de mouvement est-il appelé?

Le constructeur de std::threadfirst crée une copie de son premier argument (par decay_copy) - c'est là que le constructeur de copie est appelé. (Notez que dans le cas d'un argument rvalue , tel que thread t1{std::move(t)};or thread t1{tFunc{}};, le constructeur de déplacement serait appelé à la place.)

Le résultat de decay_copyest un temporaire qui réside sur la pile. Cependant, comme il decay_copyest effectué par un thread appelant , ce temporaire réside sur sa pile et est détruit à la fin du std::thread::threadconstructeur. Par conséquent, le temporaire lui-même ne peut pas être utilisé directement par un nouveau thread créé.

Pour «passer» le foncteur au nouveau thread, un nouvel objet doit être créé ailleurs , et c'est là que le constructeur de déplacement est appelé. (S'il n'existait pas, le constructeur de copie serait appelé à la place.)


Notez que nous pouvons nous demander pourquoi la matérialisation temporaire différée n'est pas appliquée ici. Par exemple, dans cette démo en direct , un seul constructeur est invoqué au lieu de deux. Je crois que certains détails d'implémentation interne de l'implémentation de la bibliothèque C ++ Standard entravent l'optimisation à appliquer pour le std::threadconstructeur.

Daniel Langr
la source