在基本构造函数完成之前传递“this”:UB 或只是危险

Passing `this` before base constructors are done: UB or just dangerous?

本文关键字:this UB 危险 构造函数      更新时间:2023-10-16

考虑这个最小的例子(我能想到(:

struct Bar;
struct Foo {
  Bar* const b;
  Foo(Bar* b) : b(b) {}
};
struct Bar {
  Foo* const f;
  Bar(Foo* f) : f(f) {}
};
struct Baz : Bar {
  Baz() : Bar(new Foo(this)) {}
};

this传递给 Foo 的 ctor 时,Baz层次结构中没有创建任何内容,但FooBar都不会对它们收到的指针做任何问题。

现在的问题是,以这种方式赠送this是危险的,还是不确定的行为?

问题2:如果Foo::Foo(Bar*)是具有相同语义的Foo::Foo(Bar&)呢?我必须传递*this,但在这种情况下,deref 运算符不会做任何事情。

这不是 UB。该对象可能尚未正确初始化(因此可能无法立即使用它(,但存储指针以供以后使用是可以的。

我必须传递*this,但在这种情况下,deref 运算符不会做任何事情。

当然会,它会取消引用指针。请记住,初始化与分配不同 - 当构造函数运行时,对象已经正确分配(否则您将无法初始化它( - 即它存在,但它处于不确定状态,直到其构造函数完成。

这个问题在标准 3.8/5 中直接回答C++:

在对象的生存期开始之前,但在分配了对象将占用的存储

之后,或者在对象的生存期结束之后,在重新使用或释放对象占用的存储之前,可以使用任何指向对象将要或曾经所在的存储位置的指针,但只能以有限的方式使用。对于正在建造或销毁的物体,请参见 12.7。否则,此类指针引用分配的存储 (3.7.4.2(,并且像指针类型为 void* 一样使用该指针是明确定义的。可以取消引用此类指针,但生成的左值只能以有限的方式使用,如下所述。在以下情况下,程序具有未定义的行为:

  • 该对象将是或曾经是具有非平凡析构函数的类类型,指针用作删除表达式的操作数,
  • 指针用于访问非静态数据成员或调用对象的非静态成员函数,或
  • 指针隐式转换为 (4.10( 到指向基类类型的指针,或者
  • 指针用作static_cast的操作数 (5.2.9((除非转换为 void*,或转换为 void*,然后转换为 char*,或无符号字符*(,或
  • 指针用作dynamic_cast的操作数 (5.2.7(。

此外,在 12.7/3 中:

为了显式或隐式地将引用 X 类对象的指针(glvalue(转换为指向 X 的直接或间接基类 B 的指针(引用(,X 的构造及其直接或间接派生自 B 的所有直接或间接基的构造应该已经开始,并且这些类的销毁尚未完成, 否则,转换会导致未定义的行为。

行为不是未定义的,也不一定是危险的。

FooBar都不会对他们收到的指针做任何问题。

这是关键:您只需要知道指针指向的对象尚未完全构造。

如果Foo::Foo(Bar*)是具有相同语义的Foo::Foo(Bar&)呢?

危险性或定义性而言,两者之间实际上没有区别。

这是个好问题。 如果我们阅读 §3.8,则对象的生命周期非平凡构造函数仅在构造函数完成后启动("初始化完成"(。 几段之后,该标准界定了我们可以和不能用指针做什么"在对象的生存期开始之前,但在对象将占用的存储已分配"(并且 初始化列表中的this指针似乎肯定适合鉴于上述定义,归入该类别(:特别是

在以下情况下,程序具有未定义的行为:

[...]

  • 指针隐式转换为指向基类类型的指针,或者

[...]

在您的示例中,基数参数中的指针类型类具有基类类型,因此派生类的this指针必须隐式转换为它。 这是未定义的行为根据上述。......为了调用构造函数基类,编译器必须将地址隐式转换为类型指向基类的指针。 所以一定有一些例外。

在实践中,我从来不知道编译器在这种情况下会失败,除了涉及虚拟继承的案件;我肯定有遇到具有以下模式的错误:

class L;
class VB {};
class R : virtual VB { public: R( L* ); }
class L { L( char const* p ); };
class D : private virtual L, private virtual R { D(); }
D::D( char const* p ) : L( p ), R( this ) {}

为什么编译器在这里有问题,我不知道。 它能够正确转换指针,将其作为this指针传递给L的构造函数,但在将其传递给时没有正确执行 R .

在这种情况下,解决方法是为 L 提供一个包装类,其中返回指针的成员函数,例如:

class LW : public L
{
public:
    LW( char const* p ) : L( p ) {}
    L* getAddress() { return this; }
};
D::D( char const* p ) : L( p ), R( this->getAddress(); ) {}

这一切的结果是我无法给你一个明确的答案,因为我不确定该标准的作者的意图。 在另一方面,我实际上已经看到它不起作用的情况(而不是那个很久以前(。