Jump to content

Can't downcast a dereferenced object?


Recommended Posts

Is there a "right way" (or any way for that matter) to call child class methods on a parent object that was stuffed into a DVR?

Like everyone else I've been playing around with DVRs lately, focusing primarily on ways to use object references to simplify various design pattern implementations. Today I identified a behavior that has been giving me fits for weeks. If you create an object reference to a parent object, after dereferencing the parent object there does not appear to be any way to downcast it to a child object or use dynamic dispatching to call child class methods. I built a simple example project with a parent class and a child class to illustrate this.

post-7603-125100121093_thumb.png

These show a simple implicit downcast and upcast to static dispatch methods. The downcast not working isn't surprising since wiring the parent object directly into a static dispatch child method breaks the wire also.

post-7603-125100127756_thumb.png

Here I tried explicitly downcasting to the child class before calling the static method. There are no bad wires but the run arrow is broken with the error, "Although you can modify the class object inside a reference, you cannot substitute the class object for another object, even if the object belongs to the same class. This restriction prevents you from changing the data type of the object in the reference."

This limitation did surprise me, although I suppose it is consistent--bundling the parent in a cluster and using the in-place structure's unbundle/bundle elements generates a runtime error. The difference is the in-place unbundle/bundle elements can easily be replaced with regular unbundle/bundle nodes. There's no equivalent for DVRs.

post-7603-12510013043_thumb.png

This is as close as I could come to regular unbundle/bundle nodes when using DVRs The downcast gives me a runtime error. What's really odd is that the downcast works despite the error. If I clear the error the child class correctly updates its private data in the static method and a new object reference is created for the parent. I'm not sure if I'll be able to use this for my purposes but at least it's an avenue to explore. I'm also curious which is the bug: the runtime error or the downcast actually working.

I tried several other techniques as well, but I always either got a broken run arrow or a runtime error. Are there other solutions I've missed?

Parent-Child DVR.zip

  • Like 1
Link to comment

Is there a "right way" (or any way for that matter) to call child class methods on a parent object that was stuffed into a DVR?

Like everyone else I've been playing around with DVRs lately, focusing primarily on ways to use object references to simplify various design pattern implementations. Today I identified a behavior that has been giving me fits for weeks. If you create an object reference to a parent object, after dereferencing the parent object there does not appear to be any way to downcast it to a child object or use dynamic dispatching to call child class methods. I built a simple example project with a parent class and a child class to illustrate this.

post-7603-125100121093_thumb.png

These show a simple implicit downcast and upcast to static dispatch methods. The downcast not working isn't surprising since wiring the parent object directly into a static dispatch child method breaks the wire also.

post-7603-125100127756_thumb.png

Here I tried explicitly downcasting to the child class before calling the static method. There are no bad wires but the run arrow is broken with the error, "Although you can modify the class object inside a reference, you cannot substitute the class object for another object, even if the object belongs to the same class. This restriction prevents you from changing the data type of the object in the reference."

This limitation did surprise me, although I suppose it is consistent--bundling the parent in a cluster and using the in-place structure's unbundle/bundle elements generates a runtime error. The difference is the in-place unbundle/bundle elements can easily be replaced with regular unbundle/bundle nodes. There's no equivalent for DVRs.

post-7603-12510013043_thumb.png

This is as close as I could come to regular unbundle/bundle nodes when using DVRs The downcast gives me a runtime error. What's really odd is that the downcast works despite the error. If I clear the error the child class correctly updates its private data in the static method and a new object reference is created for the parent. I'm not sure if I'll be able to use this for my purposes but at least it's an avenue to explore. I'm also curious which is the bug: the runtime error or the downcast actually working.

I tried several other techniques as well, but I always either got a broken run arrow or a runtime error. Are there other solutions I've missed?

Hi Daklu

I have been playing with LVOOP and DVR also! It is very cool stuff. I had a look at your post as it is very interesting. I think removing the references to investigate the problem may help to solve the issue. Below is what I have come up with.

Parent cannot downcast to Child as Parent cannot run extended Child methods (Child can always run all Parent Methods) so you get a broken run arrow and that makes sense, any other casting will generate a run time error (see attached png top).

