|
I've recently been doing some low level C++ work, the kind of stuff where you have the Standard document open on one screen, and your code in the other. What I found interesting was how complicated relatively simple things become when you're aiming for standards conforming correctness.
To take a basic example, dynamically allocated objects and pointers are simple in principle - allocate with new, remember to deallocate with delete. Oh, except for arrays, if you use new[] then you have to use delete[]. And don't mix malloc and free with new and delete, although they're all available to you. Why yes you can dereference a dangling pointer, and it might even appear to work for a while.
Uninitialized pointers? No problem, you're the boss. Want to allocate some raw memory and then use it to for objects? You can do that, but if you get the alignment wrong, it might blow up in your face at runtime.
How do you check alignment anyway? Shh.. that's a secret! (Actually, it's an implementation detail of the compiler, which is almost the same thing)
Many of these memory management problems can be solved by smart pointers. But when I read Meyers' comment in Effective STL that his smart pointer class was buggy, I started thinking I might become a full time Python programmer.
The reason for most of the complexity in C++ is that the standard makes very few assumptions about the target system and compilers, and also tries to avoid imposing any extraneous overhead on the program. The STL introduces a good set of higher level functionality, but brings with it more complexity to master.
There are a lot of subtleties in C++ that most of us never give a moment's thought to until something goes wrong. The (More) Effective C++/STL books exist almost entirely to bridge the gap between our everyday understanding of C++ and the details of how it really works.
We don't have to abandon ship and start converting everything to C# and Python just yet though. I was motivated to write this post because I noticed that every time I thought to
myself, there must be an easier way to do this, there was -
Boost.
I had always thought of Boost for its add-on features like regular expressions and fancy math. However, Boost also takes many of the things in C++ that are good in principle (low level memory management, smart pointers, iterators...) and makes them good in practice.
The Boost community is full of hardcore C++ experts that have worked out solutions to handle even the most obscure scenarios that C++ presents, scenarios which most of us will never have the time or inclination to investigate. Boost is nice and modular too, so you can generally pick and choose the bits you want to use.
If you find yourself frustrated writing endless lines of boilerplate code for cases you don't even know will ever happen in your code, have a look through Boost and see if someone has done the work for you. You could be pleasantly surprised. Working in C++ can be great, you just have to bring a full toolbox to the job.
|
I've not found boost to be very modular, it's a monolithic library. Just count how many files you need to include to get smart pointers or delegates.
I've worked on projects that literally failed to compile because boost overloaded the compilers max recursive includes and pre-processor memory space. It's especially bad if you are using visual studio 2k3 or earlier as their pre-processor peaks are somewhere around 64k i believe. There might be a service pack to fix that but the point is that you should never have to know that the limit existed =).
For basic algorithms and data structures, there's nothing wrong with writing them once and putting them in a safe place for re-use through all your future professional projects. Using vaguely defined common libraries is fine if you are writing a school project or a quick batch tool.
As for STL containers. I have a love hate relationship with them. I still really don't get why they need to deallocate and reallocate things you stick in them, and for anything I need total control over I generally end up with containers full of pointers, which I then manage myself. Ultimately if it's speed critical code, once I have it working I write a custom class to do the job it was doing. As Benjamin points out these can be ferreted away and used in future projects.
I guess most of my approaches to writing stuff in C++ end up being more like I am writing in C because I am at heart an assembly coder.. But so far every other language I've looked at seems as full of bloat and their own foibles & inconsistencies that I take the "better the devil you know" attitude towards it all.
It has to be said that about a million years ago I thought that I'd pick up C++ in a couple of days. Which I did. What I did not realize is that I would still be learning to use it today!
It's true, Boost does require a bunch of bootstrap files to get started. The bare minimum for smart pointers is a half dozen headers and 3 subdirs of additional boost bits. I don't think that's so bad, but if you prefer something more minimalist, check out Loki: http://loki-lib.sourceforge.net/
Ben, sounds like you had a traumatic encounter with Boost in the past! Today's library is definitely not vaguely defined - many parts of it are included in the work of the ISO standards committee for C++. But over-zealous use of Boost (or STL) and bad use of templates could definitely bloat your code and build times. These problems can be managed by good coding practices. That's not really a fault of the library, it's a feature of C++. ;-)
This philosophy makes C++ more flexible, but also harder to learn and use correctly, even if you use 3rd party libs instead of creating your own. Still, this is an approach I pretty much like, and prefer above languages that give you one built-in solution for one problem, and require you to stick with it (if you don't like the built-in garbage collection algorithm, well, pick another language, or at least another compiler; whereas in C++, you can switch to another library), but unfortunately there's one major problem with it: due to different conventions / practices, it's quite frequently a royal pain in the ass to get multiple C++ libraries work together. This is where some sort-of-standard solutions like boost may be of great help.
C++ is complex, and offers a lot of freedom. You are very close to the compiler output, so to say (especially when using SIMD instructions, where c++ almost directly translates into assembly). This is why it's fast, and this is why it's used for games.
One can start using Boost and other big libraries, but I doubt that will still be fast enough (benchmarks will need to shed light on this though). The thing is; the more it supports, the slower it gets.
I understand why the gaming industry uses C++, I think it is a language that is fun to program in, but I try to keep an open mind to C# and libraries. I remain skeptical untill benchmarks compare speed, though.
Years ago I worked at a large game company that produced a very large website for casual games. It was made from Java and used an app server. Many Java "programmers" were hired. But since Java was their first language, they new'd up objects as needed with no thought as to whether they could re use the same variable 10 lines later. This overloaded the garbage collection system, so the website ran like molasses under load.
Java was created to run set top boxes originally; it's not (yet) high performance.
C++ is a boon for many things when compared to C. When compared to newer languages it is definitely more primitive. But it is faster as long as you know how things are implemented and don't rely too heavily on using standard classes where base types can be used. I still can't get over how opaque the string class is when compared to just using so-called C strings. The latter requires that you use libc functions to do even the most basic searching or concatenation, but the libc calls that do it are often written in optimized assembly code.
There is no magic. It all comes down to assembly language. Real work has to be done somewhere.
1) Can change a "holds a" relationship to "has a" by requiring inclusion of an additional header file.
2) Encourages ignorance about the lifetime of objects.
I often prefer explicit reference counting when an object needs to be "held" by many objects. It certainly is not without its problems though. One moral of the story is higher level, often useful, abstractions don't excuse the programmer from knowing how it works(as Mr. Allen's story relates).
And the diagnostic car computers can only report on stuff controlled by the engine computer, which means nothing in the suspension, or rear end, or tires, or probably the cooling system, is covered.
Finally, the difference between the car analogy and the language analogy is that more modern languages like Java cannot be used at a lower level, while in C++ I can always go back to libc for lower level interfaces if need be.
Everyone has valid points, I favor getting things done in a shorter amount of time. So I'd lean of the high level languages when possible. We just got to get Mono working on 360 so we can have a decent scripting language on all platforms (Like Howe was saying in his blog).
I'm into using boost and stl when possible. It's not worth rewriting and redebugging your containers and such IMO. Plus more people have used boost/stl than your own rolled code, so ramping people up to your code base is less painful. Also writing containers and other features is a complete waste of time if they are already implemented how I'd want them.
The argument about people miss using features in a high level language applies to every bit of code written anyway. And new employees are more likely to miss use in house code, than industry standard features and libraries. With industry standard features and libraries, the people learn that lesson once, rather than every time they encounter a new system (We'd like to hope at least;) ).
Lastly, Depeche mode is a pretty kick ass band:)
-Troy
To be completely fair, I also think that Haskell is a viable replacement for C++, so take this with a grain of salt.
2) Don't misuse Boost and STL. You can abuse any tool out there. The trick is to leverage the tool's advantages in service of the end goal, which is actually shipping a product (and that has nothing to do with religious wars about programming choices).
All that really matters to the people paying our salary is that, in the end, you have a great game that people want to play, and you get it out on time, and your marketing team did their job. Part of what goes into a game being great, is performance. You can't justify unconventional choices until the profiler on the target platform(s) actually tells you that the conventional choice is slow -- you just can't. If in the final months of a project, the profiler says "this container class is too slow", then you rip it out and produce your own custom container that alleviates that bottleneck.
I'm in favor of using C#/Mono for scripting high level game objects, but only so changes can be made and tested on the fly. This pretty much requires a programmer to script the game objects, or a simpler system written on top Mono to allow non programmers to add fluff to the game. I think Smart pointers have their place here, but pretty much nowhere else in a game (because of the overhead).
I like to share code between tools and runtime. For that reason I also use C++ for tools.
Don't get me wrong. I really enjoy C#. It's a fun language, and it's easy to get things done quickly.
But for me, C++ is where it's at for games.