81

Stroustrup says "Don’t immediately invent a unique base for all of your classes (an Object class). Typically, you can do better without it for many/most classes." (The C++ Programming Language Fourth Edition, Sec 1.3.4)

Why is a base-class-for-everything generally a bad idea, and when does it make sense to create one?

7
  • 17
    because C++ is not Java... And you shouldn't try forcing it to be.
    – AK_
    Feb 16, 2015 at 12:30
  • 10
    Asked on Stack Overflow: Why is there no base class in C++?
    – user22815
    Feb 16, 2015 at 15:12
  • 29
    Also, I disagree with the close votes for "primarily opinion based." There are very specific reasons that can be explained for this, as the answers attest both on this question and the linked SO question.
    – user22815
    Feb 16, 2015 at 15:14
  • 2
    It's the "you aren't gonna need it" principle of agile. Unless you have identified a particular need for it already, don't do it (until you do).
    – Jool
    Feb 16, 2015 at 16:39
  • 3
    @AK_: There's an "as stupid as" missing in your comment.
    – DeadMG
    Feb 19, 2015 at 14:04

11 Answers 11

76

Because what would that object have for functionality? In java all the Base class has is a toString, a hashCode & equality and a monitor+condition variable.

  • ToString is only useful for debugging.

  • hashCode is only useful if you want to store it in a hash-based collection (the preference in C++ is to pass a hashing function to the container as template param or to avoid std::unordered_* altogether and instead use std::vector and plain unordered lists).

  • equality without a base object can be helped at compile time, if they don't have the same type then they cannot be equal. In C++ this is a compile time error.

  • the monitor and condition variable is better included explicitly in a case by case basis.

However when there is more that it needs to do then there is a use-case.

For example in QT there is the root QObject class which forms the basis of thread affinity, parent-child ownership hierarchy and signal slots mechanism. It also forces use by pointer for QObjects, However many Classes in Qt don't inherit QObject because they have no need for the signal-slot (particularly the value types of some description).

9
  • 9
    You forgot to mention probably the main reason Java has a base class: Before generics, the collection classes needed the base class to function. Everything (internal storage, parameters, return values) was typed Object. Feb 19, 2015 at 9:10
  • 1
    @AleksandrDubinsky: And generics only added syntactic sugar, not really changing anythin but the polish. May 29, 2015 at 13:13
  • 5
    I would argue, hash code, equality and monitor support are design mistakes in Java as well. Who thought it was a good idea to make all objects a lock?!
    – usr
    Jun 19, 2015 at 12:44
  • 1
    Yeah, but nobody wants it. When was the last time you needed to lock an object and could not instantiate a separate lock object to do that. It is very rare and it puts a burden onto everything. The Java guys had a bad understanding of thread safety back then as evidences by all objects being a lock and by the thread safe collections that are now deprecated. Thread safety is a global property, not a per-object one.
    – usr
    Dec 7, 2015 at 13:35
  • 2
    "hashCode is only useful if you want to store it in a hash-based collection (the preference in C++ is for std::vector and plain unordered lists)." The real counterargument to _hashCode is not 'use a different container' but rather pointing out that C++'s std::unordered_map does hashing using a template argument, instead of requiring the element class itself to provide the implementation. That is, like all other good containers & resource-managers in C++, it's non-intrusive; it doesn't pollute all objects with functions or data just in case someone might need them in some context later. Oct 27, 2018 at 18:01
103

Because there are no functions shared by all objects. There's nothing to put in this interface that would make sense for all classes.

