【问题标题】:C++ Techniques: Type-Erasure vs. Pure PolymorphismC++ 技术:类型擦除与纯多态性
【发布时间】:2012-11-09 14:30:45
【问题描述】:

比较这两种技术的优点/缺点是什么?更重要的是:为什么以及何时应该使用一个而不是另一个?这只是个人品味/偏好的问题吗?

尽我所能,我还没有找到其他明确解决我问题的帖子。在有关多态性和/或类型擦除的实际使用的许多问题中,以下似乎是最接近的,或者看起来如此,但它也没有真正解决我的问题:

C++ -& CRTP . Type erasure vs polymorphism

请注意,我非常了解这两种技术。为此,我在下面提供了一个简单、独立的工作示例,如果觉得不必要,我很乐意将其删除。但是,该示例应阐明这两种技术对我的问题的意义。我对讨论命名法不感兴趣。另外,我知道编译时和运行时多态性之间的区别,尽管我认为这与问题无关。请注意,我对性能差异的兴趣不大,如果有的话。但是,如果有一个基于性能的引人注目的论点,我会很好奇阅读它。特别是,我想听听实际上只能使用这两种方法之一的具体示例(无代码)。

看看下面的例子,一个主要的区别是内存管理,对于多态性,它保留在用户端,而对于类型擦除,它被巧妙地隐藏起来,需要一些引用计数(或提升)。话虽如此,根据使用场景,多态示例的情况可能会通过使用带有向量 (?) 的智能指针来改善,尽管对于任意情况,这很可能会变得不切实际 (?)。另一个可能支持类型擦除的方面可能是通用接口的独立性,但为什么这会是一个优势(?)。

下面给出的代码已使用 MS VisualStudio 2008 进行了测试(编译和运行),只需将以下所有代码块放入单个源文件即可。它也应该在 Linux 上使用 gcc 编译,或者我希望/假设,因为我看不出为什么不 (?) :-) 为了清楚起见,我在这里拆分/划分了代码。

这些头文件应该足够了,对吧(?)。

#include <iostream>
#include <vector>
#include <string>

简单的引用计数来避免提升(或其他)依赖。该类仅在下面的类型擦除示例中使用。

class RefCount
{
  RefCount( const RefCount& );
  RefCount& operator= ( const RefCount& );
  int m_refCount;

  public:
    RefCount() : m_refCount(1) {}
    void Increment() { ++m_refCount; }
    int Decrement() { return --m_refCount; }
};

这是简单的类型擦除示例/插图。它是从以下文章中复制和修改的。主要是我试图让它尽可能清晰和直接。 http://www.cplusplus.com/articles/oz18T05o/

class Object {
  struct ObjectInterface {
    virtual ~ObjectInterface() {}
    virtual std::string GetSomeText() const = 0;
  };

  template< typename T > struct ObjectModel : ObjectInterface {
    ObjectModel( const T& t ) : m_object( t ) {}
    virtual ~ObjectModel() {}
    virtual std::string GetSomeText() const { return m_object.GetSomeText(); }
    T m_object;
 };

  void DecrementRefCount() {
    if( mp_refCount->Decrement()==0 ) {
      delete mp_refCount; delete mp_objectInterface;
      mp_refCount = NULL; mp_objectInterface = NULL;
    }
  }

  Object& operator= ( const Object& );
  ObjectInterface *mp_objectInterface;
  RefCount *mp_refCount;

  public:
    template< typename T > Object( const T& obj )
      : mp_objectInterface( new ObjectModel<T>( obj ) ), mp_refCount( new RefCount ) {}
    ~Object() { DecrementRefCount(); }

    std::string GetSomeText() const { return mp_objectInterface->GetSomeText(); }

    Object( const Object &obj ) {
      obj.mp_refCount->Increment(); mp_refCount = obj.mp_refCount;
      mp_objectInterface = obj.mp_objectInterface;
    }
};

struct MyObject1 { std::string GetSomeText() const { return "MyObject1"; } };
struct MyObject2 { std::string GetSomeText() const { return "MyObject2"; } };

void UseTypeErasure() {
  typedef std::vector<Object> ObjVect;
  typedef ObjVect::const_iterator ObjVectIter;

  ObjVect objVect;
  objVect.push_back( Object( MyObject1() ) );
  objVect.push_back( Object( MyObject2() ) );

  for( ObjVectIter iter = objVect.begin(); iter != objVect.end(); ++iter )
    std::cout << iter->GetSomeText();
}

