James
James

Reputation: 25523

Base class holding a reference to Derived

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.

Upvotes: 5

Views: 812

Answers (5)

Lightness Races in Orbit
Lightness Races in Orbit

Reputation: 385144

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

Upvotes: 3

Johann Gerell
Johann Gerell

Reputation: 25581

I think the big problem in this is that you think you want to do one thing, when in reality you actually want to do something else. Strange, huh?

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

Yes, as long as you don't use it (for anything but storing a reference to it) in the scope of the Base constructor and remember in ~Base that Derived is destroyed before Base.

But why on earth do you think that Base wants to know of Derived? If it's static polymorphism you are after, then the curiously recurring template pattern is what you want:

template <typename T>
class Base {};

class Derived : public Base<Derived> {};

But I don't really think that's what you're aiming at.

Maybe you want a way for Base to communicate with a client and think that should be done with inheritance? If so, then this observer-ish idiom is what you need:

class Client
{
public: 
    virtual void Behavior() = 0;

protected:
    ~Client() {}
};

class Base
{
    Client& client_;

public:
    Base(Client& client) : client_(client) {}
};

class Implementor : public Client
{
public:
    Implementor() : Base(*this) {}

    virtual void Behavior() { ... }
};

If not even that is what you want, then you need to rethink your design.

Upvotes: 1

Steve Jessop
Steve Jessop

Reputation: 279255

3.8/6 says what you can do with a pointer/reference to memory for an object that has been allocated but not yet constructed. Your code doesn't provoke an lvalue-to-rvalue conversion, or otherwise break the rules, so I'd think that you're fine. Since you're observing bad values, though, I may well have missed something. Or your code might be otherwise bad.

Even if you did break those rules, 12.6.2 and 12.7 list additional things that you can do during construction and destruction.

Edit: ah, 8.3.2/4: "A reference shall be initialized to refer to a valid object or function." You initialize m_ref to refer to an object whose constructor hasn't even been entered yet. I don't know without further research whether an object under construction is "valid" or not, and in particular whether the object of most-derived type is "valid" at the time of construction of the base class. This could perhaps be the problem, though.

You might think that no unconstructed object is "valid", but then this would be invalid:

class Foo {
    Foo() {
        Foo &self = *this; // reference initialized to refer to unconstructed object!
    }
};

So, is that invalid? If not, does the most-derived object become valid somewhere between the start of the base class constructor call and the start of the derived class constructor call? I dunno, sorry.

Upvotes: 2

Andrew T Finnell
Andrew T Finnell

Reputation: 13628

I'm actually implementing a generic base class that takes a template parameter class and derives from it, and adds a "safe bool" conversion based on the result of a function call on the derived type. I'd like to avoid using virtual functions, if possible, because I'm a serial premature optimiser I really do care about performance in some of the places I'd like to use this. – Autopulated 37 mins ago

You don't need a reference to the Derived class. Your class is deriving from a template parameter. Just use the common method.

#include <iostream>

template <class T>
class Base : public T
{
public:
    bool operator!() const 
    {
        return !this->isOk();
    }
};

class TemplateClass
{

public:
    bool isOk() const
    {
        return true;
    }
};

int main (int argc, char* argv[])
{
    Base<TemplateClass> myClass;

    if (!!myClass)
    {
        std::cout << "ok" << std::endl;
    }
    else
    {
        std::cout << "not ok" << std::endl;
    }

    return 0;
}

You can even use template specialization if you know ahead of time of derived classes that don't implement a common bool check.

Upvotes: 0

Graeme Perrow
Graeme Perrow

Reputation: 57248

I think in general you're OK doing this, but be very careful in constructors and destructors. In particular, in Base::~Base, the Derived part of the object has already been destroyed so don't use m_ref there.

Upvotes: 2

Related Questions