避免在继承的树类中进行下转换

Avoid downcasting in an inherited tree class

本文关键字:转换 继承      更新时间:2023-10-16

我对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();
}

我的问题是,"计算"方法需要实现一些递归逻辑,所以它会包含这样的东西:

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

然而,由于getChildren返回Frame*vector,而不是MechanicalFrame*,因此此时我需要生成static_cast。我对这个问题想了很多,但没有一个解决方案能让我完全满意:

解决方案1)静态投射:不知何故,它表明设计不好

解决方案2)将计算方法添加到具有伪实现的基类(Frame)中,即抛出异常:基于派生类强制实现父类似乎不自然。

解决方案3)从Frame完全拆分MechanicalFrame:这意味着重新实现Frame中已经可用的许多功能。

如有任何帮助,我们将不胜感激。非常感谢:)

使用多态行为,使用您的解决方案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*/}
}

如果您确定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);
}

(关于downcast的更彻底的实现,请参阅我的会议C++2015闪电演讲"有意义的演员阵容">或我在vrm_core中的当前实现。)


请注意,这里有一个性能优势,因为您可以避免virtual调度。在gcc.godbolt.org上播放这个片段,查看生成的程序集中的差异。

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

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);

通常,Visitor模式允许您遍历异构对象的层次结构,并在不进行类型转换的情况下对它们执行操作。当类型层次结构或多或少稳定时,操作数量预计会增长时,它最有用。

由于您在寻求新的想法,我不会详细解释您在解决方案1-3中所写的任何内容。

您可以为MechanicalFrame类添加额外的功能,将其childrenMechanicalFrame类和所有其他类中分离出来,如下所示:

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原理)。这个答案中描述的方法实际上改变了使用Frame的"机械性"的原则,允许以compute方法中忽略的方式添加MechanicalFrame子项。