PDA

View Full Version : C++ Protected access curiosity


Taliesin
2002.06.28, 05:28 AM
I've been playing around with a couple of classes based around what I've read at http://baskuenen.cfxweb.net and also in the OpenGL Game Programming book (aftef a brief read at the bookshop).

Basically I have this (leaving out irrelevant stuff):

class CNode
{
protected:
CNode *_parent;
CNode *_child;

CNode *_previous;
CNode *_next;
};

Now, from any method defined within the node class, I can access all those members. I can also access members of those members, e.g. _child->_next is accessible.

Next I define CObject:

CObject : public CNode
{
};

From within methods of CObject, I can access all the node pointers listed in CNode, as expected. However, if I try to access _child->_next (for example) I get illegal access to protected member errors from my compiler (CW 7.2). if I use static_cast<CObject*>(_child)->_next, there is no problem.

I don't quite understand this anomaly. A class can access any immediate members and members of objects of the same class that are pointed to. But a subclass can access only the immediate members, and not any objects pointed to. This is not a huge problem, as I have to cast in order to call appropriate methods anyway, but is this part of the standard? Or just the way Code Warrior implements said standard? Seems very strange.

On a slightly related note, the compiler will treat the two functions the same, but from a software engineering point of view, which is more appropriate? A member function:

CNode* CNode::FindRoot()

or a static function:

static CNode* CNode::FindRoot(CNode *)

Just curious :)

