【问题标题】:Avoiding RTTI in a design在设计中避免 RTTI
【发布时间】:2011-05-24 18:33:48
【问题描述】:

我有一个 Visual Studio 2008 C++ 应用程序,其中包含从一个公共基础派生的几种类型的对象。例如:

class Base
{
public:
    std::string Time() const { /*return formatted time*/; };
private:
    SYSTEMTIME time_;
};

class Foo : public Base
{
public:
    const char* FooFunc() const { return "Hello from foo!"; };
};

typedef std::vector< Base > BaseList;

class Bar : public Base
{
public:
    const char* BarFunc() const { return "Hello from bar!"; };

    void push_back( const Base& obj ) { list_.push_back( obj ); };
    BaseList::const_iterator begin() const { return list_.begin(); };
    BaseList::const_iterator end() const { return list_.end(); };

private:
    BaseList list_;
};

这些对象存储在std::vector&lt; Base &gt; 中。我需要输出每个FooBar 类中的信息以及存储在基础中的信息。 但是,我想避免 RTTI

int main( int, char** )
{
    BaseList list;

    Foo foo;
    Bar bar;
    Foo foo2;

    list.push_back( foo );
    list.push_back( bar );
    bar.push_back( foo2 );

    for( BaseList::const_iterator it = list.begin();
         it != list.end();
         ++it )
    {
        printf( "%s ", it->Time() );

        // print Foo information for objects of type Foo
        // OR print Bar information for objects of type Bar.
        // Descend in to objects of type Bar to print its children.
    }
    return 0;
}

在这种情况下,所需的输出将是:

11:13:05 Hello from foo!
11:22:14 Hello from bar!
    11:26:04 Hello from foo!

我可以对此设计进行哪些更改,以避免使用 RTTI 作为解决方案,但仍允许我在嵌套树状结构中存储具有不同功能的对象,如 FooBar

谢谢, 保罗H

【问题讨论】:

  • @Fred:我正要回答它,说你做了什么。将您的评论转换为答案,其他人会更容易看到它。
  • 只是好奇:为什么要避免 RTTI?

标签: c++ data-structures


【解决方案1】:

我有一个 Visual Studio 2008 C++ 应用程序,其中包含从一个公共基础派生的几种类型的对象。 ... 这些对象存储在 std::vector 中。我需要输出每个 Foo 和 Bar 类中的信息以及存储在基础中的信息。但是,我想避免 RTTI。 ...我可以对此设计进行哪些更改,以避免使用 RTTI 作为解决方案,但仍允许我将具有不同功能的对象(如 Foo 和 Bar)存储在嵌套的树状结构中?

重要提示:您说您正在使用 std::vector&lt;Base&gt;,如果这是真的,您需要更改为 std::vector&lt;Base*&gt;(或 Boost 指针容器)以避免对象切片.

总结:你有一个基类和从它派生的类。您希望从基类派生的类根据它们的实际情况做事,但也希望基类提供对所有派生自它的类都有意义的方法。

答案:适当地重命名FooFuncBarFunc。让他们覆盖您的基类中的 virtual 函数:

class Base {
public:
    std::string Time() const { /*return formatted time*/; };
    // this is a pure virtual function (the "= 0" part)
    // there are other kinds of virtual functions that would also work
    virtual const char* Func() const = 0;
private:
    SYSTEMTIME time_;
};

class Foo : public Base {
public:
    const char* Func() const { return "Hello from foo!"; };
};

typedef std::vector<Base*> BaseList;

class Bar : public Base
{
public:
    const char* Func() const { return "Hello from bar!"; };

    void push_back( const Base& obj ) { list_.push_back( obj ); };
    BaseList::const_iterator begin() const { return list_.begin(); };
    BaseList::const_iterator end() const { return list_.end(); };

private:
    BaseList list_;
};

int main( int, char** )
{
    BaseList list;

    Foo foo;
    Bar bar;
    Foo foo2;

    // you normally don't want to do it this way, but since the
    // container won't outlive the stack objects you'll be safe.
    list.push_back(&foo);
    list.push_back(&bar);
    bar.push_back(&foo2);

    for(BaseList::const_iterator it = list.begin();
        it != list.end();
        ++it )
    {
        // the "extra" * was added because the list is a vector<Base*>
        // instead of a vector<Base>
        printf("%s ", *it->Time());

        // print Foo information for objects of type Foo
        // OR print Bar information for objects of type Bar.
        printf("%s\n", *it->Func());
        // Descend in to objects of type Bar to print its children.

        // This you'll need to use RTTI to know when you're looking at a
        // Bar object and need to descend into it
        bar* = dynamic_cast<Bar*>(*it);
        if (bar == NULL)
            continue;
        for (BaseList::const_iterator bit = bar->begin(); bit != bar->end(); ++bit)
            printf("\t%s\n", bit->Func());
    }
    return 0;
}