11
  • 11
    +1 for simplicity of the answer, this is really the only reason.
    – BWG
    Feb 15, 2015 at 23:58
  • 7
    In the large framework that I have experience with, the common base class provides the serialization and reflection infrastructure that is desired in <whatever> context. Meh. It just resulted in people serializing a bunch of cruft together with the data and metadata and made the data format too large and complex to be efficient. Feb 16, 2015 at 2:56
  • 19
    @dmckee: I'd also argue that serialization and reflection are hardly universally useful needs.
    – DeadMG
    Feb 16, 2015 at 8:07
  • 16
    @DeadMG: "BUT WHAT IF YOU NEED TO SAVE EVERYTHING?"
    – deworde
    Feb 16, 2015 at 8:55
  • 8
    I don't know, you stick it in quotes, you use all caps, and people can't see the joke. @MSalters: Well, that should be easy, it's got a minimal amount of state, you just specify that it's there. I can write my name on a list without entering a recursive loop.
    – deworde
    Feb 16, 2015 at 15:45
25

Whenever you build tall inheritance hierarchies of objects you tend to run into the problem of the Fragile Base Class (Wikipedia.).

Having many small separate (distinct, isolated) inheritance hierarchies reduces the chances of running into this problem.

Making all of your objects part of a single humongous inheritance hierarchy practically guarantees that you are going to run into this problem.

13
  • 6
    When the base class (in Java "java.lang.Object") does not contain any methods that call other methods the Fragile Base Class problem cannot occur. Feb 15, 2015 at 19:42
  • 3
    A mighty useful base class that would be!
    – Mike Nakis
    Feb 15, 2015 at 20:21
  • 9
    @MartinRosenau ... like you can do in C++ without needing a master base class!
    – gbjbaanb
    Feb 16, 2015 at 8:48
  • 5
    @DavorŽdralo So C++ has a stupid name for a basic function ("operator <<" instead of something sensible like "DebugPrint"), while Java has a freak of a base class for absolutely every class you write, no exceptions. I think I like C++'s wart more. Feb 16, 2015 at 15:24
  • 4
    @DavorŽdralo: The name of the function is irrelevant. Image a syntax cout.print(x).print(0.5).print("Bye\n") - it doesn't hinge on operator<<.
    – MSalters
    Feb 16, 2015 at 15:40
24

Because:

  1. You shouldn't pay for what you don't use.
  2. These functions make less sense in a value-based type system than in a reference-based type system.

Implementing any sort of virtual function introduces a virtual-table, which requires per-object space overhead that is neither necessary nor desired in many (most?) situations.

Implementing toString nonvirtually would be pretty useless, because the only thing it could return is the object's address, which is very user-unfriendly, and which the caller already has access to, unlike in Java.
Similarly, a nonvirtual equals or hashCode could only use addresses to compare objects, which is again pretty useless and often even outright wrong -- unlike in Java, objects are copied frequently in C++, and hence distinguishing the "identity" of an object isn't even always meaningful or useful. (e.g. an int really should not have an identity other than its value... two integers of the same value should be equal.)

4
  • In relation to this issue and the fragile base class issue noted by Mike Nakis, note interesting research/proposal of fixing it in Java basically by making all methods internally (i.e. when called from the same class) non-virtual but keeping their virtual behavior when externally called; to get old/standard behavior (i.e. virtual everywhere) the proposal introduced a new open keyword. I don't think it went anywhere beyond a few papers though.
    – Fizz
    Feb 17, 2015 at 19:05
  • A bit more discussion about that paper can be found at lambda-the-ultimate.org/classic/message12271.html
    – Fizz
    Feb 17, 2015 at 19:13
  • Having a common base class would make it possible to test any shared_ptr<Foo> to see if it is also a shared_ptr<Bar> (or likewise with other pointer types), even if Foo and Bar are unrelated classes which know nothing about each other. Requiring that such a thing work with "raw pointers", given the history of how such things are used, would be expensive, but for things which are going to be heap-stored anyway, the added cost would be minimal.
    – supercat
    Feb 18, 2015 at 17:32
  • While it may not be helpful to have a common base class for everything, I do think there are some rather large categories of objects for which common base classes would be helpful. For example, many (a substantial plurality if not a majority) classes in Java may be used in two ways: as an unshared holder of mutable data, or as a sharable holder of data which nobody is allowed to modify. With both usage patterns, a managed pointer (reference) is used as a proxy for the underlying data. Being able to have a common managed pointer type for all such data is helpful.
    – supercat
    Feb 18, 2015 at 17:44
