如何在派生类中强制实现静态成员?

6
我有一个基类"Primitive',我从中派生出几个其他类--如'Sphere'、'Plane'等等。
通过纯虚函数,'Primitive'强制执行一些功能,例如'intersect()'。'intersect'的计算取决于实例数据,因此将其作为成员方法是有意义的。
我的问题出在以下方面: 我希望每个派生实例都能够通过'member method std::string type()'来识别其类型。由于所有相同类的实例将返回相同的类型,因此将'type()'设置为'static'方法是有意义的。由于我还希望每个“Primitive”子类实现此方法,因此我也想将它作为纯虚函数,就像上面的'intersect()'一样。
然而,在C++中不允许使用静态虚拟方法。 C++ static virtual members?Can we have a virtual static method ? (c++) 提出了类似的问题,但它们并没有包括强制要求在派生类中执行该函数的要求。
有人可以帮助我解决以上问题吗?

1
如果你想要多态性,为什么还要使用静态类型?在你的接口中使用基类指针,让它在运行时动态地确定类型。这就是虚方法的全部意义所在。 - AJG85
你会如何调用虚拟静态方法? - John Dibling
我在考虑通过实例来进行调用,例如[https://dev59.com/AHRC5IYBdhLWcg3wW_pk],甚至可以通过`this`指针来进行调用,例如[http://publib.boulder.ibm.com/infocenter/lnxpcomp/v8v101/index.jsp?topic=%2Fcom.ibm.xlcpp8l.doc%2Flanguage%2Fref%2Fcplr039.htm]。 - wsaleem
+1 表示指出实际问题。 - Luchian Grigore
考虑更改标题以反映intersect()与子类对的关系。如果您只想要涉及其自身子类的方法,那么问题会变得更简单/不同。 - einpoklum
4个回答

6
让我们仔细考虑一下。我相信你不仅有两个子类,所以让我们将其概括一下。首先想到的是代码重复、可扩展性和紧密性。让我们详细讨论一下:
如果你想添加更多类,应该尽可能少地修改代码。因为交集操作是可交换的,交错处理A和B的代码应该与交错处理B和A的代码放在同一个位置,所以把逻辑保留在类本身内部是不可能的。
此外,添加新类不应意味着你必须修改现有类,而应该扩展委托类(是的,我们要涉及模式了)。
下面是当前的结构(或类似的结构,可能是intersect的返回类型,但现在不重要):
struct Primitive
{
    virtual void intersect(Primitive* other) = 0;
};
struct Sphere : Primitive
{
    virtual void intersect(Primitive* other)
};
struct Plane : Primitive
{
    virtual void intersect(Primitive* other);
};

我们已经决定不要将交点逻辑放在PlaneSphere中,因此我们创建了一个新的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&);
};

这是你将添加新功能和新逻辑的类。例如,如果您决定添加一个Line类,只需添加方法intersec(const Line&,...)
请记住,在添加新类时,我们不想更改现有代码。因此,我们无法在您的交点函数中检查类型。
为此,我们可以创建一个行为类(策略模式),根据类型表现出不同的行为,并在以后进行扩展:
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);
    }
};

在我们原来的方法中,我们会有:

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

一个更清晰的设计是实现一个工厂,来抽象出行为的实际类型:
struct Sphere : Primitive
{
    virtual void intersect(Primitive* other)
    {
        IntersectBehavior*  intersectBehavior = BehaviorFactory::getBehavior(this);
        return intersectBehavior.doIntersect(other);
    }
};

如果您按照这个设计,甚至不需要intersect成为虚拟的,因为它会为每个类执行此操作。

如果您遵循这个设计:

  • 添加新类时无需修改现有代码
  • 将实现放在一个地方
  • 仅为每种新类型扩展IntersectBehavior
  • Intersect类中为新类型提供实现

我敢打赌,这甚至可以进一步完善。


感谢您提供详细的答案。我想我的原始帖子中表达经济性让您误解了。intersect()const Ray&作为参数,而不是(指向)其他Primitive实例的指针。每个Primitive类知道如何与Ray相交似乎是合理的。将intersect移动到外部需要自定义的Intersect类访问Primitive内部,这违反了封装。 - wsaleem
你的类型识别最终归结为 dynamic_cast,我通常了解到它的使用并不被鼓励。有什么看法吗? - wsaleem
@wsaleem 嗯,两者都没有 id 成员。但是 dynamic_cast 已经存在了,为什么要重复造轮子呢? - Luchian Grigore

2
基于你提供的链接中所讨论的原因,无法将虚拟成员变量变为静态。
当需要强制派生类实现某个函数时,可以在抽象基类中将该函数设置为纯虚拟函数,这样就可以强制要求派生类实现该函数。

1
由于同一类的所有实例将返回相同的类型,因此将type()设置为静态方法是有意义的。
不,它并不是。当您不需要对象实例来调用函数时,使用静态方法。在这种情况下,您正在尝试识别对象的类型,因此您确实需要一个实例。
所有方法体都由所有对象共享,因此无需担心重复。唯一的例外是函数是内联的情况,但编译器会尽力最小化开销,并在成本过高时将其转换为非内联。
附言:要求类在类层次结构之外标识自身通常是糟糕的代码味道。尝试找到另一种方法。

“当您不需要实例时,可以使用静态方法...所以您确实需要一个实例。” 谢谢,这非常正确。 “所有方法体都被所有对象共享,因此无需担心重复。”另一个很好的观点。 - wsaleem
你和@ajg85都觉得我的设计很奇怪。让我详细说明一下,然后你可以提出更好的替代方案。我想通过一个Primitive*对象调用Primitive::emitXml()方法。发出的XML包含例如<Primitive type='Triangle'> ... </Primitive>,具体取决于指针所指向的Primitive对象的类型。我需要type()函数来填充emitXml()函数中type属性的值。 - wsaleem
@wsaleem,确实是一个合法的用例。大多数情况下,意图是在if语句中使用它,以根据对象类型获得不同的行为,这就是我所指的代码异味。 - Mark Ransom

0
你可以在每个派生类中适当地实现一个非静态虚方法,调用静态方法(或返回静态字符串)。
#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";

}

请注意,您将需要为每个子类实现 type() 方法,因此您能做的最好是使字符串保持静态。但是,您可以考虑使用枚举而不是字符串,在代码中避免不必要的字符串比较。
现在,回到问题的基本原理,我认为设计有些缺陷。您无法真正拥有一个可对所有类型形状进行操作的通用交集函数,因为由交集产生的形状类型差异很大,即使对于相同类型的形状(例如,两个平面可以在平面、线或根本不相交)。因此,在试图提供通用解决方案时,您将发现自己在各个地方执行这些类型检查,并且随着您添加更多形状,这种情况会变得难以维护。

谢谢,我喜欢通过虚方法返回静态字符串的想法。但是,那么每个子类中都会有一个static std::string myType声明。这似乎是代码重复。我能不能使用继承来避免它? - wsaleem
不幸的是,你无论如何都无法摆脱每个子类的虚拟方法。但我已经添加了更多信息和注释。这是一个棘手的问题... - juanchopanza
@juanchopanza,你可以看看我的回答。 - Luchian Grigore
@LuchianGrigore 我的意思是在具有返回类的静态标识符的方法的情况下,而不是在合理设计的情况下。 - juanchopanza
@juanchopanza,我现在正在思考,在 Square::type() 中加入 return "square" 并且放弃使用 static std::string 实例怎么样?编译器甚至可以通过某种方式针对常量字符串 "square" 进行优化。有什么看法吗? - wsaleem

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接