The downcast gives me a runtime error. What's really odd is that the downcast works despite the error. If I clear the error the child class correctly updates its private data in the static method and a new object reference is created for the parent.

When you use the Preserve Run Time Class function I don't think you are getting a new parent, I think you are getting a new child: When the error occurs, all data on the wire is lost and a new child is created. You can then do what you like with the child (see attached png middle). I think the references may be hiding all this in the example you posted as you can have a parent reference pointing to a child object (see attached png bottom).

post-10325-125102090384_thumb.png

Therefore I do not think it is a bug either way?

Would you agree or am I totally off! :wacko:

Edited by jgcode
  • Like 1
Link to comment

Hi

I can’t help you much there.

The design I’m looking into is to have the class-data (attributes) as a TypeDef belonging to the class and then put it into the Data Value Reference (DVR) wire.

post-941-125102049233_thumb.png

That then goes into the lvclass private data.

post-941-12510205016_thumb.png

A update method could then look like this.

post-941-125102050319_thumb.png

By using this method the dynamic dispatching will work as usually without having to put an In Place Element Structure around all abstract methods.

Cheers,

Mikael

  • Like 1
Link to comment

The design I’m looking into is to have the class-data (attributes) as a TypeDef belonging to the class and then put it into the Data Value Reference (DVR) wire.

I saw that over on the LVOOP thread and it does provide a lot of benefits, but I not sure it quite solves the larger problem I'm trying to address. (If I could describe the problem without causing you a severe case of MEGO ("my eyes glaze over") I would, but I still don't understand the problem enough to do it concisely.)

When the error occurs, all data on the wire is lost and a new child is created.

Ahh, this is key information I missed. I thought the parent class data was preserved in the child class.

So, please, don't think I'm calling you dumb...

Meh... you wouldn't be far off. laugh.gif

Customers who are completely off-base are easier to teach because they need the whole lesson.

I'm the backyard mechanic who knows just enough to really screw things up. smile.gif

Think about what you just asked for... ignore the DVR part for a moment. You just asked for a PARENT object to invoke a CHILD class method.

Yeah, when you put it that way it's apparent why the example doesn't work. Too much time in the trenches makes it easy to miss the obvious. The example I posted is a simplified representation of what I'm trying to do; I'll have to see if I can create a more accurate model to experiment with.

Upcast and downcast DO NOT create new objects EXCEPT when they return an error.

Ahh... I didn't know this either. It says so right in the help file but every single one of the dozen times I looked at it I read it as "the function returns an error and the data value of the object out is of the same wire type as the object in" instead of what it really says, "...same wire type as the target object." (In fact, it wasn't until after I pasted that quote here to show you that the documentation was wrong that I discovered my mistake.) blink.gif

Does that make sense?

Not yet, but there's lots of stuff here for me to digest. I really appreciate the detailed response. It can be hard getting a good overall view of things by picking up tidbits of information here and there. I'm sure I'll be back with more questions later.

  • Like 1
Link to comment

The candles are starting to flicker to life...

You will ALMOST NEVER WIRE PRTC WITH A CONSTANT FOR THE CENTER TERMINAL.

I read that and immediately wondered why the first case causes a broken run arrow and the second doesn't. (Hey, it was in CAPS... it was easy to skip the preceeding sentences.)

post-7603-125105213067_thumb.pngpost-7603-125105212248_thumb.png

Then the word "object" in the PRTC context help hit me over the head. From my end user perspective, previous versions of Labview don't make a distinction between classes and objects. (For example, the To More Specific Class prim has 'Target Class' as an input that in reality accepts an object.) Despite my attempts to differentiate between the two when posting, in my head I often use the terms interchangably. I'll have to work hard to change my way of thinking.

I'd give you more kudos if I could...

  • Like 1
Link to comment

I'll have to see if I can create a more accurate model to experiment with.

Thanks to AQ's explanation above, some solitude courtesy Nickelback and loud speakers, and fixing a problem with circular dependencies, I was able to develop a better model of what I was trying to do and get it to work!

post-7603-125107028014_thumb.png

