Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Consider this smallest example (I could think of):

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)) {}
};

When passing this to the ctor of Foo, nothing in the hierarchy of Baz has been created, but neither Foo nor Bar do anything problematic with the pointers they receive.

Now the question is, is it simply dangerous to give away this in this fashion or is undefined behaviour?

Question 2: What if Foo::Foo(Bar*) was a Foo::Foo(Bar&) with the same semantics? I would have to pass *this, but the deref operator wouldn't do anything in this case.

like image 636
bitmask Avatar asked Nov 14 '11 19:11

bitmask


4 Answers

It's not UB. The object might not be initialised properly yet (so using it right away might not be possible), but storing the pointer for later is fine.

I would have to pass *this, but the deref operator wouldn't do anything in this case.

Of course it would, it would dereference the pointer. Remember that initialisation is not the same as allocation — when the constructor runs, object is already properly allocated (otherwise you wouldn't be able to initialise it) — i.e. it exists, but it's in indeterminate state until its constructor is done.

like image 190
Cat Plus Plus Avatar answered Oct 18 '22 23:10

Cat Plus Plus


This question is answered directly in C++ standard 3.8/5:

Before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that refers to the storage location where the object will be or was located may be used but only in limited ways. For an object under construction or destruction, see 12.7. Otherwise, such a pointer refers to allocated storage (3.7.4.2), and using the pointer as if the pointer were of type void*, is well-defined. Such a pointer may be dereferenced but the resulting lvalue may only be used in limited ways, as described below. The program has undefined behavior if:

  • the object will be or was of a class type with a non-trivial destructor and the pointer is used as the operand of a delete-expression,
  • the pointer is used to access a non-static data member or call a non-static member function of the object, or
  • the pointer is implicitly converted (4.10) to a pointer to a base class type, or
  • the pointer is used as the operand of a static_cast (5.2.9) (except when the conversion is to void*, or to void* and subsequently to char*, or unsigned char*), or
  • the pointer is used as the operand of a dynamic_cast (5.2.7).

Additionally, in 12.7/3:

To explicitly or implicitly convert a pointer (a glvalue) referring to an object of class X to a pointer (reference) to a direct or indirect base class B of X, the construction of X and the construction of all of its direct or indirect bases that directly or indirectly derive from B shall have started and the destruction of these classes shall not have completed, otherwise the conversion results in undefined behavior.

like image 25
Gene Bushuyev Avatar answered Oct 19 '22 00:10

Gene Bushuyev


The behavior is not undefined, nor is this necessarily dangerous.

Neither Foo nor Bar do anything problematic with the pointers they receive.

This is the key: you just have to be aware that the object to which the pointer points is not yet fully constructed.

What if Foo::Foo(Bar*) was a Foo::Foo(Bar&) with the same semantics?

There's really no difference between the two, so far as dangerousness or definedness is concerned.

like image 38
James McNellis Avatar answered Oct 18 '22 22:10

James McNellis


That's a good question. If we read §3.8, the lifetime of an object with a non-trivial constructor only starts once the constructor has finished (“initialization is complete”). And a few paragraphs later, the standard delimits what we can and cannnot do with a pointer “before the lifetime of an object has started but after the storage which the object will occupy has been allocated” (and the this pointer in an initialization list would certainly seem to fit into that category, given the above definition): in particular

The program has undefined behavior if:

[...]

  • the pointer is implicitly converted to a pointer to a base class type, or

[...]

In your example, the type of the pointer in the parameter of the base class has base class type, so the this pointer of the derived class must be implicitly converted to it. Which is undefined behavior according to the above. But... in order to call the constructor of the base class, the compiler must implicitly convert the address to the type pointer to base class. So there must be some exceptions.

In practice, I've never known a compiler to fail in this case, except in cases where virtual inheritance was involved; I've definitely encountered errors with the following pattern:

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 ) {}

Why the compiler had problems here, I don't know. It was able to correctly convert the pointer to pass it as the this pointer to the constructor of L, but it didn't do it correctly when passing it to R.

In this case, the work-around was to provide a wrapper class for L, with a member function which returned the pointer, e.g.:

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(); ) {}

The result of all this is that I can't give you a definite answer, because I'm not sure what the authors of the standard intended. On the other hand, I've actually seen cases where it doesn't work (and not that long ago).

like image 42
James Kanze Avatar answered Oct 18 '22 22:10

James Kanze