Search this blog

26 February, 2011

Surviving C++

WARNING: This post (as 99% of my posts regarding C++) contains suggestions but also scathing critiques to what it could be your beloved language
If you are offended by the former, please before rushing to post a comment, at least make sure you've read and understood the "Defective C++" section of this faq, that contains some of the most high-level remarks (i.e. without going down to many of the details, like bad defaults or awkward keywords) on what is bad about C++ design. 
I know it's hard to get out of our own comfort zone, and we all tend to get defensive about things we are used to, even if they really suck. If you want to argue and discuss on the merit, then I'm happy to chat either via comments or you can find an MSN widget somewhere on the right of this blog page. If you just want to write that I don't understand C++ or something like that, avoid, it's just a waste of time (and I would really love to check if you are a better C/C++ or any other language programmer than I am...)

Introduction
 
If you've been reading my blog for some time, you know what I think about C++. It's an horrible mess that managed to be successful exactly because it's lame. It was designed to be a glorified macro system on top of C in order to please the market of C programmers, not scare them with this new "OO" thing but just give them something they could relate to, that they could reason in terms of C. In retrospect, it was not even a good at achieving that (see D for example) but it was the first and in a few years most C programmers were "won" by C++ (not all) so now we have to live with it.

How do we survive it? How do we manage to create projects and sell software made with it? Well, the best answer so far has been to "subset" it. Basically every professional studio working in C++ (that is, in gaming, everyone) restricts C++ in a "somewhat safe" subset that is more manageable and less tricky to work with, to avoid at least the biggest pitfalls.

What does it take to be a C++ masters? Is it the knowledge of fancy design patterns (omg!) or expression templates? Of course not, these are things that all the kids talk about out! To be a C++ master is really about knowing C++ defects more than its features, as it's demonstrated by these three fundamental, cornerstone books: Effective C++, More Effective C++, C++ Coding Standards.

In fact, no one uses straight C++ to make anything, in real world everyone starts by sub-setting C++ into something "safer" and then adding back the fundamental missing features (i.e. memory management, serialization, reflection and so on) via libraries, macros or  other trickery (i.e. code to code transformers, code generators or parsers and so on), so really the C++ we use is not a standard, but a studio specific version of the language.

This "custom" C++ is then enforced either informally via code reviews and coding standards, or more strictly with the help of configurable "lint" software.

So one day I decided to stop using the blog only to blame C++ but actually try to help developers by seeing if people could agree on some "gaming/realtime rendering" C++ standards. With the help of a shared notepad, I seeded the guidelines with my personal considerations and waited for people to start adding their own.


I think we got something interesting there, and I'll clean it up and post a snapshot on the blog of that work. But the more I got into this the more I grew dissatisfied with the guidelines, as it seems to me most of them are "aesthetic" in some sense that they are certainly needed to reduce the WTF/minute ratio when dealing with a C++, but they don't change fundamentally the experience of developing a project.

What is fundamental, really, when dealing with a big software project? What is the first thing we need in order to survive?

Everything else does not matter (much)

