在构造函数完成之前调用析构函数是否合法?

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,那么这是合法的。但是构造函数在多个方面都是特殊的,所以我不确定这是否有效。
此外,如果这是不合法的,我不确定如何解决它,除了在特殊的初始化函数中生成线程,该函数必须在对象构造后调用,但我真的想避免这样做。
附言:我正在寻找 C++03 的答案(此项目受到较旧编译器的限制)。此示例中的 std::thread 仅用于说明目的。

1
t的构造函数完全执行之前,你可能会调用它的析构函数。这肯定是未定义行为。 - mfontanini
关于解决这个问题,怎么样只允许从一个工厂函数构建 foo,该函数创建一个 shared_ptr<foo>,你可以将其交给线程?(使用 shared_from_this 会更好,但那不是一个选项) - user786653
在这个例子中,更令人担忧的是线程对象将在调用函数后被销毁(自我销毁)。因此,在“结束线程”之后需要执行的任何工作都将在一个已经死亡的对象上执行,这肯定是未定义行为(在这种情况下很可能会崩溃)。您始终可以控制自己的类实现,以允许安全地进行delete this;,因为您之后不会访问任何数据成员,但是您无法“欺骗”第三方类对象(std::thread)像那样销毁自己,并期望事情表现良好。 - Mikael Persson
我建议取消对我的答案的采纳。虽然我已经尽力回答了它,但是它在不正确的情况下被接受了,而且我自己也不确定它是否完全正确。 - Joseph Mansfield
4个回答

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

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

现在我们发现,在构造函数结束后,类型为foo的对象的生命周期开始(§3.8/1):

类型为T的对象的生命周期在以下情况下开始:

  • 已获得适当对齐和大小的用于类型T的存储;并且
  • 如果对象具有非平凡的初始化,则其初始化已经完成。
如果类型foo有非平凡的析构函数,则在构造函数结束之前将对象删除是未定义的行为(§3.8/5):

在对象的生命周期开始之前但在分配了对象将占据的存储空间之后[...],任何指向对象将被或将被定位的存储位置的指针都可以使用,但只能以有限的方式使用。对于正在构建或销毁的对象,请参见12.7。否则,[...]

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

构造函数或析构函数(12.6.2)可以调用成员函数,包括虚函数(10.3)。

这意味着在构造对象时调用self_destruct是可以的。但是,此节未明确讨论在构造对象时销毁对象的情况。因此,建议我们查看delete-expression的操作。
首先,它“将调用正在删除的对象[...]的析构函数(如果有的话)。” 析构函数是成员函数的特殊情况,因此调用它是可以的。然而,§12.4 Destructors没有说明在构造过程中调用析构函数是否定义良好。没有希望。
其次,“delete-expression将调用一个释放函数”,“释放函数应释放由指针引用的存储空间”。同样地,在当前正在使用对象的存储空间时做这件事情什么也没有说。
因此,我认为这是未定义的行为,因为标准没有非常精确地定义它。
仅供参考:类型为foo的对象的生命周期在析构函数调用开始时结束,因为它具有非平凡的析构函数。因此,如果delete this;在对象构造完成之前发生,则其生命周期在开始之前就已经结束了。这是玩火。

