Stroustrup说:“不要立即为所有类(对象类)发明一个独特的基础。通常,如果没有很多/大多数类,您会做得更好。” (C ++编程语言第四版,第1.3.4节)

为什么对所有内容都使用基类通常是个坏主意,何时创建一个有意义的主意?

评论

因为C ++不是Java ...而且您不应该尝试将其强制为Java。

在Stack Overflow上被问到:为什么C ++中没有基类?

另外,我不同意“主要基于意见”的直接投票。对此有非常具体的原因可以解释,因为答案证明了这个问题和相关的SO问题。

这就是敏捷的“您将不需要它”原则。除非您已经确定有特殊需要,否则请不要这样做(直到您这样做)。

@AK_:您的评论中缺少“愚蠢到”。

#1 楼

因为该对象将具有什么功能?在Java中,所有基本类都有一个toString,一个hashCode和相等性以及一个monitor + condition变量。


ToString仅对调试有用。
hashCode仅在以下情况下有用您想将其存储在基于哈希的集合中(C ++的首选是将哈希函数作为模板参数传递给容器,或者完全避免使用std::unordered_*,而是使用std::vector和普通无序列表)。基础对象可以在编译时获得帮助,如果它们的类型不同,则它们不能相等。在C ++中,这是一个编译时错误。
最好在逐个案例的基础上显式地包含Monitor和Condition变量。用例。

例如,在QT中,存在根QObject类,该类构成线程亲缘关系,父子所有权层次结构和信号槽机制的基础。它也强制将指针用于QObject,但是Qt中的许多类都不继承QObject,因为它们不需要信号槽(特别是某些描述的值类型)。

评论


您可能忘了提到Java拥有基类的主要原因:在泛型之前,集合类需要基类才能起作用。一切(内部存储,参数,返回值)都键入对象。

–亚历山大·杜宾斯基(Aleksandr Dubinsky)
15年2月19日在9:10

@AleksandrDubinsky:泛型只添加了语法糖,除了修饰外没有任何改变。

–重复数据删除器
15年5月29日在13:13

我认为,哈希代码,相等性和监视器支持也是Java中的设计错误。谁认为将所有对象都锁成一个好主意?

–usr
15年6月19日在12:44

是的,但是没人想要。您上一次需要锁定对象是什么时候,并且无法实例化单独的锁定对象来执行此操作。这是非常罕见的,并且会给所有事情带来负担。当时,Java人士对线程安全性的理解很差,无法通过所有对象作为锁以及现在不赞成使用的线程安全集合来证明。线程安全是一项全局属性,而不是针对每个对象的属性。

–usr
2015年12月7日13:35



“ hashCode仅在要将其存储在基于哈希的集合中时才有用(C ++中的首选项是std :: vector和纯无序列表)。” _hashCode的真正反驳不是“使用其他容器”,而是指出C ++的std :: unordered_map使用模板参数进行哈希处理,而不是要求元素类本身提供实现。就是说,就像C ++中所有其他好的容器和资源管理器一样,它是非侵入性的。它不会污染具有功能或数据的所有对象,以防万一以后有人需要它们时使用它们。

– underscore_d
18-10-27在18:01



#2 楼

因为没有所有对象共享的功能。在该接口中没有任何东西适合所有类。

评论


为了简单起见,+ 1是唯一的原因。

– BWG
15年2月15日在23:58

在我有经验的大型框架中,通用基类提供了上下文中所需的序列化和反射基础结构。嗯这只是导致人们将一堆废料与数据和元数据一起序列化,并使数据格式太大,太复杂以至于效率不高。

– dmckee ---前主持人小猫
2015年2月16日在2:56

@dmckee:我也认为序列化和反射并不是普遍有用的需求。

– DeadMG
15年2月16日在8:07

@DeadMG:“但是如果您需要保存一切,该怎么办?”

–deworde
15年2月16日在8:55

我不知道,您将其用引号括起来,使用大写字母,但人们看不到这个笑话。 @MSalters:嗯,这应该很容易,它只有很少的状态,您只需指定它就可以了。我可以在列表中写上我的名字而无需进入递归循环。

–deworde
15年2月16日在15:45

#3 楼

每当您建立对象的高继承层次结构时,您都会遇到脆弱基类(Wikipedia)的问题。

拥有许多小的单独的(不同的,孤立的)继承层次结构可以减少遇到此问题的机会。

使所有对象成为一个庞大的继承层次结构的一部分实际上可以保证您将遇到此问题。

评论


当基类(在Java“ java.lang.Object”中)不包含任何调用其他方法的方法时,就不会发生脆弱基类问题。

–马丁·罗森瑙(Martin Rosenau)
15年2月15日在19:42

