【问题标题】:C++ -& CRTP . Type erasure vs polymorphismC++ -& CRTP 。类型擦除与多态性
【发布时间】:2010-11-30 16:57:36
【问题描述】:

好的,我们开始吧。我正在尝试使用CRTP 模板来消除我的应用程序对多态性的需求。我使用像下面这样的方法

template <RealType> class Base 
{

    void doSomething()
    {
         static_cast<RealType>(this)->doSomethingImpl()
    }

class Derived1 : public Base
{
    void doSomethingImpl()
    {
        /* do something, for real */
    }
}

class Derived2 : public Base
{
    void doSomethingImpl()
    {
        /* do something else */
    }
}

如果我理解正确,这种方法允许我的类没有 vtable,因此函数调用是直接的,不需要额外的间接。

现在假设我想将我所有的 Derived# 类存储在一个容器中。让我们说一个向量。

第一种方法:我可以创建一个非模板 SuperBase 类,Base 从中继承并将其存储在容器中。

但在我看来,如果我想这样做,我将不得不在 SuperBase 中将 doSomething 设为虚拟。而我的目标主要是没有vtable。

第二种方法:我使用类型擦除,即类似 boost::any 的东西将我的元素存储在 Vector 中。 但是,我看不到可以迭代元素并在它们上调用 doSomething 的方法,因为要“使用” boost::any,我需要在迭代时知道对象的真实类型。

你认为我想做的事有可能吗?

在我看来,这是因为 doSomething() 是 Base 的一部分,但除了使用继承之外,我看不到这样做的方法......

【问题讨论】:

  • 从以下两个答案中总结一点,以后可能还会有更多:您说,“我的目标主要是没有 vtable”。什么?正常的目标是,“我想编写一些有效的代码”,或者“我想编写一些有效且运行速度快的代码”。 vtables 是一个实现细节,一种提供动态多态性的方式,听起来您的主要目标是拥有动态多态性。
  • 谢谢大家!好吧,避免 vtables 的重点确实是性能,因为这段代码将用于低级函数(即经常调用)。一般来说,我对 vtables 没有特别的仇恨:)。另一件事是我读了很多关于静态多态性的文章,我很好奇它是否可能!显然不是。谢谢大家的回答!
  • 多态是一种可以在某些对象的确切类型可能取决于运行时事件时使用的工具。如果你需要这个,那么多态是一个非常好的工具。为了解决甚至未被证明存在的性能问题而放弃它以支持更钝的工具似乎是一个真正可怕的错误。如果您的对象的类型只能在运行时确定,那么您需要运行时多态。如果你不使用内置的,你需要自己破解。即使在非常特殊的情况下,这非常很难做到正确并且比内置的更快。
  • 你可能想看看stackoverflow.com/questions/65628159/…的答案

标签: c++ metaprogramming type-erasure


【解决方案1】:

而我的目标主要是没有 vtable。

如果你想要这个,那么,除非你实现自己的虚拟调度系统(这可能不如编译器所做的),否则你会被模板困住,也就是编译时多态性。顾名思义,为了使用它,必须在编译时了解所有内容。如果您需要根据运行时事件(例如,用户输入)做出决策,您需要运行时多态性

我不禁要问:为什么你要避免使用 vtables? (如果你这么有决心,为什么不用 C 编程呢?)