16

Having one root object limits what you can do and what the compiler can do, without much payoff.

A common root class makes it possible to create containers-of-anything and extract what they are with a dynamic_cast, but if you need containers-of-anything then something akin to boost::any can do it without a common root class. And boost::any also supports primitives -- it can even support the small buffer optimization and leave them almost "unboxed" in Java parlance.

C++ supports and thrives on value types. Both literals, and programmer written value types. C++ containers efficiently store, sort, hash, consume and produce value types.

Inheritance, especially the kind of monolithic inheritance Java style base classes imply, requires free-store based "pointer" or "reference" types. Your handle/pointer/reference to data holds a pointer to the interface of the class, and polymorphically could represent something else.

While this is useful in some situations, once you have married yourself to the pattern with a "common base class", you have locked your entire code base into the cost and baggage of this pattern, even when it isn't useful.

Almost always you know more about a type than "it is an object" at either the calling site, or in the code that uses it.

If the function is simple, writing the function as a template gives you duck-type compile time based polymorphism where information at the calling site is not thrown away. If the function is more complex, type erasure can be done whereby the uniform operations on the type you want to perform (say, serialization and deserialization) can be built and stored (at compile time) to be consumed (at run time) by the code in a different translation unit.

Suppose you have some library where you want everything to be serializable. One approach is to have a base class:

struct serialization_friendly {
  virtual void write_to( my_buffer* ) const = 0;
  virtual void read_from( my_buffer const* ) = 0;
  virtual ~serialization_friendly() {}
};

Now every bit of code you write can be serialization_friendly.

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

Except not a std::vector, so now you need to write every container. And not those integers you got from that bignum library. And not that type you wrote that you didn't think needed serialization. And not a tuple, or an int or a double, or a std::ptrdiff_t.

We take another approach:

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

which consists of, well, doing nothing, seemingly. Except now we can extend write_to by overriding write_to as a free function in the namespace of a type or a method in the type.

We can even write a bit of type erasure code:

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

and now we can take an arbitrary type and auto-box it into a can_serialize interface that lets you invoke serialize at a later point through a virtual interface.

So:

void writer_thingy( can_serialize s );

is a function that takes anything that can serialize, instead of

void writer_thingy( serialization_friendly const* s );

and the first, unlike the second, it can handle int, std::vector<std::vector<Bob>> automatically.

It didn't take much to write it, especially because this kind of thing is something you only rarely want to do, but we gained the ability to treat anything as serializable without requiring a base type.

What more, we can now make std::vector<T> serializable as a first-class citizen by simply overriding write_to( my_buffer*, std::vector<T> const& ) -- with that overload, it can be passed to a can_serialize and the serializabilty of the std::vector gets stored in a vtable and accessed by .write_to.

In short, C++ is powerful enough that you can implement the advantages of a single base class on-the-fly when required, without having to pay the price of a forced inheritance hierarchy when not required. And the times when the single base (faked or not) is required is reasonably rare.

When types are actually their identity, and you know what they are, optimization opportunities abound. Data is stored locally and contiguously (which is highly important for cache friendliness on modern processors), compilers can easily understand what a given operation does (instead of having an opaque virtual method pointer it has to jump over, on leading to unknown code on the other side) which lets instructions be optimally reordered, and fewer round pegs are hammered into round holes.

8

There are many good answers above, and the clear fact that anything you would do with a base-class-of-all-objects can be done better in other ways as shown by @ratchetfreak's answer and the comments on it is very important, but there is another reason, which is to avoid creating inheritance diamonds when multiple inheritance is used. If you had any functionality in a universal base class, as soon as you started using multiple inheritance you'd have to start specifying which variant of it you wanted to access, because it could be overloaded differently in different paths of the inheritance chain. And the base can't be virtual, because this would be very inefficient (requiring all objects to have a virtual table at a potentially enormous cost in memory usage and locality). This would become a logistical nightmare very quickly.

