0

我有一组类,它们描述了一组可以容纳东西并对它们做事的逻辑框。我有

struct IBox // all boxes do these
{
    ....
}

struct IBoxCanDoX // the power to do X
{
    void x();
}

struct IBoxCanDoY // the power to do Y
{
    void y();
}

我想知道对于这些类的客户来说,处理这些可选功能的“最好”或者可能只是“最喜欢”的习语是什么

一种)

    if(typeid(box) == typeid(IBoxCanDoX))
    {
         IBoxCanDoX *ix = static_cast<IBoxCanDoX*>(box);
         ix->x();
    }

b)

   IBoxCanDoX *ix = dynamic_cast<IBoxCanDoX*>(box);
    if(ix)
    {
         ix->x();
    }

C)

if(box->canDoX())
{
    IBoxCanDoX *ix = static_cast<IBoxCanDoX*>(box);
    ix->x(); 
}

d)现在不同的类结构

struct IBox
{
    void x();
    void y();
}
...
box->x(); /// ignored by implementations that dont do x

e) 相同,除了

box->x() // 'not implemented' exception thrown

f) 显式测试功能

if(box->canDoX())
{
   box->x();
} 

我相信还有其他人。

编辑:

只是为了使用例更清晰

我通过交互式 ui 将这些东西暴露给最终用户。他们可以输入“make box do X”。我需要知道box是否可以做x。或者我需要禁用 'make current box do X' 命令

EDIT2:感谢所有回答者

正如诺亚·罗伯茨(Noah Roberts)指出的那样(a)不起作用(解释了我的一些问题!)。我最终做了(b)和一个轻微的变体

   template<class T>
    T* GetCurrentBox()
    {
       if (!current_box)
          throw "current box not set";
       T* ret = dynamic_cast<T*>(current_box);
       if(!ret)
          throw "current box doesnt support requested operation";
       return ret;
    }
    ...
    IBoxCanDoX *ix = GetCurrentBox<IBoxCanDoX>();
    ix->x();

并让 UI 管道很好地处理异常(我并没有真正抛出裸字符串)。

我也打算探索访客

4

8 回答 8

4

对于 C++ 中这样的双重调度问题,我建议使用访问者模式:

class IVisitor
{
public:
    virtual void Visit(IBoxCanDoX *pBox) = 0;
    virtual void Visit(IBoxCanDoY *pBox) = 0;
    virtual void Visit(IBox* pBox) = 0;
};

class IBox // all boxes do these
{
public:
    virtual void Accept(IVisitor *pVisitor)
    {
        pVisitor->Visit(this);
    }
};

class BoxCanDoY : public IBox
{
public:
    virtual void Accept(IVisitor *pVisitor)
    {
        pVisitor->Visit(this);
    }
};
class TestVisitor : public IVisitor
{
public:
    // override visit methods to do tests for each type.
};

void Main()
{
    BoxCanDoY y;
    TestVisitor v;
    y.Accept(&v);
}
于 2010-07-26T16:36:05.650 回答
2

在您给出的选项中,我会说 b 或 d 是“最好的”。但是,需要做很多此类事情通常表明设计不佳,或者表明使用动态类型语言而不是 C++ 更好地实现的设计。

于 2010-07-26T16:36:22.397 回答
1

A 和 B 需要运行时类型识别 (RTTI),如果您进行大量检查,可能会更慢。我个人不喜欢“canDoX”方法的解决方案,如果出现这种情况,设计可能需要升级,因为您暴露了与课程无关的信息。

如果您只需要执行 X 或 Y,取决于类,我会在 IBox 中使用一个虚拟方法,该方法在子类中被覆盖。

class IBox{
   virtual void doThing();
}
class IBoxCanDoX: public IBox{
   void doThing() {  doX(); }
   void doX();
}
class IBoxCanDoY: public IBox{
   void doThing() {  doY(); }
   void doY();
}

box->doThing();

如果该解决方案不适用或者您需要更复杂的逻辑,请查看访问者设计模式。但是请记住,当您定期添加新类或更改/添加/删除方法时,访问者模式不是很灵活(但对于您提出的替代方案也是如此)。

于 2010-07-26T16:58:44.257 回答
1

如果您使用“I”前缀来表示“接口”,就像它在 Java 中的含义一样,这将使用 C++ 中的抽象基来完成,那么您的第一个选项将无法工作......所以那个选项就出来了。我已经用它来做一些事情了。

不要做'd',它会污染你的层次结构。保持你的界面干净,你会很高兴你做到了。因此,Vehicle 类没有踏板()函数,因为只有一些车辆可以踏板。如果客户端需要pedal() 函数,那么它确实需要知道那些可以的类。

远离 'e' 的原因与 'd' PLUS 相同,因为它违反了 Liskov 替换原则。如果客户端需要在调用某个类之前检查它是否响应了踏板(),以便它不会爆炸,那么最好的方法是尝试强制转换为具有该功能的对象。'f' 与支票相同。