1
这个论点被有力地禁止了,但我认为即使有一个微不足道的析构函数,它也可能无效,因为标准没有定义除非对象在任何非静态成员函数调用的持续时间内都存在,否则行为是什么。 - Ben Voigt
@BenVoigt:我不会说“令人信服”……;-) - Cheers and hth. - Alf
@Cheersandhth.-Alf 我该如何说服你呢?:( - Joseph Mansfield
很遗憾,您选择了错误的答案(§3.8/5条款不适用),我必须进行负投票。在SO中,这是唯一可见的指示错误的方式。 - Cheers and hth. - Alf
@Cheersandhth.-Alf我已经尽力了,但我认为这只是定义不清楚。虽然我希望我能取消我的接受。 - Joseph Mansfield
显示剩余3条评论

2

我敢说这是非法的(尽管它显然仍可以在某些编译器上正常工作)。

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

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

你的类foo没有基类,因此没有祖先,因此this没有类型,你的对象在调用delete时实际上不是一个对象。但即使有一个基类,该对象也将是非最派生对象(仍然是非法的),并且将调用错误的构造函数。


1

delete this; 在大多数平台上实际上可以正常工作;有些甚至可以保证作为特定于平台的扩展的正确行为。但是如果我没记错,根据标准,它并没有被很好地定义。

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

标准的第3.8段第6条规定,在调用非静态成员函数期间,如果对象不处于活动状态,则其行为未定义:

同样地,在对象的生命周期开始之前但在分配对象将占用的存储空间之后,或者在对象的生命周期结束之后并在重新使用或释放对象所占用的存储空间之前,任何引用原始对象的glvalue只能以有限的方式使用。有关正在构建或销毁的对象,请参见12.7。否则,这样的glvalue指的是已分配的存储空间,并且使用不依赖于其值的glvalue属性是定义良好的。如果程序满足以下条件,则行为未定义:
- 对此类glvalue应用lvalue-to-rvalue转换, - 使用glvalue访问对象的非静态数据成员或调用非静态成员函数, - 将glvalue隐式转换为基类类型的引用,或 - 将glvalue用作static_cast的操作数,除非转换最终为cvchar&cvunsigned char&,或 - 将glvalue用作dynamic_cast的操作数或typeid的操作数。
针对这个特定情况(删除正在构建的对象),我们在5.3.5p2节中找到了以下内容:
“...在第一种选择(删除对象)中,delete的操作数的值可以是空指针值、由先前的new-expression创建的非数组对象的指针,或表示此类对象的基类的子对象的指针(第10条)。如果不是,则行为未定义。在第二种选择(删除数组)中,delete的操作数的值可以是空指针值或由先前的数组new-expression产生的指针值。如果不是,则行为未定义。”
这个要求没有得到满足。*this不是一个过去时态由new-expression创建的对象,而是一个正在被创建的对象(现在进行时)。这种解释也得到了数组情况的支持,在该情况下,指针必须是先前new-expression的结果...但是new-expression尚未完全评估;它还不是previous,也没有结果。

没有特殊规定来计算 2+2,也没有特殊规则允许使用 delete this。但是,有规则禁止访问被销毁的对象中的内容,因此在使用 delete this 后需要避免这种情况发生。大多数GUI框架都会使用 delete this,因此这是非常关键的功能。 - Cheers and hth. - Alf
@Alf:我引用的规则可能被解释为禁止在对象销毁时与非静态成员函数的调用重叠。(函数调用是控制转移的瞬间,还是函数执行的持续时间?) - Ben Voigt
@BenVoigt 也许我误解了这段文字,但它似乎只谈到了指向未在构造或销毁过程中的对象的glvalues,而我们所讨论的对象正在构造中。 - Joseph Mansfield
@sftrabbit:我试图探讨一个更广泛的话题:“当对象成员正在执行时,我能否允许删除对象?”在调用删除器之后,对象不再处于构造状态,它已经死亡,这个规则同样适用。 - Ben Voigt
@BenVoight 但是在那个时候,glvalue已经被用来调用非静态成员函数了,不是吗?是的,执行发生在销毁之后,但这是一个问题吗?这里只是说调用该函数会导致未定义的行为。 - Joseph Mansfield
显示剩余4条评论

1

在构造函数成功完成之前,对象并不存在。部分原因是构造函数可能会从派生类的构造函数中调用。在这种情况下,您肯定不希望通过显式析构函数调用销毁已构造的子对象,更不要在未完全构造的对象(的一部分)上调用delete this而导致未定义行为。


关于对象存在的标准语言,重点如下:

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

在这种情况下,构造函数即使是用户自定义的,也被视为是非平凡的。


1
请引用出处。祝好和希望有所帮助。 - Lightness Races in Orbit
@LightnessRacesinOrbit:我添加了我找到的相关标准,但我认为这是一种情况,即某些事情因未被明确允许而成为UB(即§12.7未明确允许)。这些情况通常需要大量工作来确定。很抱歉,但我的答案更多地是手挥之间的水平,我没有时间进行扩展研究 :-( - Cheers and hth. - Alf
我非常确定这句话涵盖了它。 - Lightness Races in Orbit
@BenVoigt:不,我没有说过你误导性地概括了我的话。但是,在涉及到指向正在构建中的对象的指针时,§3.8/5中有一些模糊的措辞,要“参见§12.7”。在该部分中,明确列出了一些可以执行的操作(例如typeiddynamic_cast)。尽管如此(这应该解决了您对为什么情况不同感到困惑的问题),正如我对Tomalak所指出的,我认为这个答案并不是非常可靠的。可能标准在这里并不完全清晰,如果是这样,我们就需要进行解释... - Cheers and hth. - Alf

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接