【讨论】:

    【解决方案2】:

    好的,根据 Alf 的建议,我将我的评论作为答案。

    甚至 RTTI 在这里也不起作用,因为您将 Base 对象存储在向量中。对象将是sliced,丢失派生类的所有信息。您需要存储Base 指针,最好是智能指针。此外,您没有虚拟功能。 RTTI 至少需要一个虚函数才能工作。

    使您的函数成为Base 中的纯虚方法,然后在每个派生类中以适当的行为覆盖它。这将使Base 成为一个抽象类,从而使切片问题变得不可能。

    【讨论】:

    • 即使没有切片,RTTI 也不适用于没有虚函数的类。
    【解决方案3】:

    听起来您想使用多态性。创建一个虚函数并让子类实现它。然后使您的列表成为指向基址的指针列表:

    class Base
    {
    public:
        virtual const char *Func() = 0; // pure virtual, no implementation
    };
    
    class Foo: public Base
    {
    public:
        virtual const char *Func() { return "Hello from foo!"; }
    };
    
    class Bar: public Base
    {
    public:
        virtual const char *Func() { return "Hello from bar!"; }
    };
    
    int main()
    {
        std::list<Base*> myList;
        myList.push_back(new Foo());
        myList.push_back(new Bar());
    
        for( std::list<Base*>::const_iterator it = myList.begin();
             it != myList.end();
             ++it )
        {
            printf( "%s ", (*it)->Func() );
        }
    
    // don't forget to delete your pointers.  Or better yet, use smart pointers
    
        return 0;
    }
    

    【讨论】:

      【解决方案4】:

      这是通过继承实现的普通运行时多态性。

      FooFuncBarFunc替换为继承自Base的纯虚方法DoTheOutput的实现,然后在循环中通过Base*调用该方法。如果您也需要Base 输出,那么Base::DoTheOutput 将不是纯的,并且会在子类实现完成派生类特定输出后调用。

      如果您需要在界面中保持BarFuncFooFunc 不变,可以将它们委托给新的DoTheOutput 函数。

      【讨论】:

      • +1。使用纯虚方法会使Base 抽象化,从而导致编译错误,从而使切片问题非常明显。
      【解决方案5】:

      首先,Bar 类有问题。由于您使用的是std::vector&lt; Base &gt;,因此您的对象被切片(即对象的 Base 部分的副本被插入向量中,而不是您的对象)。你会想要使用std::vector&lt; Base* &gt;,它是一个指针向量。

      其次,为了做你想做的事,你可以在基类中使用一个虚方法,该方法被 Foo 和 Bar 类覆盖。如果您认为您的应用程序需要多个遍历操作,您可以查看Visitor pattern

      【讨论】:

      • 访问者模式正是我想要的。谢谢。
      • @PaulH, @Sylvain - 这种方法确实允许您完整地保留派生类(所以 +1),但由于它们已经从 Base 继承,我很好奇为什么 Visitor 更可取。我认为这更麻烦。
      • 是的,在这种简单的情况下,我也认为它比较麻烦。但是我们在存在复合对象(Bar 实例)的情况下,如果递归下降的实例多于几个,访问者模式可能更容易使用。
      • @Steve Townsend - 访问者适用于您需要向逻辑上不属于该类的类添加行为。因此,如果成员函数确实不像 Base 所做的那样有意义(例如,如果 Base 表示一个表达式,对子树应用各种优化转换)它应该被忽略并成为改为访客。
      • @Steve, @Omnifarious:Visitor 让您“交换”轻松将类添加到(大)层次结构的能力,以轻松将虚拟方法添加到该层次结构中的基础。应该通过考虑是否更有可能引入额外的虚拟方法(在这种情况下使用访问者)或引入额外的类(在这种情况下,不要)来决定使用它。在使用 Visitor 时添加类(或在不使用时添加虚拟方法)当然是可能的,但它会很乏味且容易出错。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-08-03
      • 1970-01-01
      • 2010-10-01
      • 2015-09-15
      • 1970-01-01
      • 2013-03-02
      相关资源
      最近更新 更多