How to enforce a static member on derived classes?

2020-03-19 02:49发布

问题:

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?

回答1:

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.



回答2:

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.



回答3:

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.



回答4:

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.