262588213843476 · Gist

@nicebyte I disagree. If you're writing low-level data oriented code, virtual classes almost always get in your way. After years of practical low-level professional game engine programming and personal computer science research, I dare to say when you write code like a game engine you end up in a situations where you either have lots of objects that must be updated the frequently and efficiently or you are dealing with large and few entities (e.g. anything called *Manager, *System, etc). In the latter case an opaque struct pointer followed by several API functions that operate on an instance of that (opaque) struct is expressive enough to create a neat abstraction and I'd say much more OOP than what you'd get in C++, as all of its private implementation details (in the .c) are truly hidden from its interface (in the .h) where in C++ you are forced to declare its private members in the class interface (yuck #1). The former, you want to process all those objects in batches, grouping all the operations and common data relevant to a single object type in a "Type" struct. Something you simply cannot do with C++ because virtual tables are hidden and not supposed to be fetched or populated manually (yuck #2).

Want a practical example? Consider writing a raytracer. You could go ahead and make a Shape virtual interface that has an Intersect() method. Then you have a AggregateShape that has a std::vector<Shape*> (yuck #3) and its own Intersect() method that simply calls each child Shape Intersect in turn and returns the closest to the ray origin. Sure this simple enough, but I'd argue not good enough. A better way is, for instance, having a ShapeType struct with a uint32 containing a unique id and a bunch of function pointers such as (* Intersect)(). your AggregateShape can now be implemented with an array of ShapeTypes (using their unique ID as index) and its Intersect() function being arguably considerably faster, as it can be implemented per ShapeType and not per (any) Shape. New iteration, new ShapeType, store the address of its Intersect function in a register, call it in a hot loop for each shape of that type (naturally stored in an array by value, and not by pointer like in the previous case). Oh, and this also uses less memory, since you have removed the vptr from each Shape object.

No matter how hard I try, the more experienced I become, the less attractive C++ gets. Not to mention the various serious design mistakes the language has I will skip because I need to work. Let's just say C++ is a language that is prone to complexity which exponentially leads to even more complexity and bugs.

Totally agree with that. VTables are performance killer, yet people in C++ keep crying about Garbage Collectors. No, child, GC isn't what made Java slow, it was the excessive use of Heap and Virtual Calls. You do that in C++ and you get slow code. You don't use virtual calls and inheritance and excessive object orientation in C# (or Java), and it goes fast as hell when the JIT kicks on.

But you take OCaml with a GC and it isn't slow, (not until you misuse closures, which go on heap)

Also, C++ has no "0-cost" abstractions, because abstractions have mental costs. Nothing in C++ is free of cost, CPU time is cheap, but Brain time is not.

Sometimes I want performance, so that's why I use C++, but mostly I get that performance from tightly coupling to whatever library is that I use and was made with C++ and then doing LTO. (one day GraalVM will kill that feat and then I won't have any reason to use C++, because I can glue any C library to any higher level language without any interop cost, that thing is true engineering marvel)

Memory management hardly matters much, I got here because of a discussion on allocators. How it is allocated and freed doesn't matter, the layout of the memory matters, which is why we use C++.

But then there's STL to bork everything.

Want to go fast, just transpose your Array of Objects into an Object of Arrays, and then it doesn't matter if you use a GC because the Object is going to be allocated with its millions of elements all contiguous (*1). Then the only thing that matters is alignment to the cache lines, but can you control that with C++ ? no, so what's the point even.... of manual memory management ?

(*1) (its just one allocation, and one free, which divided by millions of elements makes the cost of any automatic memory management almost free)

Fortran only had arrays and that was good enough. Pointers are kind of an abstraction leak from the underlying hardware architecture, I don't think there's place for pointer arithmetic in higher level language, or do you guys like to do manual bank switching too ?

Read the original on gist.github.com ↗