kainsin
2002.06.28, 09:17 AM
C++ is extremely strong typed and wants to ensure that you don't do anything you shouldn't be doing. The way I understand it ( correct me if I'm wrong here ) C++ classes can access public and protected data members of the classes they inherit from, as you've observed. However, C++ classes can only directly access data members of other classes of the same type and no guarantees are made that they can access data members of other base classes, as you have also observed.

Note that you should not case a CNode as a CObject since the CNode is the base class it does not actually have any of the extra class functions/variables that a CObject would have. Instead, why don't you just implement some accessor methods? I know it may _seem_ like you're adding too much overhead and typing too much, but it may be better to consider a safety-first approach.

For your second question: What exactly are you trying to accomplish with this function. If you want to pass an arbitrary CNode * and have the _class_ determine what the root node is then it would be better implemented as a static function:

static CNode * FindRoot( CNode * );

and accessed like so:

CNode *theRoot = CNode::FindRoot( otherCNodePointer );

If you want to find the root node of a CNode object then use a regular class function instead:

CNode * FindRoot( void );

and call it on the object:

CNode *rootNode = otherCNodePointer->FindRoot();

Taliesin
2002.06.28, 08:11 PM
Originally posted by kainsin
C++ is extremely strong typed and wants to ensure that you don't do anything you shouldn't be doing. The way I understand it ( correct me if I'm wrong here ) C++ classes can access public and protected data members of the classes they inherit from, as you've observed.

This is my understanding, also. Public grants access to anyone, protected grants to the base class and its descendants, and private grants to the base class only. All well and good, makes perfect sense.

However, C++ classes can only directly access data members of other classes of the same type and no guarantees are made that they can access data members of other base classes, as you have also observed.

So what you are saying here is that what I observed is the "correct" behaviour? That the base class can access protected/private members of any object of that type, but derived classes can only access the "this" objects inherited members.

That kind of made sense, but it seems odd that once you tell it the CNode is really a CObject (which could be done safely with dynamic_cast) you are allowed access. I think my question was less about the strength of types in C++ and more about the consistency of the rules. Could just be one of those implementation-dependent things.


Note that you should not case a CNode as a CObject since the CNode is the base class it does not actually have any of the extra class functions/variables that a CObject would have. Instead, why don't you just implement some accessor methods? I know it may _seem_ like you're adding too much overhead and typing too much, but it may be better to consider a safety-first approach.

The code that I was looking at implemented the children and sibling pointers as public members. I didn't think that was the best practice, so I implemented them as private members. As yet, I haven't seen where anything outside the node class hierarchy should know about these pointers, so I implemented a bunch of protected member functions such as FirstChild(), LastChild(), Previous(), Next(). So far, so good.

I then came to write an Update() routine for CObject. There are a couple of ways to ensure that your CNode pointers are really CObject pointers. I didn't want to use RTTI unless I had to, so for the moment I have preconditions that valid CObjects have been attached to the tree :)

Anyway, there needed to be a cast in there somewhere in order to call the Update() method. I had code similar to this (not this actual code, as this will fail with only one child, its illustrative):


CNode* current = FirstChild();
CNode* last = LastChild();

while(current != last) {
static_cast<CObject*>(current)->Update();
current = current->Next();
}


This is another situation where I wonder about a static function, but I'll get to that in a moment.

When I compiled this code, I got an illegal access to protected member error, on the line current = current->Next(). I then started playing around to find out what was legal and what was not, during which time I removed the accessor functions to work directly with the member variables. I could equally put them back, and I would still need to cast to CObject pointers in order to access them.

I don't think the cast is too bad in this situation. If the client code is obeying the rules, all nodes attached to a CObject should themselves be descended from CObject (at some point, I may look into making CNode abstract, but not today :)).

For your second question: What exactly are you trying to accomplish with this function. If you want to pass an arbitrary CNode * and have the _class_ determine what the root node is then it would be better implemented as a static function:

static CNode * FindRoot( CNode * );

and accessed like so:

CNode *theRoot = CNode::FindRoot( otherCNodePointer );

If you want to find the root node of a CNode object then use a regular class function instead:

CNode * FindRoot( void );

and call it on the object:

CNode *rootNode = otherCNodePointer->FindRoot();

What I am trying to accomplish here is simply given any node in a tree, find the root of the tree.

From an interface perspective, you are either saying to a class function "Find the root of the tree this object is in" or you are saying to a member function "Find the root of the tree you are in." It's sort of an active vs passive thing.

From an implementation perspective I am leaning a little more towards it being a static function, since I have implemented it iteratively and it touches every node on its way up the tree. However, had it been implemented recursively it only ever accesses it's immediate data members and would therefore be a member function.

In a similar vein, and one I touched on before is the Update function. Since it calls DoUpdate for itself and then walks the list of children calling Update() on each of them, should Update() be static?

(In some ways, using the STL here would be a whole lot easier - I opted not to because object cleanup would have been very expensive. I may well open this can of worms in another thread.)

There was a little adage I read many years ago when C++ was the big new thing:

Put five C programmers in a room overnight with a problem and by morning you will have five different soultions. Put five C++ programmers in a room overnight with a problem, and they will argue all night over design issues.

Very true. Maybe I'm attempting to over-engineer before I've got anything coded and working :)

inio
2002.07.03, 01:49 AM
I've discovered that if you have the slightest inclination that this MIGHT be the case, it almost certainly is.

Every C programmer I've seen move to C++ (including myself) has spent quite a while bogged down in "object orienting their information design paradigms" or some similar catchphrasey uselessness. This is completely normal, and a very good thing. If you don't waste endless hours coming up with ridiculously fiendish mazes of protection, virtual functions and multiple inheritance, you won't know when and why to stay away from them later (don't say the word 'template' within 50 yards of me or I might have to strangle something). Thats not saying these devices don't have many very valid uses, they just are oft over-used by people new to C++.

story->morals[0]: Protection is useless unless you're doing your coding in a cube farm.

story->morals[1]: pay the price of wasting time learning how C++ sucks now, rather than getting fired later because you never did.

(insert sexual innuendo involving C++ terminology here)

OneSadCookie
2002.07.03, 02:14 AM
Template template template template template template!

One of the few good bits about C++, IM(NS)HO, code bloat notwithstanding.

Perhaps not the nicest way it could have been done, but it provides type parameterization (all good), and separation of type (interface) and implementation, something that most object-oriented languages largely ignore.

Taliesin
2002.07.03, 02:20 AM
Originally posted by OneSadCookie
Template template template template template template!


Ah, I was going to do that. I've seen a lot of uses for templates. After, I might add, doing a lot of copy'n'paste of largely identical code and thinking there must be a better way.

I think currently templates are what inheritance was when C++ first came on the scene - a solution that people try to apply to every problem without really understanding what they do. If all you have is a hammer, then everything looks like a nail.