Jump to content
  • Advertisement
Sign in to follow this  
Angelic Ice

C++ Virtuals hide information on the implementation

Recommended Posts

Hey everyone!

Text is down below, just wanted to provide some C++ish pseudo-code to help me expressing my issue ('...' defines omitted details):

class Interface_Tree {
 public: ...
  
}

class Tree : public Interface_Tree {
  public: ...
  private:
  	std::vector<Interface_Bucket> buckets;
  
}

class Interface_Bucket {
  public: ...
    add_object(Interface_Object* obj)
}

class Special_Bucket : public Interface_Bucket {
  public: 
  	add_object(Interface_Bucket_Object* obj) {
      /* 
      If `Interface_Bucket_Object` cannot be cast to 
      `Special_Bucket_Object_Category`, do nothing.
        
      This would require a dynamic_cast which I want to avoid, that
      probably cannot unless restructuring.
      
      Check what enum-type `Special_Bucket_Object_Category`
      If `VERY_IMPORTANT` 
      	use pointer `very_important_obj`.
      Else 
      	add to `objs`.
      */
    }
  private: 
  	std::vector<Interface_Bucket_Object*> objs;
    Interface_Bucket_Object* very_important_obj; 
}

class Interface_Bucket_Object {
  public: ...
}

enum Special_Bucket_Object_Category { IMPORTANT, VERY_IMPORTANT, SPAM };

class Special_Bucket_Object : public Interface {
  public: ...
  private: 
     Special_Bucket_Object_Category category;
}

I wanted to start programming to interfaces (or in C++, virtual classes) instead of actual classes (which are now just derived ones) in order to improve unit-testing etc., hence every class derives a base, that can be easily implemented differently.

But the issue is, if I have a Hash-Bucket data-structure class owning buckets that collect bucket-objects, how can the bucket differentiate characteristics of an implementation of the implemented bucket-object? Sure, I can do a dynamic-cast, if that fails just abort, but I do not want to do a dynamic-cast, because I would rather restructure my code.

I want my bucket-objects to have a flag (could also be called tag, category, ...) that notifies a bucket that this object needs the entire bucket, hence the enum in my code expressing the flags an object can raise.

If I would omit all interface-stuff, it would be fairly easy. My Bucket's add_object() method could easily only be called with that exact object and since the exact bucket-object type is now given, and not just some interface, accessing the enum is trivial via a getter-method.

And a last reminder: I'm not using virtual-classes to profit from inheritance in a sense of a bucket being able to collect a tons of different implementations, just one implementation that carries an enum. The bucket-tree won't own different types of buckets neither, just one implementation kind of bucket. The only reason I use inheritance is to enable easy unit-testing.

So, how can I access the object's category without dynamic-casting?

 

 

Share this post


Link to post
Share on other sites
Advertisement

There's basically no point posting a question about interfaces when you leave out all the content of the actual interface - that shows that you're not thinking in terms of the interface at all, because it's obviously irrelevant for your problem.

It sounds like you are looking for something like the Bridge design pattern, and/or the Abstract Factory pattern. If you can ensure (to a reasonable degree of certainty) that you are always generating the right derived class for whatever you're passing it into, you can just cast it as needed.

Share this post


Link to post
Share on other sites

Leaving out? What do you mean? The '...'? Those are nothing but constructors and remove/clear methods. But I wanted to take out one example that caused me trouble in fully designing the interface. None of them is actually implemented yet because I realised that inserting is already causing trouble.

And yes, I can ensure that they have the same type because there won't be another derived object-type. Derived_Bucket_A will have only one Derived_Bucket_Object_A, they are a "pair".

Originally I had a Bucket-Class and its with the signature of add_object(Categorised_Bucket_Object* object), and since Categorised_Bucket_Object has the enum as a member, it's easy to just attain it via getter.

But surely, once everything becomes an interface, it's difficult to attain a value that only is part of the implementation.

I will look into the Bridge design pattern : ) Thanks for your help!

Edited by Angelic Ice

Share this post


Link to post
Share on other sites

I'm just saying that you can't have a meaningful discussion about interfaces when you are not talking about the interface itself. The whole point of the interface is that the methods it exposes are the way in which you interact with the objects. If you're trying to do other things that cannot be implemented in terms of those methods, it's a potential sign that you don't have a good interface, or that it's inappropriate for the types in question. You don't typically need values that are only part of the implementation.

Sometimes people create an interface or a base class because they feel that they should be able to smash a bunch of different objects into the same data structure or container, only to then worry about how to get the derived type back later. The problem there is wrongly assuming objects are similar when they actually are not.

Share this post


Link to post
Share on other sites
2 hours ago, Angelic Ice said:

std::vector<Interface_Bucket> buckets;

Are you sure this matches in your code?

You need to store pointers to the objects rather than the base objects themselves. 

The way the language usually enforces this is for your code make the base class abstract so it cannot be instantiated.  They are commonly called an ABC, or Abstract Base Class, because of this.  Except in rare cases, only the endmost leaf classes should be concrete, everything below it in the hierarchy should be abstract.

2 hours ago, Angelic Ice said:

If `Interface_Bucket_Object` cannot be cast to `Special_Bucket_Object_Category`, do nothing. This would require a dynamic_cast which I want to avoid, that probably cannot unless restructuring.

In that case, are you certain you are following LSP? Are the sub-objects TRULY able to be substituted for each other?

Code should be able to move back up the hierarchy to base class pointers, but should never need to move back out the hierarchy, instead using functions in the interface to do whatever work needs to be done. 

As a code example:

for( BaseThing* theThing : AllTheThings ) {
   theThing->DoStuff();
}

versus:

for( BaseThing* theThing : AllTheThings ) {
  // Might also be a dynamic cast, or RTTI ID, or similar
  auto thingType = theThing->GetType();
  
  if( thingType == ThingType.Wizard ) {
     theThing->DoWizardStuff();
   } else if( thingType == ThingType.Warrior ) {
     theThing->DoWarriorStuff();
   } else ...
    ... // Variations for each type of thing
}

 

Also, in general it is a bad idea to have public virtual function.  Here is some reading on that by one of the most expert among C++ experts.   This is different from languages like Java where interface types are created through a virtual public interface. Some people who jump between languages forget this important detail.  As the code grows those public virtual functions end up causing breakages as people implement non-substitutable code in leaf classes, as it appears you have done here.

Share this post


Link to post
Share on other sites

Kylotan, I guess I understand what you mean. Maybe I'm trying to much here already, I just wanted to use virtuals everywhere to substitute them in unit-tests. Maybe I should only do this to actual dependencies and that's the origin of bad design.

5 hours ago, frob said:
8 hours ago, Angelic Ice said:

std::vector<Interface_Bucket> buckets;

Are you sure this matches in your code?

You are right, this is wrong. I meant: std::vector<Interface_Bucket*>, a vector of pointers.

I just read about LSP and am not totally sure if what I do is against that, but I highly assume it is. Usually, bucket-objects do not require a category, just as in most implementations, that's what the interface would convey. However in my implementations, some bucket-objects would have an enum giving more detail about what category they have, so more of a specialised customised bucket-data-structure. Since this would require a getter, they would violate the LSP, I assume.

Thanks a lot, your posts and linked resources helped me a bunch already, I was going forth and back in my head for a few days now and was not really sure how to formulate my situation into an internet-search : )

 

Edited by Angelic Ice

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!