就我而言,这似乎使用多态实现了几乎相同的效果,或者可能不是(?)。

struct ObjectInterface {
  virtual ~ObjectInterface() {}
  virtual std::string GetSomeText() const = 0;
};

struct MyObject3 : public ObjectInterface {
  std::string GetSomeText() const { return "MyObject3"; } };

struct MyObject4 : public ObjectInterface {
  std::string GetSomeText() const { return "MyObject4"; } };

void UsePolymorphism() {
  typedef std::vector<ObjectInterface*> ObjVect;
  typedef ObjVect::const_iterator ObjVectIter;

  ObjVect objVect;
  objVect.push_back( new MyObject3 );
  objVect.push_back( new MyObject4 );

  for( ObjVectIter iter = objVect.begin(); iter != objVect.end(); ++iter )
    std::cout << (*iter)->GetSomeText();

  for( ObjVectIter iter = objVect.begin(); iter != objVect.end(); ++iter )
    delete *iter;
}

最后是一起测试以上所有内容。

int main() {
  UseTypeErasure();
  UsePolymorphism();
  return(0);
}

【问题讨论】:

  • 您是否看过 Adob​​e 的 poly 类或 Boost.TypeErasure(已接受但尚未发布)?除了 CRTP 之外,它们还实现了具有适当值语义的“基于概念”的多态性。
  • 在我的书中,“多态性”和“类型擦除”都不是范式,它们只是语言工具箱中的工具。此外,我想不出一种方法来进行类型擦除也使用多态性,所以我对你所追求的更加困惑。
  • J.N.,非常感谢,我会在适当的时候检查一下。原则上,我完全赞成重用,特别是在处理非商业或私有代码时,但是,对于商业代码,我总是在决定与减少外部依赖之间取得平衡,但这是另一个讨论...... :- ) 无论如何,我渴望了解更多...
  • 如果你想要我的看法:只有当你需要一个相关的类型集合时才使用继承多态性只能在运行时决定,例如来自网络的消息包I/O,或 epoll 事件,或运行时选择的类注册表/工厂等。如果您绝对需要一个固定的单一类型来处理一组异构事物,请使用类型擦除,例如 std::shared_ptr&lt;T&gt;、@987654330 @ 或 boost::any。在编译时执行其他所有操作。
  • Kerrek,正如我所写的,恕我直言,我对讨论命名法不感兴趣,称之为工具、方法、某种做事方式……我很清楚有些使用多态性在类型擦除示例中是“隐藏的”,我考虑将其放入我的问题中,但这不是重点。这两个例子展示了两种实现非常相似结果的方法(恕我直言)——一种优于另一种(?),它取决于使用场景(?),还是只是个人喜好和“语义糖”(? )。感谢您的第二条评论。

标签: c++ paradigms


【解决方案1】:

C++风格的基于虚拟方法的多态性:

  1. 您必须使用类来保存数据。
  2. 构建每个类时都必须考虑到您特定的多态性。
  3. 每个类都有一个共同的二进制级依赖关系,这限制了 编译器创建每个类的实例。
  4. 您要抽象的数据必须明确描述一个接口,该接口描述 您的需求。

基于 C++ 样式模板的类型擦除(使用基于虚拟方法的多态性进行擦除):

  1. 你必须使用模板来谈论你的数据。
  2. 您正在处理的每个数据块都可能与其他选项完全无关。
  3. 类型擦除工作在公共头文件中完成,这会增加编译时间。
  4. 每种被擦除的类型都有自己的模板实例化,这会导致二进制大小膨胀。
  5. 您要抽象的数据不必写成直接取决于您的需求。

现在,哪个更好?好吧,这取决于在您的特定情况下上述事情是好是坏。

作为一个明确的例子,std::function&lt;...&gt; 使用类型擦除,这允许它获取函数指针、函数引用、一大堆基于模板的函数的输出,这些函数在编译时生成类型,无数具有操作符的函子( ) 和 lambdas。所有这些类型都彼此无关。而且因为它们与virtual operator() 无关,所以当它们在std::function 上下文之外使用时,它们所代表的抽象可以被编译掉。如果没有类型擦除,您将无法做到这一点,而且您可能不想这样做。

