【问题标题】:How to enforce a static member on derived classes?如何在派生类上强制使用静态成员?
【发布时间】:2012-05-17 03:26:29
【问题描述】:

我有一个基类Primitive,从中派生出其他几个类——SpherePlane 等。

Primitive 通过纯虚函数在其子类上强制执行某些功能,例如intersect()intersect 的计算依赖于实例数据,因此将其作为成员方法是有意义的。

我的问题出现在以下几点: 我希望每个派生实例都能够识别其类型,例如通过std::string type() 成员方法。由于同一类的所有实例都将返回相同的类型,因此将 type() 设为 static 方法是有意义的。因为我也想让每个Primitive子类都实现这个方法,所以我也想让它成为一个纯虚函数,就像上面的intersect()一样。

但是,C++ 中不允许使用静态虚拟方法。 C++ static virtual members?Can we have a virtual static method ? (c++) 问类似的问题,但它们不包括在派生类上强制执行函数的要求。

谁能帮我解决上述问题?

【问题讨论】:

  • 如果您想要多态性,为什么要使用静态类型?在你的接口中使用基类指针,让它在运行时动态地找出类型。这就是虚拟方法的重点。
  • 如何称呼虚拟静态?
  • 我正在考虑通过实例进行调用,例如 [stackoverflow.com/questions/325555/… 甚至通过 this 指针,例如[publib.boulder.ibm.com/infocenter/lnxpcomp/v8v101/…
  • +1 用于说明实际问题。
  • 考虑更改标题以反映 intersect() 与 pairs 子类有关的事实。如果您想要只涉及他们自己的子类的方法,问题就会变得更简单/不同。

标签: c++ methods static virtual


【解决方案1】:

由于同一类的所有实例都将返回相同的类型,因此将 type() 设为静态方法是有意义的。

不,它没有。当您不需要对象的实例来调用函数时,您可以使用静态方法。在这种情况下,您正在尝试识别对象的类型,因此您确实需要一个实例。

无论如何,所有方法体都由所有对象共享,因此无需担心重复。一个例外是函数是内联的,但编译器会尽最大努力将开销降至最低,如果成本过高,可能会将其变为非内联。

附:要求一个类在类层次结构之外标识自己通常是一种不好的代码味道。尝试另辟蹊径。

【讨论】:

  • "You use a static method when you don't need an instance ... so you do need an instance." 谢谢,确实如此。我没有想到这一点。 "All method bodies are shared by all objects anyway, so there's no need to worry about duplication." 另一个好点。
  • 你和@ajg85 都觉得我的设计很奇怪。让我详细说明一下,然后您可能会提出更好的选择。我想通过Primitive* 对象调用Primitive::emitXml() 方法。发出的 XML 包含,例如<Primitive type='Triangle'> ... </Primitive>,取决于指针指向的 Primitive 对象的类型。我需要type()函数填写emitXml()函数中type属性的值。
  • @wsaleem,这确实是一个合法的用例。大部分时间的目的是在if 语句中使用它来根据对象类型获得不同的行为,这就是我所指的代码异味。
【解决方案2】:

您可以有一个非静态虚拟方法调用静态方法(或返回静态字符串),并在每个派生类中适当地实现。

#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() 方法,因此您能做的最好的事情就是使字符串保持静态。但是,您可以考虑使用enum 代替字符串,请避免在代码中进行不必要的字符串比较。

现在,回到问题的根本,我认为设计有些缺陷。您不能真正拥有适用于所有形状的通用相交函数,因为相交产生的形状类型差异很大,即使对于相同类型的形状(两个平面可以在一个平面、一条线相交或不相交全部,例如)。因此,在尝试提供通用解决方案时,您会发现自己到处都在执行此类类型检查,并且随着您添加的形状越多,这种检查就会变得难以维护。

【讨论】:

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

让我们考虑一下。我确定您不仅有 2 个子类,所以让我们概括一下。

首先想到的是代码重复、可扩展性和紧密性。让我们扩展这些:

如果您想添加更多类,您应该在尽可能少的地方更改代码。

因为intersect操作是可交换的,所以AB相交的代码应该和BA相交的代码在同一个地方,所以将逻辑保留在类本身是不可能的。

另外,添加一个新类并不意味着您必须修改现有类,而是扩展一个委托类(是的,我们将在这里讨论模式)。

这是您当前的结构,我假设(或类似的,可能是 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&amp;,...)

请记住,添加新类时,我们不想更改现有代码。所以我们无法检查相交函数中的类型。

我们可以为此创建一个行为类(策略模式),它的行为根据类型而有所不同,我们可以在之后进行扩展:

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&amp; 作为参数,而不是(指向)其他 Primitive 实例的(指针)。每个Primitive 类都知道它如何与Ray 相交似乎是合理的。将intersect 移到外部将需要自定义Intersect 类来访问违反封装的Primitive 内部。
  • 您的类型识别最终归结为dynamic_cast,我通常认为不鼓励使用它。有意见吗?
  • @wsaleem 好吧,netiher 有一个id 成员。但是dynamic_cast已经存在了,为什么要重新发明轮子?
【解决方案4】:

由于他们在您提供的链接中讨论的原因,您不能将虚拟成员设为静态。

您关于在派生类上强制执行函数的要求的问题是通过在抽象基类中使函数纯虚拟来处理的,这将强制派生类必须实现该函数。

【讨论】:

    猜你喜欢
    • 2019-11-13
    • 2018-11-30
    • 1970-01-01
    • 2023-03-23
    • 2021-09-24
    • 1970-01-01
    • 1970-01-01
    • 2011-06-22
    • 1970-01-01
    相关资源
    最近更新 更多