比较两种技术的优点/缺点是什么?更重要的是:为什么以及何时应该使用另一个?这只是个人品味/偏好的问题吗?
尽我所能,我还没有发现另一篇明确解决我的问题的帖子.关于多态性和/或类型擦除的实际使用的许多问题中,以下似乎最接近,或者看起来如此,但它并没有真正解决我的问题:
请注意,我非常了解这两种技巧.为此,我在下面提供了一个简单,独立的工作示例,如果感觉不必要,我很乐意删除.但是,这个例子应该澄清这两种技术对我的问题意味着什么.我对讨论命名法不感兴趣.此外,我知道编译和运行时多态性之间的区别,但我不认为这与该问题相关.请注意,如果有的话,我对性能差异的兴趣会减少.但是,如果根据表现有一个引人注目的争论,我很想读它.特别是,我想听听具体的例子(没有代码),这些例子实际上只适用于这两种方法中的一种.
查看下面的示例,一个主要区别是内存管理,多态性保留在用户端,而类型擦除则整齐地隐藏起来,需要一些引用计数(或增强).话虽如此,根据使用场景,可以通过使用带有向量(?)的智能指针来改善多态性的情况,但对于任意情况,这可能很难变得不切实际(?).可能有利于类型擦除的另一个方面可能是公共接口的独立性,但为什么这将是一个优势(?).
通过简单地将以下所有代码块放入单个源文件中,使用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)
基于C++样式的虚方法多态:
基于C++样式模板的类型擦除(基于虚拟方法的多态性进行擦除):
现在哪个更好?那么,这取决于上述情况在您的特定情况下是好还是坏.
作为一个明确的例子,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>,而在另一种情况下,你可以拥有vector的std::shared_ptr<T>.你Object可以包含一个std::unique_ptr<T>,相当于std::unique_ptr<T>在另一个案例中有一个向量.你Object的ObjectModel可以从提取的拷贝构造函数和赋值操作符T和揭露他们的Object,让你的全通值语义Object,这相当于一个vector的T在你的多态性情况.
这里有一个观点:问题似乎在于如何在后期绑定("运行时多态性")和早期绑定("编译时多态性")之间做出选择.
正如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应用程序的数倍.