另一方面,仅仅因为一个类有一个名为DoFoo 的方法,并不意味着它们都做同样的事情。对于多态性,它不仅仅是您调用的任何DoFoo,而是来自特定接口的DoFoo

至于您的示例代码...在多态情况下,您的 GetSomeText 应该是 virtual ... override

没有必要仅仅因为您使用类型擦除而引用计数。没有必要因为使用多态就不用引用计数。

您的Object 可以包装T*s,就像您在另一种情况下存储vectors 的原始指针一样,手动销毁它们的内容(相当于必须调用删除)。你的Object 可以包装一个std::shared_ptr&lt;T&gt;,在另一种情况下你可以有vectorstd::shared_ptr&lt;T&gt;。您的Object 可以包含std::unique_ptr&lt;T&gt;,在另一种情况下相当于具有std::unique_ptr&lt;T&gt; 的向量。您的ObjectObjectModel 可以从T 中提取复制构造函数和赋值运算符并将它们公开给Object,从而为您的Object 提供完整的值语义,这对应于vectorT 在您的多态情况下。

【讨论】:

    【解决方案2】:

    这里有一种观点:这个问题似乎是在询问应该如何在后期绑定(“运行时多态性”)和早期绑定(“编译时多态性”)之间进行选择。

    正如 KerrekSB 在他的 cmets 中指出的那样,您可以使用后期绑定来做一些事情,而使用早期绑定是不现实的。策略模式(解码网络 I/O)或抽象工厂模式(运行时选择的类工厂)的许多用途都属于这一类。

    如果这两种方法都可行,那么选择就是一个权衡取舍的问题。在 C++ 应用程序中,我看到的早期绑定和后期绑定之间的主要权衡是实现的可维护性、二进制大小和性能。

    至少有些人觉得任何形状或形式的 C++ 模板都无法理解。或者可能对模板有一些其他不那么引人注目的保留。 C++ 模板有很多小问题(“我什么时候需要使用 'typename' 和 'template' 关键字?”)和不明显的技巧(想到 SFINAE)。

    另一个权衡是优化。当您尽早绑定时,您可以向编译器提供有关您的程序的更多信息,因此它可以(潜在地)更好地进行优化。当您绑定较晚时,编译器(可能)不会提前知道那么多信息——其中一些信息可能在其他编译单元中,因此优化器不能做那么多。

    另一个权衡是程序大小。至少在 C++ 中,使用“编译时多态性”有时会膨胀二进制大小,因为编译器会为每个使用的专业化创建、优化和发出不同的代码。相比之下,晚绑定时,只有一个代码路径。

    比较在不同环境中做出的相同权衡很有趣。以 Web 应用程序为例,其中使用(某种类型的)多态性来处理浏览器之间的差异,并可能用于国际化(i18n)/本地化。现在,一个手写的 JavaScript Web 应用程序可能会在这里使用相当于后期绑定的方法,通过在运行时检测功能来确定要做什么的方法。像 jQuery 这样的库采用了这种策略。

    另一种方法是为每种可能的浏览器/i18n 可能性编写不同的代码。虽然这听起来很荒谬,但绝非闻所未闻。 Google Web Toolkit 使用这种方法。 GWT 有其“延迟绑定”机制,用于将编译器的输出专门用于不同的浏览器和不同的本地化。 GWT 的“延迟绑定”机制使用早期绑定:GWT Java-to-JavaScript 编译器找出可能需要多态性的所有可能方式,并为每种方式生成一个完全不同的“二进制”。

    权衡是相似的。想一想如何使用延迟绑定来扩展 GWT 可能会让人头疼。在编译时获得知识允许 GWT 的编译器分别优化每个专业化,可能会产生更好的性能,并且每个专业化的大小更小;由于所有预编译的特化,整个 GWT 应用程序的大小最终可能是同类 jQuery 应用程序的许多倍。

    【讨论】:

      【解决方案3】:

      运行时泛型的一个好处是这里没有人提到(?)是生成并注入正在运行的应用程序的代码的可能性,使用相同的ListHashmap / Dictionary等。该应用程序已经在使用。 为什么你想这样做,是另一个问题。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-04-07
        • 1970-01-01
        • 2018-08-14
        相关资源
        最近更新 更多