C ++ thread menggunakan objek fungsi, bagaimana beberapa destruktor dipanggil tetapi bukan konstruktor?

15

Temukan cuplikan kode di bawah ini:

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

Output yang saya dapatkan adalah:

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

Saya bingung bagaimana destruktor dengan alamat 0x7ffe27d1b06c dan 0x2029c28 dipanggil dan tidak ada konstruktor yang dipanggil? Sedangkan konstruktor dan destruktor pertama dan terakhir masing-masing adalah dari objek yang saya buat.

SHAHBAZ
sumber
11
Tentukan dan instrument juga copy-ctor dan move-ctor Anda.
WhozCraig
Dimengerti. Karena saya melewati objek yang disebut copy constructor, apakah saya benar? Tetapi, kapan pemindah konstruktor dipanggil?
SHAHBAZ

Jawaban:

18

Anda kehilangan instruksi pembuatan salinan dan memindahkan konstruksi. Modifikasi sederhana untuk program Anda akan memberikan bukti di mana konstruksi berlangsung.

Salin Pembuat

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

Output (alamat bervariasi)

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

Salin Konstruktor dan Pindahkan Konstruktor

Jika Anda memberikan pemindah dokumen, itu akan lebih disukai untuk setidaknya satu dari salinan tersebut:

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

Output (alamat bervariasi)

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

Referensi Dibungkus

Jika Anda ingin menghindari salinan itu, Anda dapat membungkus callable Anda dalam pembungkus referensi ( std::ref). Karena Anda ingin memanfaatkan tsetelah bagian threading selesai, ini layak untuk situasi Anda. Dalam praktiknya, Anda harus sangat berhati-hati ketika melakukan threading terhadap referensi untuk memanggil objek, karena umur objek harus meluas setidaknya selama utas menggunakan referensi.

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

Output (alamat bervariasi)

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

Catat meskipun saya menyimpan copy-ctor dan move-ctor kelebihan, tidak ada yang dipanggil, karena pembungkus referensi sekarang adalah hal yang sedang disalin / dipindahkan; bukan hal itu referensi. Juga, pendekatan akhir ini memberikan apa yang mungkin Anda cari; t.xkembali main, pada kenyataannya, dimodifikasi menjadi 11. Itu tidak dalam upaya sebelumnya. Namun, tidak dapat cukup menekankan hal ini: berhati-hatilah melakukan ini . Masa objek sangat penting .


Bergerak, Dan Tidak Ada Yang Lain

Akhirnya, jika Anda tidak tertarik untuk mempertahankan tseperti pada contoh Anda, Anda dapat menggunakan semantik bergerak untuk mengirim instance langsung ke utas, bergerak di sepanjang jalan.

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

Output (alamat bervariasi)

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

Di sini Anda dapat melihat objek dibuat, referensi nilai untuk kata-sama kemudian dikirim langsung ke std::thread::thread(), di mana ia dipindahkan lagi ke tempat peristirahatan terakhir, yang dimiliki oleh utas dari titik itu ke depan. Tidak ada copy-ctors yang terlibat. Para pelaku sebenarnya menentang dua selongsong dan objek beton tujuan akhir.

WhozCraig
sumber
5

Adapun pertanyaan tambahan Anda diposting di komentar:

Kapan move constructor dipanggil?

Konstruktor std::threadpertama membuat salinan argumen pertamanya (oleh decay_copy) - di situlah copy konstruktor dipanggil. (Perhatikan bahwa dalam kasus argumen rvalue , seperti thread t1{std::move(t)};atau thread t1{tFunc{}};, pindahkan konstruktor akan dipanggil sebagai gantinya.)

Hasilnya decay_copyadalah sementara yang berada di tumpukan. Namun, karena decay_copydilakukan oleh utas panggilan , ini sementara berada di tumpukan dan dihancurkan di ujung std::thread::threadkonstruktor. Akibatnya, sementara itu sendiri tidak dapat digunakan oleh utas yang baru dibuat secara langsung.

Untuk "meneruskan" functor ke utas baru, objek baru harus dibuat di tempat lain , dan ini adalah tempat pemindah konstruktor dipanggil. (Jika tidak ada, copy constructor akan dipanggil sebagai gantinya.)


Perhatikan bahwa kita mungkin bertanya-tanya mengapa materialisasi sementara yang ditangguhkan tidak diterapkan di sini. Misalnya, dalam demo langsung ini , hanya satu konstruktor yang dipanggil, bukan dua. Saya percaya bahwa beberapa detail implementasi internal dari implementasi C ++ Standard library menghambat optimasi yang akan diterapkan untuk std::threadkonstruktor.

Daniel Langr
sumber