22

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

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

C++ -& CRTP 。类型擦除与多态性

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

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

下面给出的代码已使用 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);
}
4

3 回答 3

8

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

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

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

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

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

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

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

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

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

在另一种情况下,您可以像存储原始指针的方式一样Object包装s ,手动销毁它们的内容(相当于必须调用 delete)。您可以包装 a ,在另一种情况下您可以包装of 。您可以包含 a ,相当于在另一种情况下拥有一个向量。您的 's可以从中提取复制构造函数和赋值运算符并将它们公开给,从而为您的 提供完整的值语义,这对应于您的多态情况中的 a of 。T*vectorObjectstd::shared_ptr<T>vectorstd::shared_ptr<T>Objectstd::unique_ptr<T>std::unique_ptr<T>ObjectObjectModelTObjectObjectvectorT

于 2012-11-09T16:27:57.290 回答
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 应用程序的许多倍。

于 2012-11-09T15:32:43.573 回答
0

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

于 2017-05-04T11:24:22.377 回答