C++ std::使用类参数进行线程初始化会导致类对象被复制多次

C++ std::使用类参数进行线程初始化会导致类对象被复制多次,c++,multithreading,class,c++11,stdthread,C++,Multithreading,Class,C++11,Stdthread,看起来,如果您创建一个类的对象,并将其传递给std::thread初始化构造函数,那么该类对象的构造和销毁次数总共将达到4次。我的问题是:你能一步一步地解释一下这个程序的输出吗?为什么在这个过程中要多次构造、复制和销毁类 示例程序: #include <iostream> #include <cstdlib> #include <ctime> #include <thread> class sampleClass { public:

看起来,如果您创建一个类的对象,并将其传递给std::thread初始化构造函数,那么该类对象的构造和销毁次数总共将达到4次。我的问题是:你能一步一步地解释一下这个程序的输出吗?为什么在这个过程中要多次构造、复制和销毁类

示例程序:

#include <iostream>  
#include <cstdlib>
#include <ctime>
#include <thread>

class sampleClass {
public:
    int x = rand() % 100;
    sampleClass() {std::cout << "constructor called, x=" << x <<     std::endl;}
    sampleClass(const sampleClass &SC) {std::cout << "copy constructor called, x=" << x << std::endl;}
    ~sampleClass() {std::cout << "destructor called, x=" << x << std::endl;}
    void add_to_x() {x += rand() % 3;}
};

void sampleThread(sampleClass SC) {
    for (int i = 0; i < 1e8; ++i) { //give the thread something to do
        SC.add_to_x();
    }
    std::cout << "thread finished, x=" << SC.x << std::endl;
}

int main(int argc, char *argv[]) {
    srand (time(NULL));
    sampleClass SC;
    std::thread t1 (sampleThread, SC);
    std::cout << "thread spawned" << std::endl;
    t1.join();
    std::cout << "thread joined" << std::endl;
    return 0;
}
使用GCC4.9.2编译,无优化

int main(int argc, char *argv[]) {
    sampleClass SC; // default constructor
    std::thread t1 (sampleThread, SC); // Two copies inside thread constructor,
                                       //use std::ref(SC) to avoit it
    //..
}

void sampleThread(sampleClass SC) { // copy SC: pass by ref to avoid it
                                // but then modifications are for original and not the copy
  // ...
}

在后台有很多复制/移动操作。但是请注意,在调用线程构造函数时,不会调用复制构造函数或移动构造函数

考虑这样一个函数:

template<typename T> void foo(T&& arg);
此构造函数应用相同的语法,因此参数永远不会复制/移动到构造函数参数中

下面的代码包含一个示例

#include <iostream>
#include <thread>

class Foo{
public:
    int id;

    Foo()
    {
        id = 1;
        std::cout << "Default constructor, id = " << id << std::endl;
    }

    Foo(const Foo& f)
    {
        id = f.id + 1;
        std::cout << "Copy constructor, id = " << id << std::endl;
    }

    Foo(Foo&& f)
    {
        id = f.id;
        std::cout << "Move constructor, id = " << id << std::endl;
    }
};

void doNothing(Foo f)
{
    std::cout << "doNothing\n";
}

template<typename T>
void test(T&& arg)
{
}

int main()
{
    Foo f; // Default constructor is called

    test(f); // Note here that we see no prints from copy/move constructors

    std::cout << "About to create thread object\n";
    std::thread t{doNothing, f};
    t.join();

    return 0;
}
  • 首先,创建对象
  • 我们调用测试函数只是为了确保没有任何事情发生,没有构造函数调用
  • 因为我们将l值传递给线程构造函数,所以参数具有l值引用类型,因此对象(使用复制构造函数)被复制到线程对象中
  • 对象被移动到基础线程中(由线程对象管理)
  • 对象最终移动到线程函数doNothing的参数中

我编辑了这个示例,int x被初始化为rand()%100,这样你就可以看到什么时候构造/销毁哪个对象。据我所见,你构造一次你的对象,然后把它传递给线程对象,线程对象在接受你的参数时再次构造它,然后线程对象把它传递给你的函数,因为它按值获取类,所以copy再次构造参数。顺便说一句,尝试添加一个移动构造函数,然后看看会发生什么!我想所有这些复制都是因为您没有移动构造函数!还可以查一下“C++规则五”,为什么在编译时不使用优化?我确信编译器会优化掉这些冗余副本。@adam10603 Move似乎不是一个好的解决方案,因为在目标应用程序中,我生成了几种相同类型的线程,每个线程都有一个相同类的副本(副本需要是独立的)。据我所知,移动会使原始对象处于不确定状态。至于优化-示例没有完全优化,因此编译器不会过多地处理代码,不管怎样,即使使用
-O3
,输出也是相同的。我的意思是,您将创建与线程数量相同的对象,然后将它们移动到线程中,为我的应用程序std::ref避免复制不是一个选项,因为我正在启动更多同类线程,在我这样做之前,要设置要传递的类需要做很多工作,所以我有意复制并构造它。然而,我仍然不清楚为什么
std::threadt1(sampleThread,SC)
创建2个副本?@MarcinL:,请参见
3)
detacy\u copy
在返回中构造一个副本(因为在您的情况下没有移动),在调用它的地方构造另一个副本。我认为一些实现可能会将副本的数量减少到一个。那么,假设您希望生成多个线程,其中包含一个大型类的独立副本(由于其他原因需要提前创建),是否可以避免内存泄漏?如果是故意创建了两个拷贝而不是一个,那么就没有解决方案,我会错过什么吗?@MarcinL:目前,没有memleaks。您可能有一个移动构造函数,它可能比副本便宜。否则,您可以将
sampleThread
更改为按常量引用,并在
sampleThread
中或在创建线程之前进行复制(使用
std::vector
。。@MarcinL:您似乎有一些基本的误解。为什么你认为一个对象有两个副本会泄漏内存?
std::thread
在它的主体中做传递参数的内部副本。是的,这就是我说的:对象被复制(使用复制构造函数)到thread对象中。
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
#include <iostream>
#include <thread>

class Foo{
public:
    int id;

    Foo()
    {
        id = 1;
        std::cout << "Default constructor, id = " << id << std::endl;
    }

    Foo(const Foo& f)
    {
        id = f.id + 1;
        std::cout << "Copy constructor, id = " << id << std::endl;
    }

    Foo(Foo&& f)
    {
        id = f.id;
        std::cout << "Move constructor, id = " << id << std::endl;
    }
};

void doNothing(Foo f)
{
    std::cout << "doNothing\n";
}

template<typename T>
void test(T&& arg)
{
}

int main()
{
    Foo f; // Default constructor is called

    test(f); // Note here that we see no prints from copy/move constructors

    std::cout << "About to create thread object\n";
    std::thread t{doNothing, f};
    t.join();

    return 0;
}
Default constructor, iCount = 1
About to create thread object
Copy constructor, id = 2
Move constructor, id = 2
Move constructor, id = 2
doNothing