Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Base class holding a reference to Derived

Tags:

c++

I'd like to do this:

struct Derived;

struct Base{
    Derived const& m_ref;
    Base(Derived const& ref) : m_ref(ref){}
};

struct Derived: Base{
    Derived(): Base(*this){}
};

But I seem to get unreliable behaviour (when used later on, m_ref points to things that aren't valid Derived).

Is it permissible to construct a reference to Derived from *this before the class has been initialised?

I appreciate that it is not valid to use such a reference until it has been initialised, but I don't see how changes to the initialisation of a class can affect references to it (since initialising it doesn't move it around in memory...).

I'm not sure what to call what I'm trying to do, so my search for information on this has drawn a blank...



Update: I can't reproduce my problems with a simple test case, so it looks like it is probably okay (though I can't prove it, and would still welcome a definitive answer). Suspect my problems arose from a broken copy-assignment operator. That's another matter altogether though!

Update 2 My copy constructor and copy-assignment operators were indeed to blame, and now this seems to work reliably. Still interested in whether or not it is well-defined behaviour though.

like image 787
James Avatar asked Jun 05 '11 22:06

James


1 Answers

3.8/1 says:

The lifetime of an object of type T begins when: — storage with the proper alignment and size for type T is obtained, and — if T is a class type with a non-trivial constructor (12.1), the constructor call has completed.

3.8/5 says:

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. Such a pointer refers to allocated storage (3.7.3.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.

"Below" is 3.8/6:

Such an lvalue refers to allocated storage (3.7.3.2), and using the properties of the lvalue which do not depend on its value is well-defined.

...and then a list of things you can't do. Binding to a reference to the same, derived type is not among them.

I can't find anything elsewhere that might make your code invalid. Notably, despite the following phrase in 8.3.2/4:

A reference shall be initialized to refer to a valid object or function.

there doesn't seem to be any definition of "valid object" to speak of.

So, after much to-ing and fro-ing, I must conclude that it is legal.


Of course, that's not to say that it's in any way a good idea! It still looks like a bad design.

For example, if you later change your base constructor and any of the following become relevant (again from 3.8/6):

  • the lvalue is used to access a non-static data member or call a non-static member function of the object
  • the lvalue is implicitly converted (4.10) to a reference to a base class type
  • the lvalue is used as the operand of a static_cast (5.2.9) (except when the conversion is ultimately to char& or unsigned char&
  • the lvalue is used as the operand of a dynamic_cast (5.2.7) or as the operand of typeid.

...then your program will be undefined, and the compiler may emit no diagnostic for this!


Random other observations

I notice a couple of other interesting things whilst compiling this answer, and this is as good a place as any to share them.

First, 9.3.2 appears to leave the type of this in a ctor-initializer accidentally unspecified. Bizarre!

Second, the criteria set on a pointer by 3.8/5 (not the same list that I quoted from 3.8/6) include:

If the object will be or was of a non-POD class type, the program has undefined behavior if [..] the pointer is implicitly converted (4.10) to a pointer to a base class type.

I believe that this renders the following innocuous-looking code undefined:

struct A {
   A(A* ptr) {}
};

struct B : A {
   B() : A(this) {}
};

int main() {
   B b;
}
like image 120
Lightness Races in Orbit Avatar answered Sep 28 '22 16:09

Lightness Races in Orbit