C++ Inheritance confusion

Started by
10 comments, last by frob 10 years, 1 month ago

This has got to be a simple mistake, but I've stared at it for too long now and I just don't see it. sad.png (Excuse the canned example; it's pretty close to the real thing though.)


class Foo {

public:
Foo(vector<string> things, string place) : m_thingList(things)
{
log("in Foo ctor");
validatePlacePriorToAssignment(place);
}

virtual ~Foo() {}

private:

vector<string> m_thingList;
string m_validatedPlace;

validatePlacePriorToAssignment(string p)
{
// do stuff to validate place; if not valid, throw exception
// else
m_validatedPlace = p;
}
...
};

class Bar : public Foo {
public:
const string defaultPlace = "Validville";
Bar(vector<string> stuff) : Foo(stuff, defaultPlace)
{
log("in Bar ctor");
}
...
};

someFunctionSomewhere(vector<string> allTheThings) {
try { Bar barbar(allTheThings); }
catch (...) { log("saddness"); }
}

Okay, here's the weirdness I'm seeing: there is a log entry for the Foo ctor but not the Bar ctor. I'm stack-allocating a member of a derived class; my understanding is that the base ctor is called THEN the derived ctor is called...but in this case that doesn't seem to be happening!

I'm worried that I'm doing inheritance wrong, or using initializer lists wrong, or I'm using them wrong in the context of inheritance, or for some reason it's wrong (not allowed) to validate prior to assigning in a constructor. I think I'm doing this all correctly, but obviously I'm not so here I am. smile.png

I've read through a bunch of tutorials and stackoverflow examples this afternoon, and - for the life of me - I can't figure out why this isn't working; it looks like all the sample code. If you can catch it, I'd be especially grateful.

Thank you in advance. <3

Advertisement

Removing the init lists entirely results in the kind of behavior I'd expect, i.e. I see log entries from both ctors and in the order I'd expect (Foo then Bar).

It looks correct. Is the exception being triggered? Maybe "validatePlacePriorToAssignment(place);" is crashing and you aren't realizing it.

That's got to be it! I commented out the validate() call and just set the member in the init list. So...good. Thank you for being a sanity check for me, haha. :)

A little odd; I thought for sure I was catching there, but I am demonstrably incorrect. I was looking in the very wrong place.

Thank you!

This crashes. This runs. I think it's related to the constant string.


//This will be constructed when 'Bar's constructor is ran.
const string defaultPlace = "Validville";

//But you are passing it to 'Foo's constructor before it gets constructed,
//because Foo's constructor runs before Bar's constructor.
//So 'defaultPlace' is invalid, and it crashes.
//
//If you made it static-const, then it'd be fine (as long as no Bar's are static or global).
Bar(vector<string> stuff) : Foo(stuff, defaultPlace)
{
	std::cout << "in Bar ctor" << std::endl;
}

Sneaky little bug.

Glad I could be of help!

That (const string bug) was actually a second bug, well shielded by the first one. :)

I fixed it with stackoverflow: http://stackoverflow.com/questions/1563897/c-static-constant-string-class-member

In short, it's now declared (static const) in the class decl, then defined in the cpp file, but still scoped to the class (which is what I wanted).

Second set of eyes for the win. :) Nice to see it running again.

This is the reason I usually stick with trivial, or even empty, constructors, and then call an explicit init() function after instantiation. Constructos, exceptions, and manual memory management are a really bad combination :-) . The problem is even worse for destructors.

Not using constructors in favor of init() is like dodging a car to get hit by a train. It doesn't make things safer, it's just a different set of errors to look out for.

RAII is a really important and incredibly useful tool for code safety.

Note that RAII and constructors that build an empty state are not contradictory. You can absolutely construct empty, disconnected objects that are still well formed and initialized to a known state. Empty containers and disconnected file streams are the most obvious examples of this. You wouldn't say a constructor for a std::vector does not follow RAII just because array data was not assigned, or a std::ifstream is ill formed because the .open() function hasn't been called yet. They are fully initialized objects that are ready for use.

Constructing a logger which is not yet connected to any streams can be a very good thing; many major logging systems do exactly that.

A logger is often little more than a listener pattern. Output streams get registered as listeners, and when logging data is sent to the system any active listeners can process the data. The collection of output streams can be zero in size, or it can be large. Streams can in theory be added and removed dynamically. Constructing the logger with an empty collection of streams is fully initialized. Just because nobody is listening does not make it undefined to handle a log event; it simply forwards the event to all (currently zero) listeners.

That's true. Another reason for an init()-type function is to reuse the memory of same class instance rather than deleting and then allocating a new object, or to intentionally delay some heavy operation until a more convenient time.

I meant that avoiding constructors and destructors because they can cause bugs if misused doesn't mean your code will be safe - you'll just be looking at a different set of bugs. Constructors and destructors go hand in hand with memory management.

Using an init() function when it is part of the class's desired interface makes sense. Using an init() function solely because you don't like constructors doesn't make sense. Doubly sole for destructors.

This topic is closed to new replies.

Advertisement