This model isn't exactly what I'm doing in my real code, but it's closer. I have a reusable code package with two classes developers can use in their applications either directly or as parent classes for their own application-specific classes. In this example parent 1 is a simple counter with Increment Counter and Get Counter Value methods while parent 2 is a helper class that operates on parent 1 and makes a "Multiply by 2" method available. (They are shown with abstract methods but they could be implemented.)

While writing an application a programmer realizes he needs a counter object that can also count down and handle negative numbers, but he can't change the reuse code package. He creates Child 1 to extend Parent 1 and adds the decrement method. Now his counter can count up and down. To correctly multiply negative numbers by 2 he creates Child 2 and overrides the Mult by 2 method. Inside that method he uses Parent1:GetCtr to check if the counter is positive or negative. If it is positive he uses Child1:IncCtr to provide the x2 functionality; if it is negative he uses Child1:DecCtr.

The problem was that I couldn't figure out how to use Child1 methods inside the InpEl:DVR structure even though the value on the wire at runtime is a Child1 object. That's when I started floundering and ended up with the example posted at the start of this thread. But now, thanks to the fine folks on LAVA, I see where my assumptions were wrong and things are much more clear. *sniff* I love you guys... *sniff* laugh.gif

It might take me a bit longer to make it around the block, but I'll get there eventually. Here's the test vi I wrote and the code for Child2:MultBy2.

post-7603-125107432263_thumb.png

post-7603-125107435067_thumb.png

DVR Composition.zip

Link to comment
  • 2 weeks later...

This model isn't exactly what I'm doing in my real code, but it's closer. I have a reusable code package with two classes developers can use in their applications either directly or as parent classes for their own application-specific classes. In this example parent 1 is a simple counter with Increment Counter and Get Counter Value methods while parent 2 is a helper class that operates on parent 1 and makes a "Multiply by 2" method available. (They are shown with abstract methods but they could be implemented.)

While writing an application a programmer realizes he needs a counter object that can also count down and handle negative numbers, but he can't change the reuse code package. He creates Child 1 to extend Parent 1 and adds the decrement method. Now his counter can count up and down. To correctly multiply negative numbers by 2 he creates Child 2 and overrides the Mult by 2 method. Inside that method he uses Parent1:GetCtr to check if the counter is positive or negative. If it is positive he uses Child1:IncCtr to provide the x2 functionality; if it is negative he uses Child1:DecCtr.

