6

I have a base class, Primitive, from which I derive several other classes--Sphere, Plane, etc.

Primitive enforces some functionality, e.g intersect(), on its subclasses through pure virtual functions. The computation of intersect depends on instance data, so it makes sense to have it as a member method.

My problem arises in the following: I want every derived instance to be able to identify its type, say through a std::string type() member method. As all instances of the same class will return the same type, it makes sense to make type() a static method. As I also want every Primitive subclass to implement this method, I would also like to make it a pure virtual function, like intersect() above.

However, static virtual methods are not allowed in C++. C++ static virtual members? and Can we have a virtual static method ? (c++) ask similar questions but they do not include the requirement of enforcing the function on derived classes.

Can anyone help me with the above?

Community
  • 1
  • 1
wsaleem
  • 594
  • 8
  • 25
  • 1
    If you are desiring polymorphism why would you want a static type? Use base class pointers in your interface and let it dynamically figure out the type at run time. That's kind of the whole point to virtual methods. – AJG85 May 08 '12 at 16:11
  • How would you call a virtual static? – John Dibling May 08 '12 at 16:34
  • I was thinking of making the call through an instance, e.g.[http://stackoverflow.com/questions/325555/c-static-member-method-call-on-class-instance] or even through the `this` pointer, e.g. [http://publib.boulder.ibm.com/infocenter/lnxpcomp/v8v101/index.jsp?topic=%2Fcom.ibm.xlcpp8l.doc%2Flanguage%2Fref%2Fcplr039.htm] – wsaleem May 08 '12 at 16:43
  • +1 for stating what the actual problem is. – Luchian Grigore May 08 '12 at 16:57
  • Consider changing the title to reflect that fact that intersect() has to do with _pairs_ of child classes. The question becomes simpler/different if you want methods concerning just their own subclass. – einpoklum Aug 20 '14 at 10:47

4 Answers4

6

Let's think about this for a second. I'm sure you don't only have 2 sublcasses, so let's generalize this.

First things that come to mind are code duplication, extensibility and closeness. Let's expand on these:

Should you want to add more classes, you should change code in the least places possible.

Because the intersect operation is commutative, the code for intersecting A and B should be in the same place as the code for intersecting B and A, so keeping the logic inside the classes themselves is out of the question.

Also, adding a new class shouldn't mean you have to modify existing classes, but rather extend a delegate class (yes, we're going into patterns here).

This is your current structure, I assume (or similar, probably a return type for intersect, but not important for now):

struct Primitive
{
    virtual void intersect(Primitive* other) = 0;
};
struct Sphere : Primitive
{
    virtual void intersect(Primitive* other)
};
struct Plane : Primitive
{
    virtual void intersect(Primitive* other);
};

We already decided we don't want the intersection logic inside Plane or Sphere, so we create a new class:

struct Intersect
{
    static void intersect(const Sphere&, const Plane&);
    //this again with the parameters inversed, which just takes this
    static void intersect(const Sphere&, const Sphere&);
    static void intersect(const Plane&, const Plane&);
};

This is the class where you'll be adding the new functions, and the new logic. For example, if you decide to add a Line class, you just add the methods intersec(const Line&,...).

Remember, when adding a new class, we don't want to change existing code. So we can't check the type inside your intersect functions.

We can create a behavior class for this (strategy pattern), which behaves differently depending on type, and we can extend afterwards:

struct IntersectBehavior
{  
    Primitive* object;
    virtual void doIntersect(Primitive* other) = 0;
};
struct SphereIntersectBehavior : IntersectBehavior
{
    virtual void doIntersect(Primitive* other)
    {
        //we already know object is a Sphere
        Sphere& obj1 = (Sphere&)*object;
        if ( dynamic_cast<Sphere*>(other) )
            return Intersect::intersect(obj1, (Sphere&) *other);
        if ( dynamic_cast<Plane*>(other) )
            return Intersect::intersect(obj1, (Plane&) *other);

        //finally, if no conditions were met, call intersect on other
        return other->intersect(object);
    }
};

And in our original methods we'd have:

struct Sphere : Primitive
{
    virtual void intersect(Primitive* other)
    {
        SphereIntersectBehavior intersectBehavior;
        return intersectBehavior.doIntersect(other);
    }
};

An even cleaner design would be implementing a factory, to abstract out the actual types of the behavior:

struct Sphere : Primitive
{
    virtual void intersect(Primitive* other)
    {
        IntersectBehavior*  intersectBehavior = BehaviorFactory::getBehavior(this);
        return intersectBehavior.doIntersect(other);
    }
};

and you wouldn't even need intersect to be virtual, because it would just do this for every class.