1
  • 1
    One solution to the diamond problem is to have all types which non-virtually derive a base type via multiple paths override all virtual members of that base type; if a common base type had been built into the language from the start, a compiler could auto-generate legitimate (though not necessarily impressive) default implementations.
    – supercat
    Feb 18, 2015 at 17:23
5

In fact Microsofts early C++ compilers and libraries (I know about Visual C++, 16 bit) had such a class named CObject.

However you have to know that at that time "templates" were not supported by this simple C++ compiler so classes like std::vector<class T> were not possible. Instead a "vector" implementation could only handle one type of class so there was a class that is comparable to std::vector<CObject> today. Because CObject was the base class of nearly all classes (unfortunately not of CString - the equivalent of string in modern compilers) you could use this class for storing nearly all kinds of objects.

Because modern compilers support templates this use case of a "generic base class" is no longer given.

You have to think about the fact that using such a generic base class will cost (a little) memory and runtime - for example in the call to the constructor. So there are drawbacks when using such a class but at least when using modern C++ compilers there is nearly no use case for such a class.

7
  • 3
    Is that MFC? [comment padding]
    – user253751
    Feb 15, 2015 at 21:09
  • 3
    It is indeed MFC. A shining beacon of OO design that showed the world how things should be done. Oh, wait...
    – gbjbaanb
    Feb 16, 2015 at 8:50
  • 4
    @gbjbaanb Turbo Pascal and Turbo C++ had their own TObject before MFC even existed. Don't blame Microsoft for that part of the design, it seemed like a good idea to pretty much everyone around that time.
    – hvd
    Feb 16, 2015 at 18:21
  • Even before templates, trying to write Smalltalk in C++ produced awful results.
    – JDługosz
    Feb 16, 2015 at 22:14
  • @hvd Still, MFC was a much worse example of object-oriented design than anything Borland produced.
    – Jules
    Feb 17, 2015 at 18:25
5

I'm going to suggest another reason that comes from Java.

Because you can't create a base-class for everything at least not without a bunch of boiler-plate.

You may be able get away with it for your own classes - but you'll probably find that you end up duplicating a lot of code. E.g. "I can't use std::vector here since it doesn't implement IObject - I'd better create a new derived IVectorObject that does the right thing...".

This will be the case whenever you're dealing with built-ins or standard library classes or classes from other libraries.

Now if it was built into the language you'd end up with things like the Integer and int confusion that is in java, or a large change to the language syntax. (Mind you I think some other languages have done a nice job with building it into every type - ruby seems like a better example.)

Also note that if your base class is not run-time polymorphic (i.e. using virtual functions) you could get the same benefit from using a traits like framework.

e.g. instead of .toString() you could have the following: (NOTE: I know you can do this neater using existing libraries etc, its just an illustrative example.)

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();
  }
}
3

Arguably "void" fulfils a lot of the roles of a universal base class. You can cast any pointer to a void*. You can then compare those pointers. You can static_cast back to the original class.

However what you can't do with void which you can do with Object is use RTTI to figure out what type of object you really have. This is ultimately down to how not all objects in C++ have RTTI, and indeed it's possible to have zero-width objects.

4
  • 1
    Only zero-width baseclass subobjects, not normal ones. Feb 16, 2015 at 19:18
  • @Deduplicator By way of update, C++17 adds [[no_unique_address]], which can be used by compilers to give zero width to member subobjects. Oct 27, 2018 at 18:08
  • 1
    @underscore_d You mean planned for C++20, [[no_unique_address]] will allow the compiler to EBO member-variables. Oct 27, 2018 at 18:15
  • @Deduplicator Whoops, yup. I already started using C++17, but I guess I still think it's more cutting-edge than it really is! Oct 27, 2018 at 18:17
2

Java takes the design philosophy that Undefined Behavior should not exist. Code such as:

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

will test whether felix holds a subtype of Cat that implements interface Woofer; if it does, it will perform the cast and invoke woof() and if it doesn't, it will throw an exception. The behavior of the code is fully defined whether felix implements Woofer or not.