【讨论】:

    【解决方案2】:

    您可以通过适当的理论运算来做您想做的事情,这不是多态性,而是统一性。大多数人不知道什么是总和类型(有区别的联合),以及为什么他们一直滥用继承,这是完全不相关的。

    联合在 C 中更受欢迎,例如 X-Windows 事件消息是基于联合的,因为它们在 C++ 中被破坏了。

    联合是将异构数据类型表示为单一类型的正确方法,因此称为统一:它将所有组件统一为单一类型。联合总是有有限的已知组件数量,使用联合的函数总是使用判别式上的开关来选择正确的处理程序代码。

    OOP 无法提供统一:它提供子类型化。

    模板再次提供了完全不同的东西:参数多态性。

    这三个概念在理论和实践上都截然不同。子类型化 OOP 样式被证明是最没用的,因为它可以表示的内容受到严格限制,但是这些限制确实允许动态分派到一组开放的子类型,这非常好 如果它适用 解决您的问题。

    所以现在很清楚,在您的数组中,您只需要放置所有类的联合,您的问题就会消失。

    只有 .. 由于无原则的限制,目前 C++ 中的类必须是 POD 类型。所以最好的解决方案是使用原始 C 函数的联合,因为 C 函数指针是 POD。

    类似:

    enum tag {tag_X, tag_Y};
    
    struct X { int x; };
    void px(X a) { cout << a.x; }
    struct PX { tag t; X a; void (*p)(X); };
    
    struct Y { double x; };
    void py(Y a) { cout << a.x; };
    struct PY {tag t; Y a; void (*p)(Y); };
    
    union XY { PX anX; PY anY; };
    
    PX x1 = { tag_X, { 1 }, px };
    PY y1 = { tag_Y, { 1.0 }, py };
    
    XY xy1.anPX = x1;
    XY xy2.anPy = x2;
    
    XY xys[2] = {xy1, xy1};
    
    xy = xys[0];
    switch (xy.anPX.tag) { // hack
      case tag_X: xy.anPX.p (xy.PX.x); break;
      case tag_Y: xy.anPY.p (xy.PY.x); break;
    }
    

    如果您认为这很丑陋,那您是对的:C 和 C++ 已经脑死了。另一种解决方案是使用一个标记和一个转换为 void* 的指针,然后使用标记转换为所需的类型:这更容易,但需要对数据进行堆分配,因此存在内存管理问题。另一种选择是 Boost 变体类型(它可以自动执行一些内务处理,但仍然非常难看)。

    以下是 Ocaml 中的类似代码:

    type xy = X of int | Y of double
    let print u =
      match u with 
      | X x -> print_int x 
      | Y x -> print_double x
    in 
      print (X 1);
      print (Y 2.0)
    

    在这段代码中,X 和 Y 是上面 C 代码的标记,它们被称为类型构造函数,因为它们分别从 int 或 double(分别)构造 xy 类型。匹配表达式只有一个开关,自动选择正确的组件类型和范围,用于确保您不能引用错误的组件(就像在 C 代码中一样),也没有中断,匹配处理程序不会drop thru,内存管理由垃圾收集器完成。

    【讨论】:

    • 请注意,在 C++17 中,std::variant 为 tihs 提供了足够强大的 sum 类型。 std::visit( var_element, [](auto&amp;&amp; e){ e.doSomething(); } ) 然后提供了无 vtable 的动态调度,而 lambdas 摆脱了很多丑陋的东西。
    • 啊,我做了一些工作。您是否有参考资料,以便我查看实际接受的内容?
    • @Yakk:谢谢。不幸的是,variant 并不是真正需要的,它是一个类型安全的 C 联合,它不是正确的 sum 类型。我看到的提案是davidsankel.com/uncategorized/…,这是一个非常优越但语言内在的扩展。 ISO 变体 使用类型作为判别符而不是标记,因此变体不能支持真实变体类型所需的相同类型的不同变体。无论如何没有模式匹配都没用:)
    • 不,std::variant 支持variant&lt;int, int, int&gt;。使用的标签是编译时索引。在这样的变体上调用 get&lt;int&gt; 是编译时错误。调用get&lt;0&gt;get&lt;1&gt;get&lt;2&gt; 会得到int。或者如果你真的需要标签,你可以使用标签类型。
    • 不管怎样,variant 足够强大,足以让上面大部分丑陋的 C 代码消失,并且它的模式匹配相似性足以在这种情况下模仿上面的 Ocaml 代码。
    【解决方案3】:

    您可以结合两种优势。

    #include <iostream>
    
    struct AnimalBase
    {
        virtual std::string speak() const = 0;
        virtual ~AnimalBase() {};
    };
    
    template <typename Derived>
    struct Animal : AnimalBase
    {
        std::string speak() const
        {
            return static_cast<const Derived*>(this)->speak();
        }
    };
    
    struct Dog : Animal<Dog>
    {
        std::string speak() const
        {
            return "Woof! Woof!";
        }
    };
    
    struct Cat : Animal<Cat>
    {
        std::string speak() const
        {
            return "Meow. Meow.";
        }
    };
    
    int main()
    {
        AnimalBase* pets[] = { new Dog, new Cat };
    
        std::cout << Dog().speak() << '\n'
                  << Cat().speak() << '\n'
                  << pets[0]->speak() << '\n'
                  << pets[1]->speak() << std::endl;
    
        delete pets[0];
        delete pets[1];
        return 0;
    }
    

    【讨论】:

    • 由于这涉及基础中的虚函数,它是否保留了使用 CRTP 的任何好处?对我来说,这似乎打败了重点。看起来您只是将 vtable 间接移动到不同的位置,同时使代码复杂化。
    • @underscore_d 有 vtable 但仅在需要时才查找。 Dog().speak() 不查找 vtable,但 pets[0]-&gt;speak() 查找。
    • 谢谢,您的评论促使我做更多的研究。 :) 只要我们使用已知类型和好的编译器,这样的调用就可以从去虚拟化中受益。 -O0 甚至应用了一些简单的去虚拟化。这是一个很好的系列:hubicka.blogspot.co.uk/2014/01/…
    【解决方案4】:

    将这些对象保存在容器中会很困难(充其量是 hack)。您已经设计了多态性,但您似乎真的想使用它,因此您可以将对象保存为 container&lt;mybaseclass&gt; 并以多态方式使用它们。

    从您的帖子中我不清楚您为什么希望避免使用 vtable。如果这是为了性能,您可能过度优化。如果没有更多关于你为什么要走这条路的背景,除了“使用基类”之外,很难推荐任何东西。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-08-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多