C++技术:类型擦除与纯多态

Dr.*_*.D. 18 c++ paradigms

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

尽我所能,我还没有发现另一篇明确解决我的问题的帖子.关于多态性和/或类型擦除的实际使用的许多问题中,以下似乎最接近,或者看起来如此,但它并没有真正解决我的问题:

C++ - &CRTP.类型擦除vs多态

请注意,我非常了解这两种技巧.为此,我在下面提供了一个简单,独立的工作示例,如果感觉不必要,我很乐意删除.但是,这个例子应该澄清这两种技术对我的问题意味着什么.我对讨论命名法不感兴趣.此外,我知道编译和运行时多态性之间的区别,但我不认为这与该问题相关.请注意,如果有的话,我对性能差异的兴趣会减少.但是,如果根据表现有一个引人注目的争论,我很想读它.特别是,我想听听具体的例子(没有代码),这些例子实际上只适用于这两种方法中的一种.

查看下面的示例,一个主要区别是内存管理,多态性保留在用户端,而类型擦除则整齐地隐藏起来,需要一些引用计数(或增强).话虽如此,根据使用场景,可以通过使用带有向量(?)的智能指针来改善多态性的情况,但对于任意情况,这可能很难变得不切实际(?).可能有利于类型擦除的另一个方面可能是公共接口的独立性,但为什么这将是一个优势(?).

通过简单地将以下所有代码块放入单个源文件中,使用MS VisualStudio 2008测试(编译和运行)下面给出的代码.它也应该在Linux上用gcc编译,或者我希望/假设,因为我认为没有理由不(?):-)为了清楚起见,我在这里拆分/分割代码.

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

#include <iostream>
#include <vector>
#include <string>
Run Code Online (Sandbox Code Playgroud)

简单的引用计数以避免增强(或其他)依赖性.此类仅用于下面的类型擦除示例.

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; }
};
Run Code Online (Sandbox Code Playgroud)

这是简单的类型擦除示例/插图.它的复制和修改部分来自以下文章.主要是我试图让它尽可能清晰明了. 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();
}
Run Code Online (Sandbox Code Playgroud)

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

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;
}
Run Code Online (Sandbox Code Playgroud)

最后一起测试上述所有内容.

int main() {
  UseTypeErasure();
  UsePolymorphism();
  return(0);
}
Run Code Online (Sandbox Code Playgroud)

Yak*_*ont 7

基于C++样式的虚方法多态:

  1. 您必须使用类来保存数据.
  2. 每个类都必须考虑到您特定的多态性.
  3. 每个类都有一个共同的二进制级依赖项,它限制了编译器如何创建每个类的实例.
  4. 您抽象的数据必须明确描述描述您需求的界面.

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

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

现在哪个更好?那么,这取决于上述情况在您的特定情况下是好还是坏.

作为一个明确的例子,std::function<...>使用类型擦除,它允许它获取函数指针,函数引用,输出在编译时生成类型的一堆基于模板的函数,具有operator()和lambdas的仿函数myraids.所有这些类型彼此无关.并且因为它们与拥有a无关virtual operator(),当它们在std::function上下文之外使用时,它们所代表的抽象可以被编译掉.如果没有类型擦除,你就不能这样做,你可能不想这样做.

另一方面,仅仅因为一个类有一个被调用的方法DoFoo,并不意味着它们都做同样的事情.使用多态性,它不仅仅是DoFoo您正在调用的任何内容,而是DoFoo来自特定接口的任何内容.

至于你的示例代码......你GetSomeText应该virtual ... override处于多态的情况.

没有必要仅因为您正在使用类型擦除而引用计数.没有必要不使用引用计数只是因为您使用的是polymorphsm.

Object可以T*像你vector在其他情况下存储原始指针的方式一样包装,手动破坏它们的内容(相当于必须调用delete).你Object可以换一个std::shared_ptr<T>,而在另一种情况下,你可以拥有vectorstd::shared_ptr<T>.你Object可以包含一个std::unique_ptr<T>,相当于std::unique_ptr<T>在另一个案例中有一个向量.你ObjectObjectModel可以从提取的拷贝构造函数和赋值操作符T和揭露他们的Object,让你的全通值语义Object,这相当于一个vectorT在你的多态性情况.


Man*_*agu 5

这里有一个观点:问题似乎在于如何在后期绑定("运行时多态性")和早期绑定("编译时多态性")之间做出选择.

正如KerrekSB在他的评论中指出的那样,你可以用后期绑定做一些事情,这对早期绑定来说是不现实的.策略模式(解码网络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应用程序的数倍.