C++ takes the philosophy that if a program shouldn't attempt some operation, it shouldn't matter what the generated code would do if that operation were attempted, and the computer shouldn't waste time trying to constrain behavior in cases that "should" never arise. In C++, adding the appropriate indirection operators so as to cast a *Cat to a *Woofer, the code would yield defined behavior when the cast is legitimate, but Undefined Behavior when it is not.

Having a common base type for things makes it possible to validate casts among derivatives of that base type, and also to do try-cast operations, but validating casts is more expensive than simply assuming that they're legitimate and hoping nothing bad happens. The C++ philosophy would be that such validation requires "paying for something you [usually] don't need".

Another problem which relates to C++, but wouldn't be a problem for a new language, is that if several programmers each create a common base, derive their own classes from that, and write code to work with things of that common base class, such code will be unable to work with objects developed by programmers who used a different base class. If a new language requires that all heap objects have a common header format, and has never allowed heap objects which didn't, then a method which requires a reference to a heap object with such a header will accept a reference to any heap object anyone could ever create.

Personally, I think that having a common means of asking an object "are you convertible to type X" is a very important feature in a language/framework, but if such a feature isn't built into a language from the start it's difficult to add it later. Personally, I think such a base class should be added to a standard library at first opportunity, with a strong recommendation that all objects that will be used polymorphically should inherit from that base. Having programmers each implement their own "base types" would make passing objects between different people's code more difficult, but having a common base type which many programmers inherited from would make it easier.

ADDENDUM

Using templates, it's possible to define an "arbitrary object holder" and ask it about the type of the object contained therein; the Boost package contains such a thing called any. Thus, even though the C++ doesn't have a standard "type-checkable reference to anything" type, it's possible to create one. This doesn't solve the aformentioned problem with not having something in the language standard, i.e. incompatibility between different programmers' implementations, but it does explain how C++ gets by without having a base type from which everything is derived: by making it possible to create something that acts like one.

13
  • That cast fails at compile time in C++, Java and C#. Feb 18, 2015 at 17:49
  • 1
    @milleniumbug: If Woofer is an interface and Cat is inheritable, the cast would be legitimate because there could exist (if not now, possibly in future) a WoofingCat which inherits from Cat and implements Woofer. Note that under the Java compilation/linking model, creation of a WoofingCat would not require access to the source code for Cat nor Woofer.
    – supercat
    Feb 18, 2015 at 18:14
  • 3
    C++ has dynamic_cast, which properly handles trying to cast from a Cat to a Woofer and will answer the question "are you convertible to type X". C++ will allow you to force a cast, cause hey, maybe you actually know what you're doing, but it will also help you out if that's not what you really mean to do.
    – Rob K
    Feb 18, 2015 at 18:23
  • 2
    @RobK: You're right about syntax of course; mea culpa. I've been reading a bit more about dynamic_cast and it seems that in a sense modern C++ has all polymorphic objects derive from a base "polymorphic object" base class with whatever field(s) are necessary to identify the object's type (typically a vtable pointer, though that's an implementation detail). C++ doesn't describe polymorphic classes that way, but passing a pointer to dynamic_cast will have defined behavior if it points to a polymorphic object, and Undefined Behavior if it doesn't, so from a semantic perspective...
    – supercat
    Feb 19, 2015 at 16:22
  • 2
    ...all polymorphic objects store some information with the same layout, and all support a behavior which is not supported by non-polymorphic objects; to my mind, that means they behave as though they derive from a common base, whether the language definition uses such terminology or not.
    – supercat
    Feb 19, 2015 at 16:25
1

Symbian C++ did in fact have a universal base class, CBase, for all objects that behaved in a particular way (mainly if they alloc'd heap). It provided a virtual destructor, zeroed the memory of the class on construction, and hid the copy constructor.

The rationale behind was it was a language for embedded systems and C++ compilers and specs were really really shit 10 years ago.

Not all classes inherited from this, only some.

Not the answer you're looking for? Browse other questions tagged or ask your own question.