C ++在构造函数中使用此指针

时间:2010-03-25 15:40:55

标签: c++ constructor multithreading this

C++中,在类构造函数中,我启动了一个新的线程,其中this指针作为参数,将在线程中广泛使用(例如,调用成员函数)。这是件坏事吗?为什么以及后果是什么?

我的线程启动过程位于构造函数的末尾。

8 个答案:

答案 0 :(得分:19)

结果是线程可以启动并且代码将开始执行尚未完全初始化的对象。这本身就够糟糕了。

如果你正在考虑那个'好吧,它将是构造函数中的最后一个句子,它将只是构造它得到的......'再想一想:你可能派生自那个类,派生对象将不构造。

编译器可能想要使用你的代码并决定它会重新排序指令,它可能在执行代码的任何其他部分之前实际传递this指针...多线程很棘手

答案 1 :(得分:4)

主要结果是线程可能在构造函数完成之前开始运行(并使用指针),因此对象可能不处于已定义/可用状态。同样,根据线程的停止方式,它可能会在析构函数启动后继续运行,因此对象可能也不会处于可用状态。

如果您的类是基类,这尤其成问题,因为派生类构造函数甚至在构造函数退出之后才会开始运行,并且派生类析构函数将在您的类启动之前完成。此外,虚函数调用不会在构造派生类之前和破坏之后执行您认为的操作:虚拟调用“忽略”其对象的一部分不存在的类。

示例:

struct BaseThread {
    MyThread() {
        pthread_create(thread, attr, pthread_fn, static_cast<void*>(this));
    }
    virtual ~MyThread() {
        maybe stop thread somehow, reap it;
    }
    virtual void id() { std::cout << "base\n"; }
};

struct DerivedThread : BaseThread {
    virtual void id() { std::cout << "derived\n"; }
};

void* thread_fn(void* input) {
    (static_cast<BaseThread*>(input))->id();
    return 0;
}

现在,如果您创建DerivedThread,那么构建它的线程和新线程之间的竞争最好,以确定调用哪个版本的id()。可能会发生更糟糕的事情,您需要仔细查看线程API和编译器。

通常不必担心这种情况的方法就是给你的线程类一个start()函数,用户在构造它之后调用它。

答案 2 :(得分:1)

取决于启动线程后您的操作。如果在线程启动后执行初始化工作,那么它可能会使用未正确初始化的数据。

您可以使用首先创建对象的工厂方法来降低风险,然后启动线程。

但我认为设计中最大的缺陷是,至少对我来说,一个不仅仅是“构造”的构造函数似乎很混乱。

答案 3 :(得分:1)

这可能有潜在危险。

在构造基类期间,对虚函数的任何调用都不会发送到更多尚未完全构造的派生类中的覆盖;一旦更多派生类的构造改变了这种变化。

如果您启动的线程调用虚函数,并且在完成类的构造时发生这种情况是不确定的,那么您可能会遇到不可预测的行为;也许是崩溃。

如果没有虚函数,如果线程只使用完全构造的类部分的方法和数据,那么行为可能是可预测的。

答案 4 :(得分:1)

我会说,作为一般规则,你应该避免这样做。但是在很多情况下你肯定可以逃脱它。我认为基本上有两件事可能出错:

  1. 在构造函数完成初始化之前,新线程可能会尝试访问该对象。您可以通过确保在启动线程之前完成所有初始化来解决此问题。但如果某人从你的班级继承了呢?你无法控制他们的构造函数会做什么。
  2. 如果你的线程无法启动会怎样?在构造函数中处理错误并不是一种干净的方法。你可以抛出异常,但这是危险的,因为它意味着你的对象的析构函数不会被调用。如果您选择不抛出异常,那么您将无法在各种方法中编写代码来检查事情是否已正确初始化。
  3. 一般来说,如果要执行复杂的,容易出错的初始化,那么最好在方法而不是构造函数中进行。

答案 5 :(得分:1)

基本上,你需要的是两阶段构造:你想在完全构造对象之后只启动你的线程John Dibling answered一个类似的(不是重复的)问题昨天详尽地讨论了两阶段的建设。你可能想看看它。

但请注意,这仍然存在线程可能在派生类的构造函数完成之前启动的问题。 (派生类的构造函数在其基类之后调用。)

所以最后最安全的事情可能是手动启动线程:

class Thread { 
  public: 
    Thread();
    virtual ~Thread();
    void start();
    // ...
};

class MyThread : public Thread { 
  public:
    MyThread() : Thread() {}
    // ... 
};

void f()
{
  MyThread thrd;
  thrd.start();
  // ...
}

答案 6 :(得分:0)

没关系,只要您可以立即开始使用该指针。如果在新线程可以使用指针之前需要构造函数的其余部分完成初始化,那么您需要进行一些同步。

答案 7 :(得分:0)

有些人认为你不应该在构造函数中使用this指针,因为该对象尚未完全形成。但是,如果您小心的话,可以在构造函数中使用它(在{body}中,甚至在初始化列表中)。

以下是始终有效的东西:构造函数的{body}(或从构造函数调用的函数)可以可靠地访问在基类中声明的数据成员和/或在构造函数中声明的数据成员类。这是因为所有这些数据成员都保证在构造函数{body}开始执行时已完全构造。

这是永远不会起作用的东西:构造函数的{body}(或从构造函数调用的函数)无法通过调用在派生类中重写的virtualmember函数来获取派生类。如果您的目标是获取派生类中的重写函数,则无法获得所需内容。请注意,您将无法在派生类中进行覆盖,而与调用虚拟成员函数的方式无关:显式使用this指针(例如,this-&gt; method()),隐式使用this指针(例如,方法) ()),甚至调用一些其他函数调用此对象上的虚拟成员函数。底线是:即使调用者正在构造派生类的对象,在基类的构造函数期间,您的对象还不是该派生类。你被警告了。

以下是有时可行的方法:如果将此对象中的任何数据成员传递给另一个数据成员的初始化程序,则必须确保已初始化其他数据成员。好消息是,您可以使用一些独立于您正在使用的特定编译器的简单语言规则来确定是否已经(或尚未)初始化其他数据成员。坏消息是您必须知道这些语言规则(例如,首先初始化基类子对象(如果您有多个和/或虚拟继承,请查找顺序!),然后在类中定义的数据成员初始化它们出现在类声明中的顺序)。如果您不了解这些规则,则不要将此对象中的任何数据成员(无论您是否明确使用此关键字)传递给任何其他数据成员的初始化程序!如果您确实了解规则,请小心。