Daklu, I was wondering if you tried to implement this using SciWare's approach of having the class's data members as a single DVR of a TypeDef and not creating a DVR of the object itself (see http://lavag.org/topic/10666-labview-oop-by-ref/). I played around with it a bit on your example and it does clean up the 'Application' part of the VI. All of the IPEs can be inside Get/Set methods. Makes the application programmer's job a bit easier in my opinion.

There was an ensuing debate on whether this approach (DVR of data member cluster) was both 'in the spirit' of NI's implementation and safe from a race or deadlock conditions. SciWare any further thoughts on the latter issue? I do not see much risk beyond what is 'expected' when you share references to the same memory across threads.

The project I am working on has a fairly deep class hierarchy. One branch of the hierarchy represents hardware components - motors, actuators, sensors, analog outputs. Another branch represents assemblies of these components (or other assemblies) - arms, grippers, thermal baths. My original thought was to have the assemblies hold a DVR to the hardware components. But without the ability of Dynamic Dispatch on a DVR the IPEs were simply too cumbersome. So, in comes SciWare's idea and the IPEs are now buried in the classes Get/Set (sorry NI, Read/Write) methods.

Yes, I am worried about deadlock conditions. And a bit worried about performance, but since I am controlling slow mechanical devices (e.g. benchtop robots), speed is not really a concern.

I am hoping that others out there are also trying this. I too will give some feedback as my design get implemented a bit further.

Link to comment

Daklu, I was wondering if you tried to implement this using SciWare's approach of having the class's data members as a single DVR of a TypeDef and not creating a DVR of the object itself (see http://lavag.org/top...iew-oop-by-ref/). I played around with it a bit on your example and it does clean up the 'Application' part of the VI. All of the IPEs can be inside Get/Set methods. Makes the application programmer's job a bit easier in my opinion.

The short answer is no. I don't quite understand exactly what changes you made. If you can post images of your code it would help me understand your implementation. (My LV'09 trial license expired and I haven't got a home license from work yet.)

The project I am working on has a fairly deep class hierarchy.

Danger, Will Robinson! Deep inheritance hierarchies aren't very malleable and you may find yourself unable to accomodate future changes. Or worse, you could end up hacking in modifications and ending up with very brittle code. Can you post a diagram of your object model?

Link to comment
Danger, Will Robinson! Deep inheritance hierarchies aren't very malleable and you may find yourself unable to accomodate future changes. Or worse, you could end up hacking in modifications and ending up with very brittle code. Can you post a diagram of your object model?

Really? My opinion has generally been that deep hierarchies mean that you've adequately decomposed objects and you can inherit in the future off of any of the intermediate levels instead of being stuck with one concrete class that has everything. I've always thought that the deep hierarchies are highly extensible.
Link to comment

Really? My opinion has generally been that deep hierarchies mean that you've adequately decomposed objects and you can inherit in the future off of any of the intermediate levels instead of being stuck with one concrete class that has everything. I've always thought that the deep hierarchies are highly extensible.

This is exactly why I love LAVA. I can voice an opinion and have more experienced developers (or the lead R&D developer in charge of LVOOP!) challenge my ideas. thumbup1.gif I'm writing this response with the intent of explaining how I came to the conclusion that deep trees are bad, not as an attempt to teach you (as if!) the finer points of OO programming. I'm more than happy to be shown the error of my ways. I'll also note that a "class heirarchy" implies, in my mind, inheritance relationships.

First, I think there are subtle differences between "malleable" and "extensible." Both refer to ways of altering functionality; however, extensible code adds functionality through 'bolt-on' additions while malleable code adds functionality by changing the way the objects are organized. As a metaphor, image the code is represented by marbles in the shape of a triangle on a chinese checkers board. Extensible code allows changing the triangle into a circle by adding marbles to it. Malleable code allows changing the triangle into a circle by moving the marbles that are already there. In my opinion, deep class hierarchies may be highly extensible, but they are rarely malleable.

My opinion is based partly on my experiences and partly what I've learned from OOP literature. One of the first lessons I learned when I started "seriously" studying OO design was to favor composition over inheritance. Various books and websites have pointed out that inheritance relationships are fairly tightly coupled. Creating a long inheritance chain causes dependencies that are not easily changed later, especially when there is existing code that depends on that chain. Learning that, I realized why the class hierarchies I've worked with have ultimately fallen short of what we needed them to do.

In the past several years I've worked with two fairly deep framework class hierarchies and neither ended up being able to properly support the functionality we needed without completely redesigning the tree. Undoubtedly some of the problem was due to our inexperience with OO design. However, looking back on it now I still do not see inheritance providing a good solution to the problems we faced. Our hierarchies, like kugr's, were based on hardware devices, so when he says one branch of his "deep class hierarchy" is devoted to "motors, actuators, sensors, [and] analog outputs," the warning bells start ringing. If this branch is based primarily on inheritance he may end up in exactly the same situation I did.

Having been burned twice by deep inheritance trees, my design philosophy now leans heavily the other way. Instead of trying to fit hardware devices with different capabilities onto the same inheritance tree, now I look to have several short and small inheritance trees. Many small trees gives me malleability. Extensibility tends to be achieved by creating an abstract facade and creating child classes that are composed of the parts of the smaller trees that I need.

Link to comment

It is a philosophy question, and I doubt there is a right answer.

"Favor composition over inheritance" is a battle cry I have heard a lot over the last few years. Composition means I can "plug in" functionality from lots of sources. The trouble, as I see it, is that you end up writing a lot of wrapper functions in order than the outer object can expose the inner objects' APIs as its own. Now, the theory is that the inner object's API can change completely and as long as the new API can still support the old functionality, the outer API doesn't have to change -- you change the implementation of those wrappers, not the outer API. But what I observe is that it is rare to want to change the inner API and not change the outer one. Perhaps you're adding a new option, or you're breaking one atomic function into several individual parts. There's usually a reason you're doing that, and it is rare -- in my experience -- that the reason you're doing it is just so you can have a better internal API. Changes to deeper layer APIs are almost always driven by top-level demands for features. Or, to put it another way, it is rare that the core decides, "I'm going to change how I do things because it will be better." It is much more common for the outermost layer to say, "I need to serve the user differently... but I can't do that unless the core exposes some new functionality."

Given that the desire for a new API is almost always driven by the layer closest to the user, when the core changes, the outer layer will want to change anyway, so if there's a tight binding (inheritance) between core and outer layer, so what? Not having that binding doesn't generally seem to save you. Sure you *could* avoid rewriting the outer layer when the core changes, but if the whole point of changing the core is so you *can* rewrite the outer, then you're going to rewrite the outer.

Yes, I spend a lot of time changing inheritance trees around -- introducing new middle layer classes, wiping them out again a couple revisions later. I've got one tree of classes I'm working on at the moment where each layer of inheritance is adding one function, and one "concrete" class forking off of the trunk at each level. As I revise each of the concrete classes, they're sliding down the tree, gaining functionality, and I'll probably collapse a lot of the middle layers eventually. I find that the chain of inheritance is useful for navigating my code and I don't see a major drawback to refactoring the tree. Very few classes are persisted to disk, so that's rarely an issue, and in LV, the mutation logic built into the language generally handles everything I need it to.

  • Like 1
Link to comment

The short answer is no. I don't quite understand exactly what changes you made. If you can post images of your code it would help me understand your implementation. (My LV'09 trial license expired and I haven't got a home license from work yet.)

I will work on that. I kind of destroyed it when I was playing with it further. I was trying different approaches to the structure. Should have left it alone while it was working.

Danger, Will Robinson! Deep inheritance hierarchies aren't very malleable and you may find yourself unable to accomodate future changes. Or worse, you could end up hacking in modifications and ending up with very brittle code. Can you post a diagram of your object model?

Perhaps deep is a relative term (there are only 5 levels). I will post a diagram soon. I need to grab Visio or something. I work for a huge company and it takes a bit longer to get through the red tape of allocating a license.

Anyway, so far it has worked for me. I am using SciWare's approach regarding making the class's private data cluster contain a single DVR to a Strict TypeDef (I can also see advantages to multiple DVRs). I have hit some deadlock situations where IPEs have competed, but that was rather poor implementation on my part. I know I will hit more as I progress, but it seems better appraoch from a class usability standpoint. All of the IPEs are held inside the class's methods (typicaly data access methods). The calling VIs do not know anything about DVRs and DynamicDispatch works great. What is a real pain is debugging. Putting a probe on a wire that represents an object AND that object uses DVRs as member variables - all you see is the refnums! I am not surprised at this. I would like a way to dereference these values in the debugger. Am I missing something?

Link to comment

Given that the desire for a new API is almost always driven by the layer closest to the user, when the core changes, the outer layer will want to change anyway, so if there's a tight binding (inheritance) between core and outer layer, so what? Not having that binding doesn't generally seem to save you. Sure you *could* avoid rewriting the outer layer when the core changes, but if the whole point of changing the core is so you *can* rewrite the outer, then you're going to rewrite the outer.

I agree with everything you said, but this last paragraph highlights why I now avoid deep inheritance trees. We have an internal hierarchy that includes classes for various instruments, products that need to be tested, protocols, etc. The intent is to use this api as the foundation for all our test tools. If we come across a new requirement the api doesn't support, we *can't* change the outer layer without breaking other applications. So we either have to fork the api for that application (which we have done) or implement hacks (which we have also done.)

As you say, there is no "right" answer. It's just a matter of picking your poison.

I need to grab Visio or something.

I actually like Star UML better than Visio. Plus it's free. smile.gif

I am using SciWare's approach regarding making the class's private data cluster contain a single DVR to a Strict TypeDef.

I finally had a chance to look at the Bike/Racer implementation this morning. That looks like a very good way to create by ref class hierarchies. I'll have to play with it more to see how it fits into my use case. (Which means I'll have to remember what my use case was...wacko.gif )

Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use.