Wątek C ++ wykorzystujący obiekt funkcji, w jaki sposób wywoływanych jest wiele destruktorów, ale nie konstruktorów?

15

Fragment kodu znajduje się poniżej:

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

Otrzymuję wynik:

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

Jestem zdezorientowany, w jaki sposób wywołano destruktory o adresach 0x7ffe27d1b06c i 0x2029c28 i nie wywołano żadnych konstruktorów? Natomiast pierwszy i ostatni konstruktor i destruktor są odpowiednio obiektu, który utworzyłem.

SHAHBAZ
źródło
11
Zdefiniuj i instrumentuj również swojego copy-ctor i move-ctor.
WhozCraig
Dobrze zrozumiane. Skoro mija obiekt, który wywołuje konstruktor kopiowania, czy mam rację? Ale kiedy nazywa się konstruktor ruchu?
SHAHBAZ

Odpowiedzi:

18

Brakuje instrumentalnej konstrukcji kopii i konstrukcji przenoszenia. Prosta modyfikacja programu dostarczy dowodów na to, gdzie mają miejsce konstrukcje.

Kopiuj konstruktora

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

Wyjście (adresy się różnią)

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

Kopiuj konstruktora i przenieś konstruktora

Jeśli podasz ctor ruchu, będzie on preferowany dla co najmniej jednego z tych kopii:

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

Wyjście (adresy się różnią)

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

Referencje zapakowane

Jeśli chcesz ominąć te kopie, możesz owinąć swój program wywołujący w wrapper referencyjny ( std::ref). Ponieważ chcesz wykorzystać tpo zakończeniu gwintowania, jest to opłacalne w twojej sytuacji. W praktyce należy zachować szczególną ostrożność podczas łączenia wątków z odwołaniami do obiektów wywołujących, ponieważ czas życia obiektu musi trwać co najmniej tak długo, jak wątek wykorzystujący odwołanie.

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

Wyjście (adresy się różnią)

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

Zauważ, że chociaż zachowałem przeciążenia copy-ctor i move-ctor, żadne z nich nie zostało wywołane, ponieważ opakowanie referencyjne jest teraz przedmiotem kopiowania / przenoszenia; nie rzecz, do której się odnosi. Ponadto to końcowe podejście zapewnia to, czego prawdopodobnie szukałeś; t.xz powrotem mainjest w rzeczywistości zmodyfikowany do 11. Nie było w poprzednich próbach. Nie mogę tego jednak wystarczająco podkreślić: bądź ostrożny . Żywotność obiektu ma kluczowe znaczenie .


Rusz się i nic więcej

Wreszcie, jeśli nie jesteś zainteresowany zachowaniem, ttak jak w przykładzie, możesz użyć semantyki move, aby wysłać instancję bezpośrednio do wątku, poruszając się po drodze.

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

Wyjście (adresy się różnią)

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

Tutaj możesz zobaczyć, że obiekt jest tworzony, odwołanie do wartości tego samego - a następnie wysyłane prosto do std::thread::thread(), gdzie jest ponownie przenoszone do ostatecznego miejsca spoczynku, którego właścicielem jest wątek od tego momentu. Zaangażowani są nie kopiownicy. Rzeczywiste kropki opierają się o dwie skorupy i konkretny obiekt docelowy.

WhozCraig
źródło
5

Co do twojego dodatkowego pytania zamieszczonego w komentarzach:

Kiedy wywoływany jest konstruktor ruchu?

Konstruktor std::threadfirst tworzy kopię swojego pierwszego argumentu (by decay_copy) - to tam wywoływany jest konstruktor kopii . (Należy pamiętać, że w przypadku rvalue argumentów, takich jak thread t1{std::move(t)};lub thread t1{tFunc{}};, konstruktor posunięcie byłoby nazwać zamiast).

Wynikiem decay_copyjest tymczasowe, które znajduje się na stosie. Ponieważ jednak decay_copyjest wykonywany przez wątek wywołujący , ten tymczasowy rezyduje na stosie i jest niszczony na końcu std::thread::threadkonstruktora. W związku z tym sam plik tymczasowy nie może być bezpośrednio używany przez nowo utworzony wątek.

Aby „przekazać” funktor do nowego wątku, nowy obiekt musi zostać utworzony gdzieś indziej i tutaj wywoływany jest konstruktor ruchu . (Gdyby nie istniał, zamiast tego wywoływany byłby konstruktor kopii).


Zauważmy, że możemy się zastanawiać, dlaczego nie zastosowano tutaj odroczonej tymczasowej materializacji . Na przykład w tym demo na żywo wywoływany jest tylko jeden konstruktor zamiast dwóch. Uważam, że niektóre wewnętrzne szczegóły implementacji biblioteki standardowej C ++ utrudniają optymalizację, którą należy zastosować dla std::threadkonstruktora.

Daniel Langr
źródło