【问题标题】:subclassing with or without virtuals vs performance vs convenience有或没有虚拟的子类化 vs 性能 vs 便利性
【发布时间】:2011-06-12 20:00:58
【问题描述】:

众所周知,STL 类在任何地方都不使用虚方法(STL 也不在任何地方使用继承,而且这两个事实是相互关联的),而且 STL 并不是唯一的。

让我们假设地球上存在其他性能怪胎[他们确实存在],这些性能怪胎在每个类都问自己“我需要这个类 X 的虚拟方法吗?”和“这个类 X 是否可以像 STL 类一样在没有任何虚拟的情况下实现更好的性能?”

缺少任何虚拟方法(包括 d'tor)使得多态性和子类化比使用“虚拟”基类更加困难。显然,“非虚拟”类非常适合作为基类。

问题:是否有技术(用于 c++)允许程序员一次性创建相同类 X 的两个版本,一个“非虚拟”版本 Xnv(用于性能)和“虚拟” “版本 Xv,用于子类化?如果不需要,请说明原因。

Post-note

人们回答 “如果您需要子类化,请使用 virtuals。如果不需要,请不要使用 virtuals”

这个建议有问题。几个问题。

1) 需求随着时间而变化。那时不需要从类 X 子类化,但现在需要,反之亦然。
2) 编写基类的人与编写派生类的人不同。从问题中可以清楚地看出这一点。人们有不同的思维类型、不同的判断、不同的需求。再次清除。
3) 因此,不同的程序员在回答“从类 X 继承是否有意义?”之类的问题时,会给出不同的答案。这是主观的,没有一成不变的答案。
4)它与问题的要求相矛盾。

因此,我们希望满足频谱的两端——这在工程中经常发生——这就是问题背后的动机。

动机过于复杂,无法在问题中简明扼要地表达出来。我假设人们可以(1)假设存在动机,因为问题是精确制定的,或者可以(2)计算动机,因为他们已经处于 c++ 设计的类似权衡和平衡情况。

没有人想出动机——令我惊讶的是——即使是现在。这对我来说是一个教训。

我接受了提到 CRTP 的答案,因为它很有趣。

【问题讨论】:

  • 如果您不需要虚拟,为什么要添加它们?如果你需要它们,你就需要它们。
  • 一些 STL 类确实使用虚拟方法 ...例如std::streambuf
  • @smerlin:标准库STL存在一些混淆和争论,其中STL是指的是最初由 Stepanov 实现的库(容器、迭代器、算法......)。 std::streambuf(如 std::iostream 和其他)是 标准库 的一部分,但不是 STL
  • @David:严格来说,在 C++ 语言的上下文中,没有 STL 这样的东西。说某些部分绝对属于 STL,而另一些部分似乎从一开始就没有道理。
  • 与您的断言相反,STL 确实使用虚拟方法,并且确实使用继承。与您的主要假设相反,效率通常不是使方法虚拟与否的考虑因素。我想说这个问题需要重新思考。

标签: c++ performance inheritance stl


【解决方案1】:

问题:是否有技术(用于 c++)允许程序员一次性创建相同类 X 的两个版本,一个“非虚拟”版本 Xnv(用于性能)和“虚拟”版本 Xv,用于子类化?

当你可以吃蛋糕的时候,为什么要这样做?

使用 CRTP,您拥有编译时多态性,以及子类覆盖行为的能力,而没有任何虚函数开销。

或者,您可以使用“特征”类来注入行为。