Apart from running and producing an output, what is the most important quality that a software project has to have, from a development perspective? If you think about it it's obvious. The most important quality that we want is the ability to change it.
That is what we do all day, all days, we edit code. We don't just add functionality. We don't design a perfect, immutable system that will withstand centuries (especially not if you're making a game!).

Code changes, code rots, hardware changes, design changes, ideas change, players change, the single most important quality of code is its ability to be changed without breaking, without effort.

Also, really, if we can change code easily, we have all that we need. A junior programmer made a mess in our camera system near the deadline of our game? No problem, for the next title we can rewrite it! We have a crash that we don't have any clue about? Well, maybe we can rip out some systems, one by one, and find out the culprit. Designers changed their mind on the control system? Well, we might write some prototypes with them at their desk.

The more our language and project is easy to change, the less time and less obstacles it poses to change, the more all the other problems fade in the background. It's not a coincidence that the more a language is dynamic, the less need there is for fancy debugging tools. Who needs a watch window indeed, if I can just add on the fly a widget on screen that graphs the value of a given variable?

Unfortunately though, when you look at our beloved (not!) C++, there is little to be happy about. C++ as a medium for our art looks more like statuary marble than modeling clay: it requires a team of muscular stone cutters under the guidance of a genius in order to produce some amazing sculptures. And once a given idea has been translated into the stone, you hardy can change it without re-sculpting most of it or having to work in the constraints that the shape it currently has impose on you.

C++ is not only static typed but also statically linked and its performance depends on that. You don't get any fancy JIT or runtime optimization, but not even a standard way of loading modules or functions (even if to be fair, 99% of the systems you work with will have some non-standard functionality to achieve some sort of dynamic linking). Not that I'm advocating dynamic typing here, that's another can of worms (and you might even argue that types are not really the best form of static checking, and C++ classes are surely not the best expression of user defined types, I digress...), but static linking surely give us less options.

So what we can do? Well, the art of sculpting in C++, the art of design, becomes really the art of subdividing our sculpture in pieces, the real design challenge is all about how to cut or work into pieces. Too many of them and the sculpture will be a fragile and ugly mess (translation: craptastically slow OOP shit). Too little and we loose flexibility. 
Also, we would of course love if each piece was connected to as few other pieces as possible, otherwise replacing it would be a pain, and that the connections themselves to be as simple as possible.

How do we achieve that? Well, it can be tricky because dependencies are in many ways evil, but they are also the most innocent looking features of any languages, devils in disguise. Structured programming is all about composition (even disregarding the excesses of OOP)!

If you see these... think twice.

I originally thought this as some "coding flashcards" for a course at my former workplace but project deadlines had priority and I didn't end up finishing it. So when I started the collaborative guidelines experiment I wrote them in a similar "visual" style: if you see this code, then your spider-coding-senses should tingle. 

Great, experienced coders develop an instinct, an aesthetic if you want, something really subconscious about good structure and good looking code that is often difficult to formalize, is some sort of gut feeling that comes from having read a lot of code and knowing the perils of some structures.

So here it is. This is the part that really matters of these visual coding guidelines: the global-disaster causing wall of shame.

You see: Foo.h (header file)
You think:
  • Is everything in this header really meant to be seen outside?
    • Can we make more things part of the implementation?
    • I.E. Look for private functions that could be outside of the class.
  • Is everything in this header really meant to be seen by every other module depending on this one?
    • Can we more move things into a header that is seen by this module only (i.e. a good schema would be to have local includes in a module_name/source directory and the outside visible ones in a module_name/include/module_name one)
  • Should we consider using PIMPL (Façade if you like changing names of existing techniques)? Or abstract interfaces?
  • Are we exporting concrete types, or contracts and functions?
    • If we are exporting concrete types, are they fundamental enough? If we are exporting interfaces, do we really need them or are we over-abstracting, over-generalizing?
      • Remember: don't generalize something if you don't already have two/three different usages of a given thing. Don't abstract anything if you don't have two/three different dependencies on a given thing. Code should change to adapt to situations, not try to predict them.
You see: #include (especially in an header)
You think:
  • Do we really need the include or we can use forward declaration?
  • Can we include that file or are we introducing a dependency that we don’t want across two modules of our system? (even better, make sure that this can't happen by structuring your project in a way that to access a given module from another one, the project properties have to be changed, and make sure you have all these changes approved by a technical director)
    • Is the dependency static (function linking / inline functions / templates / types) or dynamic (interface calls / function pointers)?
    • Which one makes most sense here? Do we need performance or abstraction?
      • You will usually want to statically depend on core system libraries (memory, math...) and dynamically from other modules.
You see: a_type* Foo(...) or a_type& Foo(...) (interface returning a pointer, or returning a structure containing pointers)
You think: 
  • Who will own that memory? Who will destroy it? When?
    • What if we want to hot-swap resources?
    • Better use a handle? Reference counted?
  • Should we be allowed to mutate it (non-const)?
    • And who else will mutate it?
  • Is the pointer type the least derived (if it's part of a type hierarchy) possible?
You see: class Foo : public Bar
You think: 
  • Are we inheriting from an interface (pure abstract class)? Are we generalizing needlessly? Is the interface as small as possible? Then go ahead.
  • Otherwise, the class has to have some data associated with it. Are we sure we want to link Foo both to the interface of Bar and to its in memory layout? 
    • No, you are not. Don't do it, use composition instead. [More Effective C++, 33 Make non-leaf classes abstract]
You see: Class Foo{ void MyFunction(…) }
You think:
  • Can it be implemented as an external function instead? 
    • Especially if it's private, should it be visible to anyone that can see the header? Prefer implementation-only functions to private member functions.
  • Should MyFunction be const?
  • Has it really to be part of the class or should it be an external utility function?
    • Is it needed to make the class complete and minimal?
  • Should it be declared const?
You see: static
You think:
  • Can we safely have multiple instances of this static (i.e. if we link this statically to multiple dynamically-linked modules), or it will be a problem with DLLs?
  • Can we safely access this static from multiple threads, will that be needed?
Conclusion
This is it for now. Look at the typewithme pad above to see the full coding guidelines. I'm sure I forgot other things and I will probably edit this post over and over. If you want, comment as usual or better, join the discussion on the typewithme shared document!

p.s. There is of course quite some discussion in the comments and actually something persuaded me that I should clarify here. A point someone raised is basically: "you seem to hate C++ but still in the end you say things about decoupling that will apply to many other languages (to some degree - I would add). 

It is 100% true and right. That is what I felt and what I probably failed to clearly express. I started this "collaborative guidelines" experiment, I got some great input there and it was all nice and exciting. But then I started thinking that yes, these are the landmines we all know and have to avoid, there are books written on them and everyone really agrees. 

Yes, there might be people arguing on the details (Boost is good or evill? Design patterns? and so on), surely everyone comes up with their own implementation of the same extensions (reference counting, serialization and on and on) and that sucks but still the bottom line is, everyone knows that the default new/delete operators are useless, everyone knows that "friend" is bad and so on. 
There are things to avoid always, things to mostly avoid and things to remember because of bad defaults (i.e. "explicit" or "virtual ~Foo", redefining equality operators or hiding them and so on) and probably even many things that everyone forgets until they hit them, but at the end of the day... they don't matter much.

In all my years as a professional programmer I've learned that. You can work with the worst code and language and tools on earth, if the problems are "local", they are hidden behind well designed abstractions, then all the implementation behind them does not really matter. Or it matters, yes, but at a totally different order of magnitude.


So yes, I still hate C++. But we can work with it. If we pay attention to the only thing that really matters, the decoupling, then life will be fine. And slowly, we will even be able to move stuff over other languages, if it's all nicely decouple and the interfaces are sane. And believe it or not, it's already happening.


Now go, look at your project and try to do an experiment. Take a small piece that has defined inputs and outputs, with some coworkers we tried once with our camera library. Can you take it apart and put it in a DLL? Or rewrite it in another language? If yes, then sweet dreams, your project is nice. If not, then you should think, maybe you have a problem...

08 February, 2011

Gamma and diffuse shading

Remember than (tex^a * shading)^b = tex * shading^b if a=1/b... (cheap diffuse-only stuff, i.e. particles, vegetation... of course this assuming software gamma in the shader, that will give you no gamma-correct blending)