If you follow this design

  • no need to modify existing code when adding new classes
  • have the implementations in a single place
  • extend only IntersectBehavior for each new type
  • provide implementations in the Intersect class for new types

And I bet this could be perfected even further.

Luchian Grigore
  • 253,575
  • 64
  • 457
  • 625
  • Thanks for the detailed answer. I suppose my economy of expression in the original post led you astray. `intersect()` takes `const Ray&` as an argument, and not (a pointer to) other `Primitive` instances. It seems reasonable that each `Primitive` class knows how it intersects with a `Ray`. Moving `intersect` outside would require the custom `Intersect` class to access `Primitive` internals which violates encapsulation. – wsaleem May 08 '12 at 17:56
  • Your type identification eventually boils down to a `dynamic_cast`, the use of which, I have generally gathered, is not encouraged. Any views? – wsaleem May 08 '12 at 18:24
  • @wsaleem well, netiher is having an `id` member. But `dynamic_cast` already exists, why reinvent the wheel? – Luchian Grigore May 08 '12 at 18:35
2

For the reasons they discussed in the link you provided, you cannot make a virtual member static.

Your question about the requirement of enforcing the function on derived classes is handled by making the function pure virtual in the abstract base class which will enforce that derived classes must implement the function.

dag
  • 2,168
  • 2
  • 16
  • 15
1

As all instances of the same class will return the same type, it makes sense to make type() a static method.

No it does not. You use a static method when you don't need an instance of an object to call the function. In this case you're trying to identify the type of the object, so you do need an instance.

All method bodies are shared by all objects anyway, so there's no need to worry about duplication. The one exception is when functions are inline, but the compiler will do its best to minimize the overhead and may turn it non-inline if the cost is too great.

P.S. Requiring a class to identify itself outside of the class hierarchy is usually a bad code smell. Try to find another way.

Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
  • `"You use a static method when you don't need an instance ... so you do need an instance."` Thanks, that is very true. I had not thought of that. `"All method bodies are shared by all objects anyway, so there's no need to worry about duplication."` Another good point. – wsaleem May 08 '12 at 18:05
  • Both you and @ajg85 find my design curious. Let me elaborate and you may then suggest a better alternative. I want to call a `Primitive::emitXml()` method through a `Primitive*` object. The emitted XML contains, e.g. ` ... `, depending on the type of `Primitive` object the pointer points to. I need the `type()` function to fill in the value of the `type` attribute in the `emitXml()` function. – wsaleem May 08 '12 at 18:33
  • @wsaleem, that is indeed a legitimate use case. Much of the time the intent is to use it in `if` statements to get different behavior depending on the object type, and that's the code smell I was referring to. – Mark Ransom May 08 '12 at 18:44
0

You could have a non-static virtual method calling the static one (or returning a static string), appropriately implemented in each derived class.

#include <iostream>
#include <string>

struct IShape {
  virtual const std::string& type() const =0;
};

struct Square : virtual public IShape {
  virtual const std::string& type() const { return type_; }
  static std::string type_;
};
std::string Square::type_("square");

int main() {

  IShape* shape = new Square;
  std::cout << shape->type() << "\n";

}

Note that you will have to implement the type() method for every subclass anyway, so the best you can do is for the string to be static. However, you may consider using an enum instead of a string, do avoid unnecessary string comparisons in your code.

Now, going back to the fundamentals of the problem, I think the design is somewhat flawed. You cannot really have a general intersection function that operates on all kinds of shapes, because the types of shapes resulting from intersections vary greatly, even for the same type of shape (two planes can intersect in a plane, a line, or not intersect at all, for example). So in trying to provide a general solution, you will find yourself performing these kind of type checks all over the place, and this will grow unmaintainably the more shapes you add.

juanchopanza
  • 223,364
  • 34
  • 402
  • 480
  • Thanks, I like the idea of returning a static string through a virtual method. But then, I would have a `static std::string myType` declaration in every subclass. This seems like code duplication. Can I not use inheritance to avoid it? – wsaleem May 08 '12 at 16:14
  • You can't really get away from the virtual method for each subclass anyway, unfortunately. I have added some more info and comments though. It is a tricky problem... – juanchopanza May 08 '12 at 17:11
  • @juanchopanza you can, see my answer. – Luchian Grigore May 08 '12 at 17:17
  • @LuchianGrigore I meant in the context of having a method that returns a static identifier of the class, not in the context of a reasonable design :-) – juanchopanza May 08 '12 at 17:22
  • @juanchopanza I am now thinking, how about a `return "square"` in `Square::type()` and ditching the `static std::string` instance? The compiler might even optimize for the constant string `"square"` in some way. Any views? – wsaleem May 08 '12 at 18:02