一个强大的有用的基类!

–迈克·纳基斯(Mike Nakis)
15年2月15日在20:21

@MartinRosenau ...就像您可以在C ++中完成的一样,而无需掌握主基础类!

– gbjbaanb
15年2月16日在8:48

@DavorŽdralo因此,C ++对于基本功能(“ operator <<”而不是诸如“ DebugPrint”之类的明智名称)有一个愚蠢的名称,而Java对于您所编写的每个类都具有基本类的怪胎,没有例外。我想我更喜欢C ++的疣。

–塞巴斯蒂安·雷德尔(Sebastian Redl)
15年2月16日在15:24

@DavorŽdralo:函数的名称无关紧要。以语法cout.print(x).print(0.5).print(“ Bye \ n”)语法显示图像-它不依赖于运算符<<。

– MSalters
15年2月16日在15:40

#4 楼

因为:


您不应该为不使用的东西付费。
这些功能在基于值的类型系统中比在基于引用的类型系统中意义更小。

非虚拟地实现virtual几乎是没有用的,因为它唯一可以返回的就是对象的地址,这对用户非常不友好。同样,非虚拟的toStringequals只能使用地址来比较对象,这再次是毫无用处的,甚至常常是完全错误的-与Java中的对象不同在C ++中经常被复制,因此区分对象的“身份”甚至不总是有意义或有用的。 (例如,一个hashCode确实不应该具有除其值以外的标识...具有相同值的两个整数应该相等。)

评论


关于此问题和Mike Nakis指出的脆弱的基类问题,请注意有趣的研究/建议,基本上是通过在内部(即从同一类中调用)所有方法都非虚拟的,但在执行时保留其虚拟行为来在Java中修复外部调用为了获得旧的/标准的行为(即到处都是虚拟的),该提案引入了一个新的open关键字。我认为除了几篇论文外,它没有其他地方。

–嘶嘶声
2015年2月17日在19:05



有关该论文的更多讨论可以在lambda-the-ultimate.org/classic/message12271.html中找到

–嘶嘶声
15年2月17日在19:13

即使Foo和Bar是彼此不相关的无关类,拥有一个通用的基类也可以测试任何shared_ptr 是否也是shared_ptr (或者其他指针类型也是如此) 。给定这样的东西与“原始指针”一起使用,考虑到如何使用它们的历史,将是昂贵的,但是对于无论如何将要堆存储的东西,增加的成本将是最小的。

–超级猫
15年2月18日在17:32

虽然为所有内容提供通用基类可能没有帮助,但我确实认为,存在一些相当大的对象类别,对于通用基类将有所帮助。例如,可以以两种方式使用Java中的许多(相当多的,如果不是多数的话)类:作为可变数据的非共享持有者,或作为任何人都不允许修改的数据的共享持有者。在这两种使用模式下,托管指针(引用)都用作基础数据的代理。能够为所有此类数据使用通用的托管指针类型是有帮助的。

–超级猫
15年2月18日在17:44

#5 楼

拥有一个根对象限制了您可以做的事情以及编译器可以做的事情,而没有多大的收益。 ,但是如果您需要任何容器,那么可以使用类似dynamic_cast的方法而无需通用的根类。而且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() {}
};


现在,您编写的每个代码都可以是boost::any。除了不是serialization_friendly,所以现在您需要编写每个容器。而不是从bignum库中获得的那些整数。并不是您写的那种您认为不需要序列化的类型。而不是std::vectortupleintdouble

我们采用另一种方法:

void serialize( my_buffer* b, serialization_friendly const* x ) {
  if (x) x->write_to(b);
}


,其中包括:好吧,貌似什么也没做。除了现在,我们可以通过将std::ptrdiff_t覆盖为类型的名称空间或类型的方法中的自由函数来扩展write_to。我们甚至可以编写一些类型擦除代码:

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接口中,使您可以在以后通过虚拟接口调用can_serialize。 >因此:

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) );
) {}


是一个可以接受任何可以序列化的函数,而不是

void writer_thingy( can_serialize s );


,第一个与第二个不同,它可以自动处理serializeint

编写它并不需要很多,尤其是因为这种事情是您很少想做的事情,但是我们

此外,我们现在可以通过简单地覆盖std::vector<std::vector<Bob>>来使std::vector<T>可序列化为一等公民,有了这个过载,传递给write_to( my_buffer*, std::vector<T> const& )can_serialize的可序列化性都存储在vtable中,并可以由std::vector访问。

