The more language features and dependencies those game middleware libraries use, the harder they are to integrate and combine with each other. Some may require compiler options that contradict each other, or require a very recent compiler version, some do an absurd amount of dynamic memory allocations (which is easy when using the STL - ahem stdlib - carelessly), some may depend on exceptions, or on boost libraries etc...
It was always the pure C projects, or the 'simple C++ style' projects that were easiest to integrate, while projects which used a boost-style 'over-engineering' approach caused a lot of little problems during integration and also afterwards (they are usually much harder to debug, take much longer to compile, and so on...).
The only sensible thing for C++ libraries that aim to be easy to integrate is to avoid many of the typical 'modern and old C++-isms' outlined in the Orthodox C++ document (but nothing in there is particularly new or special or revolutionary, it's a collection of pragmatic advice learned the hard way, and should be fairly familiar in embedded and game development).
The other big problem (besides integration) is that some C++ features (like iostreams or RTTI) increase code bloat, and basically violate the "don't pay for what you don't use" principle, or dramatically decrease performance on some platforms (mainly using C++ exceptions on asm.js). I have written a blog post a little while ago which goes more into the details, the TL;DR is basically that a 'simple C++ style' may help to reduce code bloat and increase performance: http://floooh.github.io/2016/08/27/asmjs-diet.html
* Avoid multiple inheritance. It seldom helps and there are lots of confusing cases. Putting one object inside another is fine. If your inheritance tree is more than two or three deep, you're probably doing it wrong.
* RTTI is better than unchecked downcasting.
* STL collections are fine and should be used in preference to built-in arrays.
* Avoid Boost. If anything goes wrong, you'll spend days to weeks figuring it out.
* Exceptions are not that bad. Go and Rust now have de-facto exceptions, with all the complicated unwinding machinery, but they're called "panic" and "recover". Just make sure that anything you pass in an exception is a self-contained copyable object with no pointers to anything, because that's where ownership and allocation trouble appear.
* Don't do anything in a destructor that can fail or block. Like closing files opened for writing. Destructors are for releasing memory. Raising an exception in a destructor is not a good thing.
* Don't overload operators for anything other than math. It just confuses people.
* Streams are more cool than useful. But mostly harmless.
* unique_ptr and move semantics have promise, but it may be too soon for those features. If you have complex ownership issues, consider Rust, which has compile-time checking for that stuff.
Regarding unique_ptr and move semantics, I used both quite a lot on a recent project (VC++/clang/gcc) and have been pretty pleased with the results.
It can be quite hard to make things copyable sometimes, but it's usually easy to make everything at least moveable, so where in the past you might have had to use a pointer (leaving room for error) or a smart pointer (more coding, slow debug build, annoying in the debugger), you can now just always have a value.
And unique_ptr is good for documenting ownership; I used it for stuff that gets passed between threads, but of course there are other options. You don't get any useful compile-time checking, but if you get things wrong then it does at least go pop at runtime in an obvious way... which is better than nothing. (As the saying goes, C programmers know the value of nothing! And that's why we appreciate it so much when we get anything better.)
My two takeaways from the experience:
- moving as a language feature is far more useful than copying... I suspect (though without having thought through it especially thoroughly) C++ would have been heaps better if it started out with move semantics only, with copying being only by convention
- Rust has moved further up my todo list
https://doc.rust-lang.org/std/panic/fn.catch_unwind.html
Particularly you may not be able to catch all panics. Result<T,E> is the canonical way to handle errors in Rust.
Also in gamedev(where this looks like this comes from) we generally avoid RTTI(for size reasons) and STL(for allocation control reasons, see EASTL, etc).
Do you think << and >> in iostreams confuses people?
> unique_ptr and move semantics have promise, but it may be too soon for those features.
What makes it "too soon" for unique_ptr (or shared_ptr)? These are the most useful and long-overdue improvements to C++ I can think of in its entire history.
I'd like to know more about this. That means to RAII file types?
[1] https://akrzemi1.wordpress.com/2011/09/21/destructors-that-t...
Don't use C++ runtime wrapper for C runtime includes (<cstdio>, <cmath>, etc.), use C runtime instead (<stdio.h>, <math.h>, etc.)
I don't have any idea why it may be useful.
Since we often deal with scientific image processing, there's a heavy use of templates, but no “esoteric” metaprogramming, and it does not contradict this “orthodox” manifesto.
On the system design side, we mostly compose our systems out of rather heavy objects encapsulating separate testable subsystems. These objects exclusively use smart pointers and often exceptions and can use RTTI if it somehow aids postmortem analysis (e.g. including type_info in exceptions). Also, multi-threading also lives here, and it is usually heavily-modern-C++-flavored Intel TBB and/or simple message queues.
On the whole, it is Java-like on the top level, C-like on the bottom, templates at the basement, and whatever language helps best to stabilize the whole structure.
ADD: Overall, these “orthodox” qualities have significant effect on whether the library should be accepted by us in a serious project. I.e. mostly for this reason OpenCV use is mostly limited to prototypes and auxiliary software. However, Eigen and TBB are the exceptions, since they provide great features while being very well maintained with respect to reliability and compatibility.
In some ways this style was limiting, but in other ways it was refreshing coming from previous jobs where I was working with C++ loving developers, including just myself in an indie code base I'd written that was too C++-y for its own good. Sometimes, keeping the rules of the language in my head, writing "proper C++" and dealing with random gotchas was a big cognitive load. Writing simpler, almost C style code and focusing on data layout and transformation instead of class hierarchies and C++ syntax made me a better, more productive developer.
I also completely bombed an interview after working at this job for 4 years when I had a typo in assignment operator syntax (very embarrassing) and couldn't remember modern C++ syntax which I had read about but never used in a production environment (move semantics, didn't know shared_ptr needed a default delete for char[]), so I guess you win some and you lose some.
Copy and move semantics combined with heap allocations and STL container copy/move behaviors can get you into weird places and behaviors if you use them too liberally.
Yep. That would explain it. From your description, sounds like an idtech 3 lineage (CoD, perhaps? I'd keep guessing, but idtech 3 was so heavily licensed that it honestly could be anything).
It certainly wasn't idtech 4, which I think used a bit more C++ than you describe inside the engine itself.
There is a wisdom that you should never re-implement something that a library exists for, but IMHO a strict interpretation of that view leads to horrendously bloated projects that are nearly impossible to build and maintain. I see this sometimes in node projects where the developers will pull in half a dozen dependencies before they even consider writing a for loop.
The goal in general is to produce code whose behaviour, compilation and runtime performance, and resource consumption characteristics are evident from reading it, while remaining clear and easy to write and maintain. Such code is explicit in what it does, and so tend to be the bugs in it (which means they are usually easier to catch).
It raises the minimum bar for programmer skill and discipline.
It's hard to refute directly because there isn't any motivation or reasoning presented, only prescriptions (the author cites their "experience" in comments), so the best I can do is point out that most of it is really bad advice.
If I were to guess, C++ was designed in a certain way, and you really ought to understand it if you want to use it well (other languages do this too: it's not a fault!). So if you absolutely refuse to learn any thing that requires learning or a nonzero amount of effort, this is the sort of coding style you end up with.
If you want to refute it, why not say what you think is wrong with each of the prescriptions instead of just saying "most of it is bad"?
Personally I agree with most of the prescriptions but there's one I disagree with completely and another that I'd add a caveat to.
The one I disagree with is using c headers instead of their c++ wrappers. Some of the c++ wrappers do actually add value (e.g. cmath adding templated versions of abs, etc) and it's easier for me to remember a single consistent rule ("use the c++ wrappers, always") rather than a list of which ones to use the c++ wrapper for and which to use the c header for.
The one I'd add a caveat to is the one about not using anything from the STL that allocates: I think that's good advice under some circumstances, but not all. The STL containers are really useful for getting something up and running quickly, so I think it's fine to use them in that case and only switch to custom containers once allocation shows up as a hot spot in your profiling.
As a caveat to the caveat, I would add that STL classes should only ever be used as an internal implementation detail, never exposed as part of an API. This is because the implementation of the STL classes can change, causing binary incompatibility. For example, Microsoft changed their implementation of std::string between Visual Studio 2008 and 2010 (if I remember correctly; and possibly again since?); if you have a library compiled against the older std::string you can't use that in a project being compiled against the newer std::string and vice versa - unless you have the source for the library and can recompile it. Using your own classes protects you from that because it puts you in control of when the ABI changes.
That is what's wrong with them: if you follow them you'll end up writing worse code for no good reason. I would be a lot more interested in discussing the reasons the author might have had, but they don't present them, so there's little to discuss there.
Reasoning about code that can throw is almost like COMEFROM. Instead, on any serious bad (oom, etc) process aborts and is restarted.
OTOH, lambdas for callbacks helps writing decoupled interfaces a lot.
Two examples:
- In 2017 you don't say STL but "Standard Library" and saying that it has "bad memory management" means you don't understand how it works (because you can use whatever memory management you want for most containers and you can even use the algorithms on C-arrays).
- I don't see why you would want to use printf instead of libfmt for example.