在继承树类中避免向下转型

3

我对C++相对陌生,现在在我的设计中面临一个问题:不可避免地需要进行向下转换。我知道这通常是糟糕设计的标志,所以我想知道有什么更好的方法来解决这个问题。

我有一个代表几何帧树并允许它们之间进行几何变换的类 Frame

class Frame
{
    private:
      Frame *_parent;
      std::vector<Frame*> _children;

    public:
      Frame* getParent() const;
      std::vector<Frame*> getChildren() const;
      ... (extra methods for geometrical transformations)
}

我现在想创建一个新的Frame子类,MechanicalFrame,以添加一些处理动态属性的功能。

class MechanicalFrame
{
   private:
     double mass;
     ...
   public:
     void compute();
 }

我的问题是,“compute”方法需要实现一些递归的逻辑,因此它可能包含以下内容:

MechanicalFrame::compute()
{
   for element in getChildren():
     element.compute();
}

然而,由于getChildren返回一个Frame*vector而不是MechanicalFrame*,因此我需要在这一点上进行static_cast。我已经认真思考了这个问题,但我找到的解决方案都不能完全令我满意:
解决方案1)静态转换:某种程度上表明设计不良。
解决方案2)将计算方法添加到基类(Frame)中,并使用虚拟实现,即抛出异常:强制基于派生类的父类实现似乎很不自然。
解决方案3)完全分离MechanicalFrameFrame:这意味着需要重新实现Frame中已有的许多功能。
非常感谢您的帮助。提前致以万分感谢 :)

在基类中创建Frame::compute()并将其设为virtual。问题是 - 你打算实例化基类吗?也许将Frame定义为抽象类会是个好主意。 - pSoLT
如果您确定MechanicalFrame::getChildren()中的所有Frame*指针都指向MechanicalFrame实例,那么使用static_cast应该没有问题。请确保在调试版本中使用dynamic_cast + assert来捕获错误。 - Vittorio Romeo
2
不同意,最好在派生类中使用addChild/getChildren方法。 - alexeykuzmin0
如何将孩子添加到“MechanicalFrame”中?如何强制确保它们也是“MechanicalFrame”? - Chris Drew
@ChrisDrew:只有在构建子节点时,才会将子节点添加到父节点中。除了用户之外,没有任何东西可以防止将Frame作为MechanicalFrame的子节点。 - Juanma García
显示剩余5条评论
4个回答

3

使用多态行为,使用您的解决方案2)

您可以遵循以下模式(接口 -> 基类 -> 派生类)

class IFrame
{
public:
    virtual void compute()=0;
}

class Frame:public IFrame
{
public:
    virtual void compute() {/*nothing to do*/}
}

class MechanicalFrame:public Frame
{
public:
    virtual void compute() {/*your implementation with mass*/}
}

1
这是我在注释中写的。 - pSoLT

2

如果你确定 MechanicalFrame::getChildren() 中的所有 Frame* 指针都指向 MechanicalFrame 实例,那么使用 static_cast 不会有任何问题。在调试版本中确保使用 dynamic_cast + assert 来捕获错误。

void MechanicalFrame::compute()
{
    for(auto frame_ptr : getChildren())
    {
        downcast<MechanicalFrame*>(frame_ptr)->compute();
    }
}

downcast类似于:

template <typename TOut, typename T>
auto downcast(T* ptr)
{
    static_assert(std::is_base_of<T, TOut>{});

    assert(ptr != nullptr);
    assert(dynamic_cast<TOut>(ptr) == ptr);

    return static_cast<TOut>(ptr);
}
< p >请注意,这里有一个性能优势,因为您避免了虚拟分派。在gcc.godbolt.org上玩弄此片段以查看生成的汇编中的差异

(要更彻底地实现downcast,请参见我的Meeting C++ 2015闪电谈“有意义的转换”或我vrm_core中的当前实现。)



1
另一个选择是使用访问者模式:

class Frame;
class MechanicalFrame;

class FrameVisitor
{
public:
    virtual ~FrameVisitor() = default;

    virtual void visit(Frame&) = 0;

    virtual void visit(MechanicalFrame&) = 0;
};

class Frame
{
public:
    virtual void accept(FrameVisitor& visitor)
    {
        visitor.visit(*this);
    }

    void acceptRecursive(FrameVisitor& visitor)
    {
        accept(visitor);

        for (Frame* child : getChildren())
        {
            child->acceptRecursive(visitor);
        }
    }

    ...
};

class MechanicalFrame : public Frame
{
public:
    virtual void accept(FrameVisitor& visitor) override
    {
        visitor.visit(*this);
    }

    ...
};

然后客户端代码将是:
class ConcreteVisitor : public FrameVisitor
{
public:
    virtual void visit(Frame& frame) override
    {
        // Deal with Frame (not a subclass) object.
    }

    virtual void visit(MechanicalFrame& frame) override
    {
        // Deal with MechanicalFrame object.
    }
};

Frame root = ...;
ConcreteVisitor visitor;
root.acceptRecursive(visitor);

一般而言,访问者模式允许您遍历异构对象的层次结构并在它们上执行操作,而无需进行类型转换。当预期操作数量将增加而类型层次结构基本稳定时,它最为有用。


0

既然您要求新想法,我将不会详细解释您在解决方案1-3中提到的任何内容。

您可以向MechanicalFrame类添加额外功能,将其类别为MechanicalFrame类和所有其他类别的children分开处理,像这样:

class Frame {
public:
    std::vector<Frame*> getChildren(); // returns children
    void addChild(Frame* child);       // adds child to children
private:
    std::vector<Frame*> children;
}

class MechanicalFrame : public Frame {
public:
    void compute();
    std::vector<MechanicalFrame*> getMechanicalChildren(); // returns mechanical_children
    void addChild(MechanicalFrame* child);                 // adds child to mechanical_children
private:
    std::vector<MechanicalFrame*> mechanical_children;
}

compute 的一种可能的实现如下:

void MechanicalFrame::compute() {
    ...
    for (auto* child : getMechanicalChildren()) {
        child->compute();
    }
}

UP: 就我所理解的来说,类型转换的问题之一是代码在对象的实际类别不同时表现出非常不同的行为,而我们不能用子类替代父类对象(参见 Liskov principle)。本答案中描述的方法实际上改变了使用您的Frame机械结构的原则,允许以这样一种方式添加MechanicalFrame子级,即它们在compute方法中被忽略。


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