C++技术:类型抹除与纯虚多态性

24

两种技术相比的优缺点是什么?更重要的是:为什么和何时应该使用其中之一?这只是个人口味/偏好的问题吗?

据我所知,我没有找到另一个明确回答我的问题的帖子。在许多关于多态性和/或类型抹除实际使用的问题中,以下问题似乎最接近,但它也没有真正回答我的问题:

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

请注意,我非常理解这两种技术。为此,下面我提供了一个简单的、自包含的工作示例,如果认为没有必要,我很乐意删除它。但是,该示例应该澄清这两种技术在我的问题中的含义。我不感兴趣讨论命名规范。另外,我知道编译时多态和运行时多态之间的区别,尽管我不认为这与问题相关。请注意,如果有任何性能差异,我的兴趣不在于性能差异。然而,如果有一个明显的基于性能的论据支持其中一种方法,我会很想看看。特别是,我想听听具体例子(没有代码),这些例子只适用于其中一种方法。
从下面的示例来看,一个主要的区别是内存管理,对于多态而言,内存管理留给用户处理,而对于类型抹除而言,则可以整洁地处理需要一些引用计数(或boost)。话虽如此,根据使用场景的不同,通过使用智能指针结合vector,多态示例的情况可能会得到改进,但是对于任意情况来说,这很可能会变得不切实际。另一个方面,可能有利于类型抹除的是通用接口的独立性,但是这究竟是什么优势呢?

以下代码已经测试(编译和运行)通过MS VisualStudio 2008,只需将所有以下代码块放入单个源文件中即可。我希望它也可以在Linux上使用gcc编译,因为我认为没有理由不行。我在此处分割/划分代码以便清晰易懂。

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

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

简单的引用计数,避免使用boost(或其他)依赖。这个类只在下面的类型抹除示例中使用。
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);
}

3
你是否曾经查看过Adobe的poly类或者Boost.TypeErasure(已通过但尚未发布)?除了CRTP之外,它们实现了“基于概念”的多态性,具有适当的值语义。 - J.N.
6
在我的书中,“多态性”和“类型擦除”既不是范式,也只是语言工具箱中的工具。此外,我想不出有没有一种方式可以使用类型擦除而不使用多态性,所以我更加困惑于您的要求是什么。 - Kerrek SB
J.N.,非常感谢,我会在适当的时候查看。原则上,我非常支持重复使用,特别是在处理非商业或私人代码时,但对于商业代码,我总是权衡决策,以减少外部依赖,但这是另一个讨论... :-) 无论如何,我渴望学习更多... - Dr.D.
5
如果你想听我的看法:只有在需要处理运行时才能确定的相关类型集合,例如从网络I/O收到的消息数据包、epoll事件或运行时选择的类注册表/工厂等情况下,才使用继承多态性。如果你确实需要一个固定的、单一的类型来处理异构物品集合,比如std :: shared_ptr <T>std :: functionboost :: any,则使用类型擦除。其余请在编译时完成。 - Kerrek SB
@Dr.D.:这就是我写第二条评论的原因。你的问题听起来像是“X或Y哪个更好?”,但我觉得这并不是非常有用的。唯一重要的问题是“我该如何解决问题Z?”我的第二条评论包含了我能想到的唯一的“绝对”建议,而所有其他选择都归结为选择正确的工具来解决你实际的问题。(尽管如果被迫的话,我会认为任何以class Object开头的代码都是可疑的 :-).) - Kerrek SB
显示剩余2条评论
3个回答

8

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

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

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

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

现在,哪个更好?嗯,这取决于上述内容在您特定情况下是好还是坏。

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

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

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

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

您的Object可以像在另一种情况下存储原始指针的vector一样包装T*,并手动销毁其内容(相当于需要调用delete)。您的Object可以包含一个std::shared_ptr<T>,在另一种情况下,您可以拥有std::shared_ptr<T>vector。您的Object可以包含一个std::unique_ptr<T>,相当于在另一种情况下拥有std::unique_ptr<T>vector。您的ObjectObjectModel可以从T中提取复制构造函数和赋值运算符,并将它们暴露给Object,允许您的Object具有完全的值语义,这对应于您的多态性案例中的vector


5
这里有一个观点:问题似乎是要问如何在后期绑定(“运行时多态”)和早期绑定(“编译时多态”)之间进行选择。
正如KerrekSB在他的评论中指出的那样,有些事情你可以使用后期绑定来完成,而使用早期绑定则不现实。很多策略模式(解码网络I/O)或抽象工厂模式(运行时选择类工厂)的用途属于这个范畴。
如果两种方法都可行,那么选择就是一种权衡的结果。在C++应用程序中,我看到早期和后期绑定之间的主要权衡是实现可维护性、二进制大小和性能。
至少有一些人认为C++模板在任何形式下都是难以理解的。或者可能对模板有其他不太明显的保留意见。C++模板有许多小问题(“我什么时候需要使用'typename'和'template'关键字?”),还有非常棘手的技巧(例如SFINAE)。
另一个权衡是优化。当您早期绑定时,您向编译器提供了更多关于程序的信息,因此它可以(潜在地)更好地进行优化。当您后期绑定时,编译器(可能)无法预先了解太多信息 - 某些信息可能在其他编译单元中,因此优化器无法做太多工作。
另一个权衡是程序大小。至少在C++中,使用“编译时多态”有时会导致二进制大小膨胀,因为编译器为每个使用的特化创建、优化和发出不同的代码。相比之下,当进行后期绑定时,只有一条代码路径。
比较在不同上下文中进行相同的权衡是很有趣的。以Web应用程序为例,其中使用(某种类型的)多态来处理浏览器之间的差异,可能还包括国际化(i18n)/本地化。现在,手写JavaScript Web应用程序可能会在这里使用类似于后期绑定的方法,通过具有检测运行时功能的方法来确定要执行什么操作。像jQuery这样的库采用这种方法。
另一种方法是为每个可能的浏览器/i18n可能性编写不同的代码。虽然听起来荒谬,但这并不罕见。Google Web Toolkit就采用了这种方法。GWT有其“延迟绑定”机制,用于将编译器的输出专门针对不同的浏览器和本地化进行特化。GWT的“延迟绑定”机制使用早期绑定:GWT Java到JavaScript编译器找出可能需要多态的所有可能方式,并为每个完全不同的“二进制文件”输出。
权衡是相似的。理解如何使用延迟绑定扩展GWT可能会让人头痛不已;在编译时拥有知识允许GWT的编译器分别优化每个专业化,可能会产生更好的性能和更小的大小;由于所有预编译的专业化,整个GWT应用程序的大小可能是可比较的jQuery应用程序的许多倍。

0
运行时泛型的一个好处是可以生成并注入到正在运行的应用程序中的代码,使用与该应用程序中其他所有内容已经在使用的相同的List、Hashmap/Dictionary等。为什么要这样做,又是另一个问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接