简而言之,C ++功能强大,可以在运行时即时实现单个基类的优点。不需要,而不必在不需要时付出强制继承层次结构的代价。而且只需要一个基数(伪造与否)的时间就很少见了。当类型实际上是它们的标识,并且您知道它们是什么时,优化机会比比皆是。数据存储在本地和连续的位置(这对于现代处理器上的缓存友好性而言非常重要),编译器可以轻松地了解给定操作的功能(而不是必须跳过不透明的虚拟方法指针,从而导致未知代码)另一面),可以使指令进行最佳排序,并且将较少的圆钉锤入圆孔中。

#6 楼

上面有很多好的答案,很明显的事实是,可以使用其他所有方式来完成所有对象的基类的任何工作,如@ratchetfreak的答案所示,对此的评论非常重要,但是还有另一个原因,那就是避免在使用多重继承时创建继承菱形。如果您在通用基类中具有任何功能,则一旦开始使用多重继承,就必须开始指定要访问的哪个变体,因为在继承链的不同路径中它可能会以不同的方式进行重载。而且基础不能是虚拟的,因为这会非常低效(要求所有对象都拥有一个虚拟表,而这在内存使用和位置方面可能会付出巨大的代价)。这将很快成为后勤上的噩梦。

评论


钻石问题的一种解决方案是让所有通过多个路径非虚拟地派生基本类型的类型覆盖该基本类型的所有虚拟成员。如果从一开始就在语言中内置了通用基本类型,则编译器可以自动生成合法的(尽管不一定令人印象深刻)默认实现。

–超级猫
15年2月18日在17:23

#7 楼

实际上,Microsoft的早期C ++编译器和库(我知道Visual C ++,它是16位的)有一个名为CObject的类。

但是您必须知道当时不支持“模板”简单的C ++编译器,因此不可能使用std::vector<class T>之类的类。取而代之的是,“向量”实现只能处理一种类型的类,因此存在一个与今天的std::vector<CObject>相当的类。因为CObject是几乎所有类的基类(不幸的是CString并不是现代编译器中的string的等效类),所以您可以使用此类存储几乎所有类型的对象。

因为现代编译器支持模板

不再需要考虑这种情况。对构造函数的调用。因此,使用此类时存在弊端,但至少在使用现代C ++编译器时,此类类几乎没有用例。

评论


那是MFC吗? [注释填充]

–user253751
2015年2月15日在21:09

确实是MFC。面向对象设计的闪亮灯塔,向世界展示了应该如何做。等一下...

– gbjbaanb
15年2月16日在8:50

@gbjbaanb Turbo Pascal和Turbo C ++甚至在MFC诞生之前就有了自己的TObject。不要责怪微软的那部分设计,那段时间对几乎所有人来说都是一个好主意。

–hvd
15年2月16日在18:21

甚至在没有模板之前,尝试用C ++编写Smalltalk都会产生可怕的结果。

–JDługosz
15年2月16日在22:14

@hvd尽管如此,MFC还是比Borland生产的任何产品差的面向对象设计的例子。

–法律
15年2月17日在18:25

#8 楼

我将提出来自Java的另一个原因。
因为至少没有一个样板,您无法为所有内容创建基类。
/>
您可能可以在自己的类中使用它-但是您可能会发现最终要复制大量代码。例如。 “我不能在这里使用std::vector,因为它没有实现IObject-我最好创建一个新的派生的IVectorObject,做正确的事...”。

无论何时您遇到这种情况正在处理内置库或标准库类或其他库中的类。

现在,如果将其内置到语言中,则最终会遇到Java中的Integerint混淆,或对语言语法进行较大的更改。 (请记住,我认为其他一些语言在将其构建为每种类型方面也做得很好-ruby似乎是一个更好的示例。)

还要注意,如果您的基类不是运行时多态的(即使用虚函数),可以从使用诸如框架之类的特性中获得相同的收益。

例如而不是.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();
  }
}


#9 楼

可以说,“ void”扮演着通用基类的许多角色。您可以将任何指针强制转换为void*。然后,您可以比较那些指针。您可以将static_cast返回到原始类。

但是对void不能执行的操作(对于Object可以执行的操作)是使用RTTI来确定您真正拥有的对象的类型。最终,这取决于C ++中并非所有对象都具有RTTI,并且实际上可能有零宽度的对象。

评论


仅零宽度的基类子对象,而不是普通的子类。

–重复数据删除器
15年2月16日在19:18

@Deduplicator通过更新,C ++ 17添加了[[no_unique_address]],编译器可以使用它来为成员子对象提供零宽度。

– underscore_d
18-10-27在18:08

@underscore_d您的意思是计划用于C ++ 20,[[no_unique_address]]将允许编译器使用EBO成员变量。

–重复数据删除器
18-10-27在18:15