'c' 是多余的。如果您按照应有的方式设置层次结构,则转换为 ICanDoX 足以检查 x 是否可以执行 X()。

因此,“b”成为您从给定选项中的答案。但是,正如 Gladfelter 所展示的,您在帖子中没有考虑过一些选项。

编辑说明:我没有注意到 'c' 使用了 static_cast 而不是动态的。正如我在回答中提到的那样,dynamic_cast 版本更干净,应该是首选,除非特定情况另有规定。它与以下选项类似,因为它会污染基本接口。

编辑2:我应该注意,关于'a',我已经使用了它,但我不像你在你的帖子中那样静态地使用类型。每当我使用 typeid 根据类型拆分流时,它始终基于运行时注册的内容。例如,打开正确的对话框以编辑一些未知类型的对象:对话框调控器根据它们编辑的类型向工厂注册。这使我在添加/删除/更改对象时不必更改任何流控制代码。在不同的情况下,我通常不会使用此选项。

于 2010-07-26T17:14:15.650 回答
0

如果您尝试从代码的偶然部分调用这些类操作中的任何一个,我建议您将该代码包装在模板函数中,并以相同的方式命名每个类的方法以实现鸭子类型,因此您的客户端代码看起来像这样.

template<class box>
void box_do_xory(box BOX){
    BOX.xory();
}
于 2010-07-26T16:43:37.137 回答
0

您的问题没有一般性的答案。一切都取决于。我只能说:
- 不要使用 a),改用 b)
- b) 很好,需要最少的代码,不需要虚拟方法,但是 dynamic_cast 有点慢
- c) 类似于 b) 但它更快(没有dynamic_cast)并且需要更多内存
-e)没有意义,您仍然需要发现是否可以调用该方法以便不引发异常
-d)比f)更好(要编写的代码更少)
-d ) e) 和 f) 产生的垃圾代码比其他代码多,但速度更快,内存消耗更少

于 2010-07-26T16:53:53.017 回答
0

我假设您在这里不仅会使用一种类型的对象。

我将列出您正在使用的数据,并尝试了解如何将其布置在内存中以进行数据驱动的编程。良好的内存布局应该反映您在类中存储数据的方式以及类在内存中的布局方式。一旦你有了基本的设计结构(不应该超过一张餐巾纸),我将开始根据你计划对数据执行的操作将对象组织到列表中。如果您打算对子集 X 中的对象集合 { Y } 执行 X(),我可能会确保从一开始就创建一个 Y 的静态数组。如果您希望偶尔访问整个 X,可以通过将列表收集到动态指针列表中来安排(使用 std::vector 或您最喜欢的选择)。

我希望这是有道理的,但是一旦实施,它就会提供简单直接的解决方案,易于理解且易于使用。

于 2010-07-26T20:31:25.947 回答
0

有一种通用的方法可以测试一个类是否支持某个概念,然后执行最合适的代码。它使用SFINAE hack。这个例子的灵感来自 Abrahams 和 Gurtovoy 的“C++ Template Metaprogramming”一书。如果函数doIt存在,它将使用x方法,否则它将使用y方法。您也可以扩展CanDo结构以测试其他方法。您可以根据需要测试任意数量的方法,只要可以唯一地解决doIt的重载。

#include <iostream>
#include <boost/config.hpp>
#include <boost/utility/enable_if.hpp>

typedef char yes;      // sizeof(yes) == 1
typedef char (&no)[2]; // sizeof(no)  == 2

template<typename T>
struct CanDo {
    template<typename U, void (U::*)()>
    struct ptr_to_mem {};

    template<typename U>
    static yes testX(ptr_to_mem<U, &U::x>*);

    template<typename U>
    static no testX(...);

    BOOST_STATIC_CONSTANT(bool, value = sizeof(testX<T>(0)) == sizeof(yes));
};

struct DoX {
    void x() { std::cout << "doing x...\n"; }
};

struct DoAnotherX {
    void x() { std::cout << "doing another x...\n"; }
};

struct DoY {
    void y() { std::cout << "doing y...\n"; }
};

struct DoAnotherY {
    void y() { std::cout << "doing another y...\n"; }
};

template <typename Action>
typename boost::enable_if<CanDo<Action> >::type
doIt(Action* a) {
    a->x();
}

template <typename Action>
typename boost::disable_if<CanDo<Action> >::type
doIt(Action* a) {
    a->y();
}

int main() {

    DoX         doX;
    DoAnotherX  doAnotherX;
    DoY         doY;
    DoAnotherY  doAnotherY;

    doIt(&doX);
    doIt(&doAnotherX);
    doIt(&doY);
    doIt(&doAnotherY);
}
于 2010-07-28T06:10:22.213 回答