在基础构造函数完成之前传递`this`:UB还是危险的?

时间:2011-11-14 19:04:27

标签: c++ constructor this undefined-behavior

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

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运算符不会执行任何操作。

4 个答案:

答案 0 :(得分:8)

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

  

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

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

答案 1 :(得分:5)

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

  

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

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

  

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

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

答案 2 :(得分:5)

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

在对象的生命周期开始之前但在对象占用的存储空间已经被分配之后,或者在对象的生命周期结束之后以及在重用或释放对象占用的存储之前,任何引用的指针可以使用对象将位于或位于的存储位置,但仅限于有限的方式。对于正在建造或销毁的物体,见12.7。否则,这样的指针指的是已分配的存储(3.7.4.2),并且使用指针就像指针的类型为void *一样,是明确定义的。这样的指针可以被解除引用,但是所得到的左值可以仅以有限的方式使用,如下所述。如果出现以下情况,该程序具有未定义的行为:

  • 该对象将是或具有非平凡析构函数的类类型,并且该指针用作delete-expression的操作数,
  • 指针用于访问非静态数据成员或调用对象的非静态成员函数,或
  • 指针被隐式转换(4.10)为指向基类类型的指针,或者
  • 指针用作static_cast(5.2.9)的操作数(除非转换为void *,或者为void *,随后是char *或unsigned char *),或者
  • 指针用作dynamic_cast(5.2.7)的操作数。

此外,在12.7 / 3中:

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

答案 3 :(得分:4)

这是一个很好的问题。如果我们读§3.8,一个对象的生命周期 一个非平凡的构造函数只在构造函数完成后才会启动 (“初始化完成”)。几段之后, 标准界定了我们可以用指针做什么和做什么 “在对象的生命周期开始之前,但在之后 对象占用的存储空间已被分配“(和 初始化列表中的this指针肯定似乎合适 根据上述定义,进入该类别:特别是

  

如果出现以下情况,程序会有未定义的行为:

     

[...]

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

[...]

在您的示例中,基数参数中指针的类型 class具有基类类型,因此派生类的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(); ) {}

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