【讨论】:

    【解决方案2】:

    我想你可以...

    struct Base
    {
        virtual ~Base();
        virtual void foo();
    };
    
    struct Dummy {};
    
    template <bool is_virtual>
    struct SelectBase
    {
        typedef Base type;
    };
    
    template <>
    struct SelectBase<false>
    {
        typedef Dummy type;
    };
    
    template <bool is_virtual>
    struct MyClass : SelectBase<is_virtual>::type
    {
        ~MyClass();
        void foo();
    };
    
    int main()
    {
        Base* xv = new MyClass<true>(); // virtual version
        MyClass<false>* xnv = new MyClass<false>(); // non-virtual version
        xv->foo(); // virtual call
        xnv->foo(); // non-virtual call
    }
    

    不过,我真的想不出这样做的充分理由。

    【讨论】:

      【解决方案3】:

      与其首先担心性能,一个更好的问题是“从这个类继承是否有意义?”如果答案是否定的,为什么要虚拟化?毕竟,本质上是 final 类有一些存储和性能优势。 (虽然 C++ 不支持 final 类的 Java 概念,但没有虚拟方法的类非常接近“最终”。)

      但是,我通常会走相反的路线:我倾向于将析构函数设为虚拟,因为其他人可能会看到从类继承的用途。

      【讨论】:

      • C++0x 为此引入了final 上下文关键字。
      • 不同的人,在回答“从 X 类继承是否有意义?”时,会给出相反的答案,从是到否再到可能,因为他们的需求不同。他们的需求不同。这就是我提出这个问题的原因——让这两种可能性都保持开放。这种情况一直发生在工程领域——试图适应频谱的两端。
      【解决方案4】:

      我认为您对决定是否将方法设为虚拟的原因感到困惑。性能不是使您的方法是否虚拟化的一个原因,事实上,性能的影响往往是最小的[1]。 p>

      是否提供虚方法的决定应该基于你的需求,特别是:你需要运行时多态吗?如果你这样做,那么方法应该是虚拟的,如果你不这样做,那么它们不应该。

      [1] 在单继承的设计中,虚拟分派只需要一个额外的间接,而在多分派中,成本略高,可能需要额外的指针偏移和间接。这样做的成本将比该方法所做的任何操作要小得多。

      【讨论】:

        【解决方案5】:

        另外一点就是以后可以简单的写。

        template<typename T> class something {
            virtual const T& operator*() = 0;
            virtual something<T>& operator++() = 0;  // This should be only prefix
            // etc
        };
        
        template<typename Derived, typename T> class something_better : public something<T> {
            typename Derived::const_iterator i;
        public:
            something_better(const Derived& d) {
                i = d.begin();
            }
            const T& operator*() {
                return *i;
            }
            something<T>& operator++() {
                ++i;
                return *this;
            }
            something<T>& operator--() {
                --i;
                return *this;
            }
        };
        

        Et 瞧——运行时多态迭代。那很简单。当然,只要迭代器和容器继续有效。

        如果你想要非常量迭代和其他东西,那么你需要等到 C++0x 才能正确处理右值。或者问BOOST_FOREACH的人,他们似乎已经以某种方式解决了问题。

        【讨论】:

        【解决方案6】:

        我不同意这个假设:

        就像 STL 类一样,以获得更好的性能?

        我不同意缺少虚拟是出于性能原因。

        缺少任何虚拟方法(包括 d'tor)使得多态性和子类化比使用“虚拟”基类更加困难。

        非常真实。这就是我在容器类中没有虚函数的原因。它被刻意设计为不被子分类。使用容器类时,最好使用成员而不是继承。

        问题:是否有技术(对于 c++)允许程序员一次性创建同一个类 X 的两个版本,一个“非虚拟”版本 Xnv(用于性能)和“虚拟”版本 Xv,用于子类化?如果不需要,请说明原因。

        您为什么要这样做。
        如果您是为性能而设计的,您的界面通常会从基本的 OO 界面发生变化。在暴露内部结构时,您往往会变得更加松懈(因为交易通常是用速度换取更紧密的类耦合)。

        为了获得更高的性能,算法通常更了解内部结构,以便它可以使用这些信息来假设它的工作原理。因此,您可以通过将存储与使用存储的算法相耦合来提高性能。

        【讨论】:

        • 关于“我不同意缺少虚拟是出于性能原因。”虚拟开销可能相当大。例如,请参阅 citeseerx.ist.psu.edu/viewdoc/… 。 STL 容器是高度优化的代码,摆脱虚拟开销是 STL 设计者从一开始就解决的问题之一。
        • @David:虽然虚函数可能会影响性能,但我不同意这是开发 STL 时的主要考虑因素(尽管我确信这是 A 考虑因素)。我怀疑你引用的论文有任何影响,因为它似乎是在 1996 年之后发表的,而 STL 最初是在 1994 年免费提供的。
        • @David:Stepanov 和 Musser 都在研究泛型编程以及它如何有益于开发。因此,他们研究的重点更多是代码的通用可用性。但就像总是考虑到所有性能一样,我只是反对认为这是主要考虑因素。
        • Stepanov 不是 OOP 爱好者,他不会屈尊将 STL 变成程序员可以继承的东西。然而,C++ 委员会 (WG21) 对 Stepanov & Musser 开发的 STL 进行了重大更改。容器具有非虚拟 dtor 归功于 WG21,而不是 Stepanov 和 Musser。 C++11 延续了这一传统。如果不是出于性能原因,为什么?
        • @David:如果不是出于性能原因,为什么?如上所述。从成员资格扩展而不是继承是更好的设计。
        猜你喜欢
        • 2014-08-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-08-25
        • 2021-02-27
        • 2013-10-18
        • 2011-10-29
        相关资源
        最近更新 更多