@Deduplicator糟糕,是的。我已经开始使用C ++ 17,但是我想我仍然认为它比实际更先进!

– underscore_d
18-10-27在18:17

#10 楼

Java采用了不存在未定义行为的设计哲学。如下代码:

Cat felix = GetCat();
Woofer Rover = (Woofer)felix;
Rover.woof();


将测试felix是否持有实现接口CatWoofer子类型;如果这样做,它将执行强制转换并调用woof();否则,将引发异常。无论felix是否实现Woofer,都完全定义了代码的行为。

C ++的哲学是,如果程序不应该尝试某些操作,则生成的代码将执行什么操作无关紧要。尝试执行该操作,并且计算机不应浪费时间试图限制行为,以防“应”发生。在C ++中,添加适当的间接运算符以便将*Cat强制转换为*Woofer,代码在强制转换合法时将产生已定义的行为,而在强制转换不合法时将产生未定义的行为。

具有共同的基础用于事物的类型可以在该基本类型的派生对象中验证强制类型转换,还可以进行尝试转换操作,但是验证强制类型转换比简单地假设它们是合法的并且希望不会发生任何不良情况要昂贵得多。 C ++的哲学是这样的验证需要“支付(通常)不需要的东西”。

另一个与C ++相关的问题,但是对于新语言而言,这不是问题,如果几个程序员各自创建一个公共基类,从中派生自己的类,然后编写代码以使用该公共基类的东西,则此类代码将无法与使用不同基类的程序员开发的对象一起使用。如果一种新的语言要求所有堆对象都具有通用的标头格式,并且从未允许不允许使用的堆对象,那么一种需要使用这样的标头引用堆对象的方法将接受对任何堆对象的引用。可以创造。

我个人认为,在语言/框架中,具有一种常见的询问对象“您是否可以转换为X型”的功能是一项非常重要的功能,但是如果从一开始就没有将这种功能内置到语言中,则很难以后添加。就我个人而言,我认为应该首先将这样的基类添加到标准库中,强烈建议所有将被多态使用的对象都应从该基类继承。让程序员各自实现自己的“基本类型”会使在不同人的代码之间传递对象更加困难,但是拥有许多程序员都可以继承的通用基本类型将使它变得更容易。

ADDENDUM

使用模板,可以定义一个“任意对象持有人”,并询问其中包含的对象的类型; Boost软件包包含一个称为any的东西。因此,即使C ++没有标准的“对任何内容进行类型检查的引用”类型,也可以创建一个类型。这并不能解决语言标准中没有的问题,即不同程序员的实现之间的不兼容性,但是它确实解释了C ++如何在没有基础类型的情况下获得C ++:像一个人的东西。

评论


该强制转换在C ++,Java和C#编译时失败。

–千年虫
15年2月18日在17:49

@milleniumbug:如果Woofer是一个接口并且Cat是可继承的,则强制转换将是合法的,因为可能存在(如果不是现在,则可能是将来)一个从Cat继承并实现Woofer的WoofingCat。请注意,在Java编译/链接模型下,创建WoofingCat不需要访问Cat或Woofer的源代码。

–超级猫
15年2月18日在18:14

C ++具有dynamic_cast,可以正确处理从Cat转换为Woofer的尝试,并会回答“您是否可以转换为X型”的问题。 C ++将允许您强制转换,原因,嘿,也许您实际上知道自己在做什么,但是如果这不是您的真正意图,它也可以帮助您。

–机器人K
15年2月18日在18:23

@RobK:您当然对语法是正确的;米卡帕。我已经阅读了更多有关dynamic_cast的内容,从某种意义上讲,现代C ++似乎使所有多态对象都从基本“多态对象”基类派生而来,该基本类具有识别对象类型(通常是vtable)所需的任何字段指针,尽管这是实现细节)。 C ++不会以这种方式描述多态类,但是,如果传递指向dynamic_cast的指针指向一个多态对象,则将具有已定义的行为;如果未指向它,则将传递未定义的行为,因此从语义角度来看...

–超级猫
2015年2月19日在16:22

...所有多态对象以相同的布局存储一些信息,并且所有对象都支持非多态对象不支持的行为;在我看来,这意味着无论它们的语言定义是否使用此类术语,它们的行为都好像是来自一个共同的基础。

–超级猫
2015年2月19日在16:25

#11 楼

实际上,对于所有以特定方式(主要是如果它们分配了堆)的对象,Symbian C ++确实具有通用基类CBase。它提供了一个虚拟的析构函数,将构造时的内存清零,并隐藏了复制构造函数。

背后的理由是它是一种用于嵌入式系统和C ++编译器的语言,而且规范确实很糟糕10年前。

不是所有的类都继承自此,只有一些。