Thread C ++ usando o objeto de função, como são chamados vários destruidores, mas não os construtores?

15

Encontre o trecho de código abaixo:

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

A saída que estou recebendo é:

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

Estou confuso como os destruidores com o endereço 0x7ffe27d1b06c e 0x2029c28 foram chamados e nenhum construtor foi chamado? Enquanto o primeiro e o último construtor e destruidor, respectivamente, são do objeto que eu criei.

SHAHBAZ
fonte
11
Defina e instrumente também o seu copiador e movedor.
WhozCraig
Bem entendido. Desde que estou passando o objeto que o construtor de cópia está sendo chamado, estou correto? Mas quando é chamado o construtor de movimentação?
SHAHBAZ 27/11/19

Respostas:

18

Você está faltando instrumentar a cópia-construção e mover a construção. Uma simples modificação no seu programa fornecerá evidências de que as construções estão ocorrendo.

Copiar Construtor

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

Saída (endereços variam)

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 Construtor e Mover Construtor

Se você fornecer um gerenciador, será preferido para pelo menos uma dessas cópias:

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

Saída (endereços variam)

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

Referência agrupada

Se você deseja evitar essas cópias, pode agrupar sua chamada em um wrapper de referência ( std::ref). Como você deseja utilizar tapós a conclusão da peça de rosqueamento, isso é viável para a sua situação. Na prática, você deve ter muito cuidado ao encadear referências a objetos de chamada, pois a vida útil do objeto deve estender pelo menos o tempo que o encadeamento que utiliza a referência.

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

Saída (endereços variam)

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

Observe que, embora eu tenha mantido as sobrecargas de copy-ctor e move-ctor, nenhuma delas foi chamada, pois o wrapper de referência agora é a coisa que está sendo copiada / movida; não é a coisa que ele faz referência. Além disso, essa abordagem final fornece o que você provavelmente estava procurando; t.xde volta mainé, de fato, modificado para 11. Não estava nas tentativas anteriores. No entanto, não posso enfatizar isso o suficiente: tenha cuidado ao fazer isso . A vida útil do objeto é crítica .


Mover, e nada mais

Por fim, se você não tem interesse em reter tcomo no seu exemplo, pode usar a semântica de movimentação para enviar a instância diretamente para o encadeamento, movendo-se ao longo do caminho.

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

Saída (endereços variam)

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

Aqui você pode ver que o objeto é criado, a referência rvalue para o mesmo e enviada diretamente para std::thread::thread(), onde é movida novamente para seu local de descanso final, pertencente ao encadeamento daquele ponto em diante. Não há copiadores envolvidos. Os dtors reais são contra duas conchas e o objeto concreto de destino final.

WhozCraig
fonte
5

Quanto à sua pergunta adicional publicada nos comentários:

Quando o construtor de movimentação é chamado?

O construtor de std::threadprimeiro cria uma cópia do seu primeiro argumento (by decay_copy) - é aí que o construtor de cópia é chamado. (Observe que, no caso de um argumento rvalue , como thread t1{std::move(t)};or thread t1{tFunc{}};, move o construtor seria chamado.)

O resultado de decay_copyé um temporário que reside na pilha. No entanto, como decay_copyé executado por um encadeamento de chamada , esse temporário reside em sua pilha e é destruído no final do std::thread::threadconstrutor. Conseqüentemente, o próprio temporário não pode ser usado por um novo thread criado diretamente.

Para "passar" o functor para o novo encadeamento, um novo objeto precisa ser criado em outro lugar , e é aqui que o construtor de movimentação é chamado. (Se não existisse, o construtor de cópias seria invocado.)


Observe que podemos nos perguntar por que a materialização temporária adiada não é aplicada aqui. Por exemplo, nesta demonstração ao vivo , apenas um construtor é chamado em vez de dois. Acredito que alguns detalhes internos da implementação da biblioteca C ++ Standard dificultem a otimização a ser aplicada ao std::threadconstrutor.

Daniel Langr
fonte