假设我有一个类,其构造函数生成一个删除对象的线程:
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
,则std::thread
语句在成员函数中是合法的。但构造函数在几个方面都很特殊,所以我不确定这是否有效。
另外,如果它是非法的,我不知道如何解决它,其他产生线程的特殊初始化函数必须在构造对象后调用,我真的很想避免。
P.S。:我正在寻找C ++ 03的答案(我只限于这个项目的旧编译器)。示例中的{{1}}仅用于说明目的。
答案 0 :(得分:7)
首先,我们看到类型foo
的对象具有非平凡的初始化,因为它的构造函数是非平凡的(§3.8/ 1):
如果一个对象属于类或聚合类型,并且它或其成员之一由一个普通的默认构造函数之外的构造函数初始化,则称该对象具有非平凡的初始化。
现在我们看到类型为foo
的生命周期的对象在构造函数结束后开始(§3.8/ 1):
类型为
T
的对象的生命周期始于:
- 获得具有类型T的适当对齐和尺寸的存储,并且
- 如果对象具有非平凡的初始化,则其初始化完成。
现在,如果在构造函数结束之前对对象执行delete
,那么它是未定义的行为,如果类型foo
具有非平凡的析构函数(§3.8) / 5):
在对象的生命周期开始之前但在对象将占用的存储空间被分配之后,任何指向对象所在或将被定位的存储位置的指针都可以被使用但仅限于有限的方式。对于正在建造或销毁的物体,见12.7。否则,[...]
因此,由于我们的目标正在构建中,我们来看看§12.7:
成员函数,包括虚函数(10.3),可以在构造或销毁期间调用(12.6.2)。
这意味着在构造对象时调用self_destruct
很好。但是,本节没有具体说明在构造对象时销毁对象。所以我建议我们看看delete-expression
。
首先,它“将为被删除的对象调用析构函数(如果有的话)。”析构函数是成员函数的一个特例,所以可以调用它。但是,§12.4析构函数在构造期间调用析构函数时没有说明是否定义良好。这里没有运气。
其次,“ delete-expression 将调用释放函数”并且“释放函数将释放指针引用的存储”。再一次,没有说关于这样做的存储,目前正在使用的是正在建设中的对象。
所以我认为这是一个未定义的行为,因为标准没有非常准确地定义它。
请注意:析构函数调用启动时,foo
类型的对象的生命周期结束,因为它有一个非平凡的析构函数。因此,如果delete this;
在对象构造结束之前发生,其生命周期在其开始之前结束。这是在玩火。
答案 1 :(得分:2)
我敢说它被定义为非法(虽然它可能显然仍适用于某些编译器)。
这与“从构造函数抛出异常时未调用析构函数”的情况有些相同。
根据标准,delete-expression会破坏最新派生的对象(1.8)或由新表达式创建的数组(5.3.2)。在构造函数结束之前,对象不是最派生的对象,而是其直接祖先类型的对象。
你的班级foo
没有基类,所以没有祖先,this
因此没有类型,你的对象根本就不是一个对象delete
被调用。但即使有一个基类,该对象也将是一个不是最有派生的对象(仍然将其视为非法),并且将调用错误的构造函数。
答案 2 :(得分:1)
delete this;
在大多数平台上都可以正常使用;有些甚至可能保证正确的行为作为特定于平台的扩展。但是根据标准,IIRC没有明确定义。
您依赖的行为是,通常可以在死对象上调用非虚拟非静态成员函数,只要该成员函数不实际访问this
即可。但标准不允许这种行为;它至多是不便携的。
如果在调用非静态成员函数期间对象不存在时,标准的第3.8p6节将其定义为未定义:
类似地,在对象的生命周期开始之前但在对象将占用的存储之后已经分配,或者在对象的生命周期结束之后以及在重用或释放对象占用的存储之前,任何glvalue可以使用引用原始对象但仅限于有限的方式。对于正在建造或销毁的物体,见12.7。否则,这样的glvalue指的是已分配 存储,并使用不依赖于其值的glvalue的属性是明确定义的。如果出现以下情况,该程序具有未定义的行为:
- 左值到右值的转换应用于这样的glvalue,
- glvalue用于访问非静态数据成员或调用对象的非静态成员函数,或
- glvalue隐式转换为对基类类型的引用,或
- glvalue用作
static_cast
的操作数,除非最终转换 到cv
char&
或cv
unsigned char&
或- glvalue用作
dynamic_cast
的操作数或typeid
的操作数。
对于这种特定情况(删除正在构建的对象),我们在5.3.5p2节中找到:
...在第一个备选方案(删除对象)中,
delete
的操作数值可能是空指针值,指向由非创建的非数组对象的指针先前的 new-expression ,或指向表示此类对象的基类的子对象的指针(第10条)。如果不是,则行为未定义。在第二种方法(删除数组)中,delete
的操作数的值可以是空指针值或由前一个数组 new-expression <产生的指针值/ em>的。如果不是,则行为未定义。
不符合此要求。 *this
不是创建的对象,过去时,是由 new-expression 创建的。它是正在创建的对象(呈现渐进式)。数组的情况支持这种解释,其中指针必须是前一个 new-expression 的结果......但 new-expression 尚未完全评估;它不是之前的,它还没有结果。
答案 3 :(得分:1)
在构造函数成功完成之前,对象不存在。部分原因是构造函数可能是从派生类的构造函数中调用的。在这种情况下,您当然不希望通过显式析构函数调用来销毁构造的子对象,甚至通过在(未完全构造的)对象的(部分)上调用delete this
来调用UB。
关于物体存在的标准,强调补充:
C ++11§3.8/ 1 :
对象的生存期是对象的运行时属性。据说一个对象具有非平凡的初始化 如果它是一个类或聚合类型,它或它的一个成员是由一个非常重要的构造函数初始化的 默认构造函数。 [注意:通过简单的复制/移动构造函数进行初始化是非平凡的初始化。 -end note ]类型为T的对象的生命周期开始于:
- 获得具有适当对齐和T型尺寸的存储,并且
- 如果对象具有非平凡的初始化, 其初始化已完成 。
在这种情况下,构造函数只需用户提供即可。