6

假设我有一个类,其构造函数产生一个删除对象的线程:

class foo {
public:
    foo() 
    : // initialize other data-members
    , t(std::bind(&foo::self_destruct, this)) 
    {}

private:
    // other data-members
    std::thread t;
    // no more data-members declared after this

    void self_destruct() { 
        // do some work, possibly involving other data-members
        delete this; 
    }
};

这里的问题是析构函数可能在构造函数完成之前被调用。在这种情况下这是合法的吗?由于t最后声明(并因此初始化),并且构造函数主体中没有代码,并且我从未打算对此类进行子类化,因此我假设对象在self_destruct被调用时已完全初始化。这个假设正确吗?

我知道如果在该语句之后不使用该语句delete this;在成员函数中是合法的。this但是构造函数在几个方面都很特殊,所以我不确定这是否有效。

此外,如果它是非法的,我不知道如何解决它,其他在构造对象后必须调用的特殊初始化函数中产生线程,我真的想避免这种情况。

PS:我正在寻找 C++03 的答案(我仅限于该项目的旧编译器)。示例中的std::thread仅用于说明目的。

4

4 回答 4

7

首先,我们看到一个类型的对象foo具有非平凡的初始化,因为它的构造函数是非平凡的(第 3.8/1 节):

如果一个对象是一个类或聚合类型,并且它或它的一个成员由一个普通默认构造函数以外的构造函数初始化,则称该对象具有非普通初始化。

现在我们看到类型对象foo的生命周期在构造函数结束后开始(第 3.8/1 节):

类型对象的生命周期T开始于:

  • 获得具有适合类型 T 的对齐和大小的存储,并且
  • 如果对象有非平凡的初始化,它的初始化就完成了。

现在,如果类型具有非平凡的析构函数(第 3.8/5 节)delete,则在构造函数结束之前对对象执行操作是未定义的行为:foo

在对象的生命周期开始之前但在分配对象将占用的存储空间之后 [...] 任何指向对象将要或曾经位于的存储位置的指针都可以使用,但只能以有限的方式使用。对于正在建造或破坏的物体,见 12.7。除此以外, [...]

因此,由于我们的对象正在构建中,我们来看看 §12.7:

成员函数,包括虚函数 (10.3),可以在构造或销毁 (12.6.2) 期间调用。

这意味着self_destruct在构造对象时调用它是可以的。但是,本节没有具体说明在构造对象时销毁它。所以我建议大家看一下操作delete-expression

首先,它“将为正在删除的对象 [...] 调用析构函数(如果有的话)”。析构函数是成员函数的一种特殊情况,所以调用它就可以了。但是,§12.4 析构函数没有说明在构造过程中调用析构函数时是否定义良好。这里没有运气。

其次,“ delete-expression将调用释放函数”和“释放函数将释放指针引用的存储空间”。再一次,没有提到对当前正在使用的存储进行此操作作为正在构建的对象。

所以我认为这是未定义的行为,因为标准没有非常精确地定义它。

请注意:类型对象的生命周期在析构函数调用开始时foo 结束,因为它有一个非平凡的析构函数。所以如果delete this;发生在对象构造结束之前,它的生命周期在它开始之前就结束了。这是在玩火。

于 2013-01-21T17:58:41.137 回答
2

我敢说它被明确定义为非法(尽管它可能显然仍然适用于某些编译器)。

这与“构造函数抛出异常时未调用析构函数”的情况有些相同。

根据标准,删除表达式会破坏由新表达式(5.3.2)创建的最派生对象(1.8)或数组。在构造函数结束之前,对象不是最派生的对象,而是其直接祖先类型的对象。

您的类foo没有基类,因此没有祖先,this因此没有类型,并且您的对象在delete被调用时根本不是真正的对象。但即使有一个基类,该对象也将是一个非派生对象(仍然使其非法),并且会调用错误的构造函数。

于 2013-01-21T18:09:44.997 回答
1

delete this;在大多数平台上都能正常工作;有些甚至可以保证作为特定于平台的扩展的正确行为。但是 IIRC 根据标准并没有很好地定义它。

您所依赖的行为是,通常可以在死对象上调用非虚拟非静态成员函数,只要该成员函数实际上不访问this. 但是标准不允许这种行为;它充其量是不可移植的。

如果在调用非静态成员函数期间对象不存在,则标准的第 3.8p6 节使其行为未定义:

类似地,在对象的生命周期开始之前但在对象将占用的存储空间分配之后,或者在对象的生命周期结束之后并且在对象占用的存储空间被重用或释放之前,引用的任何泛左值可以使用原始对象,但只能以有限的方式使用。对于正在建造或破坏的物体,见 12.7。否则,这样的glvalue指的是分配的存储,并且使用不依赖于其值的glvalue的属性是明确定义的。如果出现以下情况,该程序具有未定义的行为:

  • 左值到右值的转换应用于这样的左值,
  • glvalue 用于访问非静态数据成员或调用对象的非静态成员函数,或
  • glvalue 被隐式转换为对基类类型的引用,或者
  • 泛左值用作 a 的操作数,static_cast除非最终转换为or ,或cvchar&cvunsigned char&
  • 泛左值用作 a 的操作数dynamic_cast或用作 的操作数typeid

对于这种特定情况(删除正在构建的对象),我们在第 5.3.5p2 节中找到:

...在第一种选择(删除对象)中,操作数的值可以是空指针值、指向由先前的new-expressiondelete创建的非数组对象的指针或指向表示基的子对象的指针此类对象的类别(第 10 条)。如果不是,则行为未定义。在第二种选择(删除数组)中,操作数的值可以是空指针值或由前一个数组new-expression产生的指针值。如果不是,则行为未定义。delete

不满足此要求。 *this不是由new-expression创建的过去时的对象。它是一个正在创建的对象(现在渐进式)。并且这种解释得到了数组案例的支持,其中指针必须是前一个新表达式的结果......但新表达式尚未完全评估;它不是以前的,还没有结果。

于 2013-01-21T17:53:40.083 回答
1

在构造函数成功完成之前,该对象正式不存在。部分原因是构造函数可能是从派生类的构造函数中调用的。在这种情况下,您当然不想通过显式的析构函数调用来破坏构造的子对象,甚至更不想通过调用delete this未完全构造的对象(a 的一部分)来调用 UB。


关于对象存在的标准术语,重点补充说:

C++11 §3.8/1:对象
生命周期是对象的运行时属性。如果一个对象是一个类或聚合类型,并且它或它的一个成员由一个普通默认构造函数以外的构造函数初始化,则称该对象具有非普通初始化。[注意:通过简单的复制/移动构造函数进行初始化是非平凡的初始化。—尾注] 类型 T 的对象的生命周期开始于:
— 获得类型 T 的具有适当对齐和大小的存储,并且
— 如果对象具有非平凡初始化,则其初始化完成

在这种情况下,仅由用户提供构造函数是不平凡的。

于 2013-01-21T18:00:17.753 回答