Поток C ++, использующий объект функции, как называются множественные деструкторы, но не конструкторы?

15

Пожалуйста, найдите фрагмент кода ниже:

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

Вывод, который я получаю:

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

Я запутался, как вызывались деструкторы с адресами 0x7ffe27d1b06c и 0x2029c28, а конструкторы не назывались? В то время как первый и последний конструктор и деструктор соответственно относятся к объекту, который я создал.

Shahbaz
источник
11
Определите и оформите ваш copy-ctor и move-ctor.
WhozCraig
Хорошо понимал. Так как я передаю объект вызываемому конструктору копирования, я прав? Но когда вызывается конструктор перемещения?
Шахбаз

Ответы:

18

Вам не хватает инструментов копирования-строительства и перемещения конструкции. Простая модификация вашей программы предоставит свидетельство того, где происходят конструкции.

Копировать конструктор

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

Вывод (адреса меняются)

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

Скопируйте конструктор и переместите конструктор

Если вы предоставите ctor для перемещения, предпочтительнее будет хотя бы одна из этих копий:

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

Вывод (адреса меняются)

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

Ссылка завернута

Если вы хотите избежать этих копий, вы можете обернуть свой отзыв в справочную оболочку ( std::ref). Так как вы хотите использовать tпосле завершения потоковой обработки, это подходит для вашей ситуации. На практике вы должны быть очень осторожны при работе с ссылками на объекты вызова, так как время жизни объекта должно увеличиваться как минимум до тех пор, пока поток использует ссылку.

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

Вывод (адреса меняются)

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

Обратите внимание, что хотя я сохранил перегрузки copy-ctor и move-ctor, ни один из них не был вызван, так как справочная обертка теперь является объектом копирования / перемещения; не то, на что оно ссылается. Кроме того, этот последний подход дает то, что вы, вероятно, искали; t.xобратно main, на самом деле, изменен на 11. Это было не в предыдущих попытках. Однако не могу этого подчеркнуть: будьте осторожны . Время жизни объекта имеет решающее значение .


Двигаться, и ничего, кроме

Наконец, если вы не заинтересованы в сохранении, tкак в своем примере, вы можете использовать семантику перемещения, чтобы отправить экземпляр прямо в поток, двигаясь по пути.

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

Вывод (адреса меняются)

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

Здесь вы можете видеть, что объект создан, ссылка rvalue на said-same затем отправляется прямо туда std::thread::thread(), где он снова перемещается в свое последнее место отдыха, принадлежащее потоку с этой точки вперед. Копировщики не участвуют. Фактические дторы против двух оболочек и конкретного объекта конечного назначения.

WhozCraig
источник
5

Что касается вашего дополнительного вопроса, размещенного в комментариях:

Когда вызывается конструктор перемещения?

Конструктор std::threadfirst создает копию своего первого аргумента (by decay_copy) - именно там вызывается конструктор copy . (Обратите внимание, что в случае аргумента rvalue , такого как thread t1{std::move(t)};или thread t1{tFunc{}};, вместо этого будет вызываться конструктор перемещения .)

Результатом decay_copyявляется временный объект, который находится в стеке. Однако, поскольку decay_copyвыполняется вызывающим потоком , этот временный объект находится в его стеке и уничтожается в конце std::thread::threadконструктора. Следовательно, сам временный объект не может использоваться новым созданным потоком напрямую.

Чтобы «передать» функтор в новый поток, новый объект должен быть создан где-то еще , и именно здесь вызывается конструктор перемещения . (Если он не существует, вместо этого будет вызван конструктор копирования.)


Обратите внимание, что мы можем задаться вопросом, почему отсроченная временная материализация здесь не применяется. Например, в этом демонстрационном примере вызывается только один конструктор вместо двух. Я полагаю, что некоторые внутренние детали реализации библиотеки C ++ Standard препятствуют оптимизации, применяемой для std::threadконструктора.

Даниэль Лангр
источник