Stroustrup说:“不要立即为所有类(对象类)发明一个唯一的基础。通常,如果没有很多/大多数类,您可以做得更好。” (C ++编程语言第四版,第1.3.4节)
为什么对所有事物都采用基类通常是个坏主意,何时创建一个基类有意义?
Stroustrup说:“不要立即为所有类(对象类)发明一个唯一的基础。通常,如果没有很多/大多数类,您可以做得更好。” (C ++编程语言第四版,第1.3.4节)
为什么对所有事物都采用基类通常是个坏主意,何时创建一个基类有意义?
Answers:
因为该对象将具有什么功能?在Java中,所有基类都有一个toString,一个hashCode和相等性以及一个monitor + condition变量。
ToString仅对调试有用。
hashCode仅在要将其存储在基于哈希的集合中时才有用(C ++的首选是将哈希函数作为模板参数传递给容器,或者std::unordered_*
完全避免使用而是使用std::vector
无格式的普通列表)。
没有基础对象的相等可以在编译时得到帮助,如果它们没有相同的类型,那么它们就不能相等。在C ++中,这是一个编译时错误。
最好在逐个案例的基础上显式包括Monitor和condition变量。
但是,当需要执行更多操作时,就会有一个用例。
例如,在QT中,存在一个根QObject
类,它构成线程亲和力,父子所有权层次结构和信号槽机制的基础。它也强制将指针用于QObject,但是Qt中的许多类都不继承QObject,因为它们不需要信号槽(特别是某些描述的值类型)。
Object
。
_hashCode
不是“使用其他容器”而是指向C ++ std::unordered_map
使用模板参数进行哈希处理,而不是要求元素类本身提供实现。就是说,就像C ++中所有其他好的容器和资源管理器一样,它是非侵入性的。它不会污染具有函数或数据的所有对象,以防万一以后有人在某些情况下需要它们。
因为没有所有对象共享的功能。在该接口中没有任何东西适合所有类。
每当您建立对象的高继承层次结构时,您都会遇到脆弱基类(Wikipedia)的问题。。
具有许多小的单独的(不同的,隔离的)继承层次结构可以减少遇到此问题的机会。
使所有对象成为单个庞大继承体系的一部分实际上可以确保您会遇到此问题。
cout.print(x).print(0.5).print("Bye\n")
-它不依赖语法operator<<
。
因为:
实施任何形式的virtual
功能都会引入一个虚拟表,该表需要每个对象的空间开销,而这在很多(大多数?)情况下都是不必要的。
实施toString
nonvirtually将是毫无用处的,因为它可以返回的唯一事情是对象的地址,这是非常用户不友好,以及调用者已经访问了,不像在Java中。
同样,非虚拟的equals
或hashCode
只能使用地址来比较对象,这再次是毫无用处的,甚至常常是完全错误的-与Java不同,对象经常在C ++中复制,因此区分对象的“身份”甚至不总是有意义或有用的。(例如int
真应该不会有其他的身份比它的价值......相同值的两个整数应该是平等的。)
shared_ptr<Foo>
以查看它是否也是一个基类shared_ptr<Bar>
(或者同样适用于其他指针类型),即使Foo
和Bar
是相互不了解的无关类。给定这样的东西与“原始指针”一起使用,考虑到如何使用这些东西的历史,将是昂贵的,但是对于无论如何将要堆存储的东西,增加的成本将是最小的。
具有一个根对象限制了您可以做的事情和编译器可以做的事情,而没有太多收益。
通用根类使得创建任何容器并使用提取它们成为可能dynamic_cast
,但是如果您需要任何容器,那么无需通用根类就boost::any
可以做到这一点。和boost::any
还支持原语-甚至可以支持小缓冲区优化,并以Java的说法将它们几乎“拆箱”。
C ++支持并繁荣发展值类型。文字和程序员编写的值类型。C ++容器有效地存储,排序,散列,使用和产生值类型。
继承,特别是那种整体继承Java样式基类暗示的继承,需要基于自由存储的“指针”或“引用”类型。您对数据的句柄/指针/引用持有一个指向类接口的指针,并且多态可以表示其他内容。
尽管这在某些情况下很有用,但是一旦您将自己与“通用基类”结合使用,就可以将整个代码库锁定在该模式的成本和负担范围内,即使它没有用。
在调用站点或使用它的代码中,几乎总是比“类型是对象”更了解类型。
如果函数很简单,则将函数编写为模板会为您提供基于鸭子类型的编译时多态性,而不会丢弃调用站点上的信息。如果函数更复杂,则可以执行类型擦除,从而可以构建并存储(在编译时)要对类型执行的统一操作(例如,序列化和反序列化),以供运行时使用(在运行时)使用。代码在其他翻译单元中。
假设您有一些想要所有内容都可序列化的库。一种方法是拥有一个基类:
struct serialization_friendly {
virtual void write_to( my_buffer* ) const = 0;
virtual void read_from( my_buffer const* ) = 0;
virtual ~serialization_friendly() {}
};
现在,您编写的所有代码都可以了serialization_friendly
。
void serialize( my_buffer* b, serialization_friendly const* x ) {
if (x) x->write_to(b);
}
除了不是std::vector
,所以现在您需要编写每个容器。而不是从bignum库中获得的那些整数。并不是您写的那种您认为不需要序列化的类型。而不是tuple
或int
或double
或或std::ptrdiff_t
。
我们采取另一种方法:
void write_to( my_buffer* b, int x ) {
b->write_integer(x);
}
template<class T,
class=std::enable_if_t< void_t<
std::declval<T const*>()->write_to( std::declval<my_buffer*>()
> >
>
void write_to( my_buffer* b, T const* x ) {
if (x) x->write_to(b);
}
template<class T>
void serialize( my_buffer* b, T const& t ) {
write_to( b, t );
}
看来,它什么也不做。除了现在,我们可以write_to
通过重写write_to
为类型的名称空间中的自由函数或类型中的方法来扩展。
我们甚至可以编写一些类型擦除代码:
namespace details {
struct can_serialize_pimpl {
virtual void write_to( my_buffer* ) const = 0;
virtual void read_from( my_buffer const* ) = 0;
virtual ~can_serialize_pimpl() {}
};
}
struct can_serialize {
void write_to( my_buffer* b ) const { pImpl->write_to(b); }
void read_from( my_buffer const* b ) { pImpl->read_from(b); }
std::unique_ptr<details::can_serialize_pimpl> pImpl;
template<class T> can_serialize(T&&);
};
namespace details {
template<class T>
struct can_serialize : can_serialize_pimpl {
std::decay_t<T>* t;
void write_to( my_buffer*b ) const final override {
serialize( b, std::forward<T>(*t) );
}
void read_from( my_buffer const* ) final override {
deserialize( b, std::forward<T>(*t) );
}
can_serialize(T&& in):t(&in) {}
};
}
template<class T> can_serialize::can_serialize<T>(T&&t):pImpl(
std::make_unique<details::can_serialize<T>>( std::forward<T>(t) );
) {}
现在我们可以采用任意类型并将其自动装箱到can_serialize
接口中,以便您serialize
稍后通过虚拟接口进行调用。
所以:
void writer_thingy( can_serialize s );
是一个需要任何可以序列化的函数,而不是
void writer_thingy( serialization_friendly const* s );
和第一,与第二,它可以处理int
,std::vector<std::vector<Bob>>
自动。
编写它并不需要花费太多,特别是因为这种事情是您很少想做的事情,但是我们获得了将任何东西视为可序列化的能力,而无需基类型。
而且,我们现在可以std::vector<T>
通过简单地覆盖使一流的公民变成可序列化的对象write_to( my_buffer*, std::vector<T> const& )
-通过该重载,可以将其传递给can_serialize
,而get的可序列std::vector
化性存储在vtable中,并由进行访问.write_to
。
简而言之,C ++足够强大,您可以在需要时即时实现单个基类的优点,而不必在不需要时付出强制继承层次结构的代价。而且,只需要一个基础(伪造与否)的时间就很少见了。
当类型实际上是它们的标识,而您知道它们是什么时,优化机会比比皆是。数据存储在本地和连续的位置(这对于现代处理器上的缓存友好性而言非常重要),编译器可以轻松地了解给定操作的功能(而不是必须跳过不透明的虚拟方法指针,否则会导致未知代码)另一面),可以使指令进行最佳排序,并且将较少的圆钉锤入圆孔中。
上面有很多好的答案,很明显的事实是,您可以对所有基类进行任何操作都可以通过@ratchetfreak的答案所示的其他方式来做得更好,并且对此的评论非常重要,但是还有另一个原因,那就是避免创建继承钻石使用多重继承时。如果您在通用基类中具有任何功能,则一旦开始使用多重继承,就必须开始指定要访问的哪个变体,因为在继承链的不同路径中它可能会以不同的方式进行重载。而且基础不能是虚拟的,因为这会非常低效(要求所有对象都拥有一个虚拟表,而这在内存使用和位置方面可能会付出巨大的代价)。这将很快成为后勤噩梦。
实际上,Microsoft的早期C ++编译器和库(我知道Visual C ++是16位的)就具有这样的类CObject
。
但是,您必须知道,那时这个简单的C ++编译器不支持“模板”,因此不可能提供类似的类std::vector<class T>
。取而代之的是,“向量”实现只能处理一种类型的类,因此存在一个与std::vector<CObject>
今天相当的类。因为CObject
几乎是所有类的基类(不幸的是,不是现代编译器中CString
的等效string
类),所以可以使用此类存储几乎所有类型的对象。
因为现代的编译器支持模板,所以不再给出“通用基类”的用例。
您必须考虑以下事实:使用这样的通用基类会花费(一点)内存和运行时-例如在调用构造函数时。因此,使用此类时存在弊端,但至少在使用现代C ++编译器时,此类类几乎没有用例。
TObject
甚至在MFC 诞生之前就已经有了。不要责怪微软的那部分设计,那段时间对几乎所有人来说都是一个好主意。
我将提出另一个来自Java的原因。
因为至少没有一堆样板,您无法为所有内容创建基类。
您可能可以在自己的类中使用它-但是您可能会发现最终要复制大量代码。例如:“我不能std::vector
在这里使用它,因为它没有实现IObject
-我最好创建一个新的派生类IVectorObject
来做正确的事情……”。
每当您处理内置或标准库类或其他库中的类时,都是如此。
现在,如果将它内置到语言中,您最终会遇到诸如Java中的Integer
和int
混乱之类的问题,或者对语言语法进行了很大的更改。(请记住,我认为其他一些语言在将其构建为每种类型方面也做得很好-红宝石似乎是一个更好的示例。)
还要注意,如果您的基类不是运行时多态的(即使用虚函数),则可以从使用诸如框架之类的特性中获得相同的收益。
例如,代替.toString()
您可能会有以下操作:(注意:我知道您可以使用现有的库等进行整洁,这只是一个说明性示例。)
template<typename T>
struct ToStringTrait;
template<typename T>
std::string toString(const T & t) {
return ToStringTrait<T>::toString(t);
}
template<>
struct ToStringTrait<int> {
std::string toString(int v) {
return itoa(v);
}
}
template<typename T>
struct ToStringTrait<std::vector<T>> {
std::string toString(const std::vector<T> &v) {
std::stringstream ss;
ss<<"{";
for(int i=0; i<v.size(); ++i) {
ss<<toString(v[i]);
}
ss<<"}";
return ss.str();
}
}
可以说“无效”扮演了通用基类的许多角色。您可以将任何指针投射到void*
。然后,您可以比较那些指针。您可以static_cast
返回原始班级。
然而,你有什么不能做void
,你可以做的Object
是使用RTTI找出你真的有什么类型的对象。最终,这取决于C ++中并非所有对象都具有RTTI,并且实际上可能有零宽度的对象。
[[no_unique_address]]
,编译器可以使用,将成员子对象的宽度设置为零。
[[no_unique_address]]
它将允许编译器转换为EBO成员变量。
Java采用了不存在未定义行为的设计哲学。代码如:
Cat felix = GetCat();
Woofer Rover = (Woofer)felix;
Rover.woof();
将测试是否felix
拥有Cat
实现接口的子类型Woofer
; 如果这样做,它将执行强制转换和调用woof()
,如果不执行,将抛出异常。 代码的行为是完全定义是否felix
工具Woofer
与否。
C ++秉承这样的理念:如果程序不应该尝试某些操作,则尝试执行该操作也不会影响生成的代码,并且计算机不应浪费时间尝试在“应该”情况下限制行为。永远不会出现。在C ++中,添加适当的间接运算符以将a强制转换*Cat
为a *Woofer
,当强制转换合法时,代码将产生已定义的行为,但如果不是,则代码将产生未定义的行为。
拥有通用的事物基本类型可以在该基本类型的派生对象中验证强制类型转换,并进行尝试转换操作,但是验证强制类型转换比简单地假设它们是合法的并且希望不会发生任何不良情况要昂贵得多。C ++的哲学是这样的验证需要“支付您(通常)不需要的东西”。
与C ++有关的另一个问题,但对于新语言而言并不是问题,如果多个程序员各自创建一个公共基数,从中派生自己的类,然后编写代码以使用该公共基类的东西,这样的代码将无法与使用其他基类的程序员开发的对象一起使用。如果一种新语言要求所有堆对象都具有通用的标头格式,并且从未允许不允许使用的堆对象,那么一种需要使用此类标头引用堆对象的方法将接受对任何堆对象的引用。可以创造。
我个人认为,在语言/框架中,具有一种常见的询问对象“您是否可以转换为X型”的功能是一项非常重要的功能,但是如果从一开始就没有将这种功能内置到语言中,则很难以后添加。我个人认为,应该首先将这样的基类添加到标准库中,强烈建议所有将被多态使用的对象都应从该基类继承。让程序员各自实现自己的“基本类型”将使在不同人员的代码之间传递对象更加困难,但是拥有许多程序员可以继承的通用基本类型将使其更加容易。
附录
使用模板,可以定义一个“任意对象持有人”,并询问其中包含的对象的类型。Boost程序包包含这样的东西any
。因此,即使C ++没有标准的“对任何内容进行类型检查的引用”类型,也可以创建一个类型。这并不能解决语言标准中没有的问题,即不同程序员的实现之间不兼容,但是它确实解释了C ++是如何在没有基础类型的情况下获得的:通过使创建成为可能像一个人的东西。
Woofer
是接口并且Cat
是可继承的,则强制转换将是合法的,因为可能存在(如果不是现在,则可能是将来)WoofingCat
从Cat
和实现的a Woofer
。请注意,在Java编译/链接模型下,创建WoofingCat
不需要访问Cat
nor 的源代码Woofer
。
Cat
到a的Woofer
转换,并会回答“您是否可以转换为X类型”的问题。C ++将允许您强制转换,原因,嘿,也许您实际上知道自己在做什么,但是如果这不是您的真正意图,它也可以帮助您。
dynamic_cast
它将指向已定义的行为(如果它指向一个多态对象),如果未指向未定义的行为,则从语义角度来看……