【问题标题】:c++ switch vs. member function pointer vs. virtual inheritancec++ switch vs. 成员函数指针 vs. 虚拟继承
【发布时间】:2013-12-23 09:33:38
【问题描述】:

我正在尝试分析实现多态性的各种方法之间的权衡。我需要一个在成员函数上有一些相似之处和不同之处的对象列表。我看到的选项如下:

  1. 在每个对象中都有一个标志,在每个函数中都有一个 switch 语句。 该标志的值将每个对象定向到其特定部分 每个功能。
  2. 在对象中有一个成员函数指针数组,它们是 施工时指定。然后,我将该函数指针称为 获取正确的成员函数。
  3. 有一个带有多个派生类的虚拟基类。一 这样做的缺点是我的列表现在必须包含指针, 而不是对象本身。

我的理解是,选项 3 中的列表中的指针查找将比选项 2 的成员函数查找花费更长的时间,因为保证成员函数的接近性。

这些选项有哪些优点/缺点?我的首要任务是性能而不是可读性。 还有其他的多态方法吗?

【问题讨论】:

  • 1.可怕,2. 稍微不那么可怕,3. 你最好的选择,4. 你需要多态性吗?通常你可以通过使用模板来解决这个问题......
  • “我的首要任务是性能而不是可读性。” - 大错特错。大错特错。即使您需要性能,如果语言已经提供了虚函数(通过多态性——vtables 任何人?),为什么还要手动重新实现虚函数?
  • “我的首要任务是性能而不是可读性。”。在你有权说出这句话之前,你应该学会对代码进行基准测试/分析。
  • 上面评论的人。有时性能比可读性更重要,我认为假设他或她还没有分析是不礼貌的。这是一个有效的问题,不要投反对票,因为它与你狭隘的编程世界观相冲突。
  • 您没有列出一种既易读又可能比所有 3 种都快的方法:模板。您可能还想查看静态多态性,即 CRTP。

标签: c++ pointers inheritance


【解决方案1】:
  1. 每个对象都有一个标志,每个函数都有一个 switch 语句。该标志的值将每个对象引导到每个函数的特定部分

    好的,所以如果很少有代码根据标志而变化,这可能是有意义的。 这最大限度地减少了必须放入缓存中的(重复的)代码量,并避免了任何函数调用间接。在某些情况下,这些好处可能超过 switch 语句的额外成本。

  2. 在对象中有一个成员函数指针数组,这些指针在构造时分配。然后,我调用该函数指针来获取正确的成员函数

    您保存了一个间接(到 vtable),但也使您的对象更大,因此更少适合缓存。不可能说哪个会占主导地位,所以你只需要分析一下,但这并不是一个明显的胜利

  3. 有一个带有多个派生类的虚拟基类。这样做的一个缺点是我的列表现在必须包含指针,而不是对象本身

    如果您的代码路径足够不同以至于完全分开它们是合理的,这是最干净的解决方案。如果您需要优化它,您可以使用专门的分配器来确保它们是顺序的(即使在您的容器中不是顺序的),或者使用类似于 Boost.Any 的巧妙包装器将对象直接移动到您的容器中。你仍然会得到 vtable 间接,但我更喜欢这个而不是 #2,除非分析表明它确实是一个问题。

因此,在您做出决定之前,您应该回答几个问题:

  1. 共享多少代码,变化多少?
  2. 对象有多大,内联函数指针表是否会对缓存未命中统计数据产生重大影响?

而且,在你回答完这些问题后,无论如何你都应该做个人资料。

【讨论】:

    【解决方案2】:

    实现更快的多态性的一种方法是通过CRTP idiom and static polymorphism

    template<typename T>
    struct base
    {
        void f()
        {
             static_cast<T*>( this )->f_impl();
        }
    };
    
    struct foo : public base<foo>
    {
        void f_impl()
        {
           std::cout << "foo!" << std::endl;
        }
    };
    
    struct bar : public base<bar>
    {
        void f_impl()
        {
           std::cout << "bar!" << std::endl;
        }
    };
    
    struct quux : public base<quux>
    {
        void f_impl()
        {
           std::cout << "quux!" << std::endl;
        }
    };
    
    
    template<typename T>
    void call_f( const base<T>& something )
    {
        something.f();
    }
    
    int main()
    {
        foo my_foo;
        bar my_bar;
        quux my_quux;
    
        call_f( my_foo );
        call_f( my_bar );
        call_f( my_quux );
    }
    

    这个输出:

    嘘!
    吧!
    qux!

    静态多态performs far better than virtual dispatch,因为编译器知道在编译时将调用哪个函数,它可以内联所有内容

    即使它提供动态绑定,它也不能以常见的异构容器方式执行多态性,因为基类的每个实例都是不同的类型。
    但是,这可以通过 boost::any 之类的方式来实现。

    【讨论】:

      【解决方案3】:

      使用switch 语句,如果您想添加一个新类,那么您需要修改打开该类的所有位置,这些位置可能位于您代码库中的不同位置。您的代码库之外可能还有需要修改的地方,但也许您知道在这种情况下并非如此。

      在每个成员中都有一个成员函数指针数组,唯一的缺点是您为每个对象复制了该内存。如果您知道只有一两个“虚拟”功能,那么这是一个不错的选择。

      至于虚函数,你是对的,你必须堆分配它们(或手动管理内存),但它是最可扩展的选项。

      如果您不追求可扩展性,那么 (1) 或 (2) 可能是您的最佳选择。与往常一样,唯一的判断方法就是测量。我知道许多编译器在某些情况下会通过跳转表来实现 switch 语句,它本质上与虚函数表相同。对于少量的case 语句,他们可能只使用二分搜索分支。

      测量!

      【讨论】:

      • As for virtual functions, you are right in that you have to heap allocate them (or manual manage the memory) 不正确。您只需要通过引用或指针传递对象(或者,显然,直接使用其真正的静态类型调用它)。对象是如何分配的完全无关紧要。我用堆栈上分配的对象做了很多多态的东西。最糟糕的是,Stroustrup 在他的一个常见问题解答中有同样的错误陈述。我想这就是人们不断重复这一点的地方。
      猜你喜欢
      • 2013-06-25
      • 2010-09-08
      • 2016-06-24
      • 2012-04-12
      • 1970-01-01
      • 1970-01-01
      • 2012-11-30
      相关资源
      最近更新 更多