C++ Guidelines – Made-to-Measure vs One-Size-Fits-All

 
Author:  Follow: TwitterFacebook
Job Title:Sarcastic Architect
Hobbies:Thinking Aloud, Arguing with Managers, Annoying HRs,
Calling a Spade a Spade, Keeping Tongue in Cheek
 
 
One-Size-Fits-All vs Made-to-Measure

#DDMoG, Vol. V
[[This is Chapter 16(c) from “beta” Volume V of the upcoming book “Development&Deployment of Multiplayer Online Games”, which is currently being beta-tested. Beta-testing is intended to improve the quality of the book, and provides free e-copy of the “release” book to those who help with improving; for further details see “Book Beta Testing“. All the content published during Beta Testing, is subject to change before the book is published.

To navigate through the book, you may want to use Development&Deployment of MOG: Table of Contents.]]

Each project with more than one single developer inevitably has its own guidelines, and C++ projects are not an exception. These guidelines can be formal, or can be informal, this is not that important for a small project, though when you have more than 3 people on your team, some level of formalization (even if it is as little formalization as “we have a 20-line file in git which describes our current conventions“) is usually a Good Thing™.

On One-Size-Fits-All Guidelines

Everybody should have their own philosophy,

tailored, just like pants, by an individual measurement.

— Stanislaw Lem, Cyberiad —

When facing the task of defining guidelines for your project, it is always tempting just to say “hey, we’re using these {[Google]|[Core]|[JSF]|whatever-else} guidelines, THOSE BIG GUYS CERTAINLY KNOW WHAT THEY’RE DOING” – and to save yourself quite a bit of thinking. Unfortunately, this is rarely a good idea 🙁 . In practice, while those guys indeed DO know what they’re doing,1 they DON’T know about your project and its specific needs.

Hare with hopeless face:As soon as you have some Big Name Guideline, you will have quite a bit of developers taking The Guideline as a gospelTo make things worse, as soon as you have some Big Name Guideline, you will have quite a bit of developers taking The Guideline as a gospel,2 and making lots of noise about a (usually very small) thing which is completely irrelevant in your context (and of course, these nitpicking developers will ignore all the explicit disclaimers such as “[The rules] are not meant to define a single “one true C++” language” [Core]).

To make things even further worse, these Big Name guidelines are usually NOT just “Big Name” ones, they’re also Big In Size, which means that quite a few of your developers will fall victims of “not seeing the forest behind the trees” syndrome.

As a result, I’ve seen those one-size-fits-all guidelines often becoming detrimental to development process 🙁 . Does it mean that I’m against guidelines at all? Certainly I’m not. Does it mean that I’m against the advice given in these guidelines? Most of the time, I’m not either. However,

I DO insist that each and every project DOES need its own guidelines. Moreover, commonly different sub-projects need their own sets of guidelines, often differing from the project-wide ones.

My position on the guidelines can be summarized as follows:

  • DO have your own set of guidelines
  • DO make sure they’re YOUR OWN guidelines, which you (a team as a whole) LIKE to follow (or at least agree they are necessary), and that you DO know WHY you have each of the guidelines on the list
    • YOUR OWN guidelines MAY (and often SHOULD) come from those Big Lists, however, you SHOULD think and Really Understand each one before adding it to YOUR OWN list
  • Arguing hare:DO start small, and add more guidelines laterDO start small, and add more guidelines later
    • One approach which tends to work well when you start a new project from scratch, is to start from just naming conventions, but to include a guideline whenever you see something-you-don’t-like in the project code.3 Then this something-you-don’t-like needs to be discussed (or, depending on the chain of command, elevated to the architect), and a guideline can be established.
  • DO have different guidelines for different parts of your project. In particular, in the context of our Reactor-fest Architecture, we’ll need at least TWO different sets of guidelines: one for infrastructure-level code, and another one – for Reactors themselves (i.e. for game logic). We’ll discuss subproject-specific guidelines a bit later.

1 To be entirely honest, I need to add “most of the time” here
2 This is especially typical for not-so-good developers who want to establish their importance. These are usually the very same guys who can quote the most obscure paragraphs from the standard by heart while being unable to write anything half-meaningful themselves.
3 of course, it will work only if your project has a culture of looking into the code written by other developers – but you SHOULD have such a culture regardless of guidelines

 

Popular Sets of C++ Guidelines

There are quite a few popular sets of guidelines out there; while I do NOT want to list all of them, I will list some IMO the most popular ones, and will provide some comments on the significant points which I disagree with (disclaimer: this list of disagreements is NOT meant to be exhaustive). Mind you, I DO agree with MOST of the stuff these guys say, but well, I DO have my own opinion (sometimes a Very Strong One) on certain points.

  • Surprised hare:Core is edited by Bjarne Stroustrup and Herb Sutter, which automatically makes it a 'benchmark' for all the C++ Guidelines[Core]. Edited by Bjarne Stroustrup and Herb Sutter, which automatically makes it a “benchmark” for all the C++ Guidelines. As of beginning of 2016, the project is in Very Beta stage, and I hope that most of my issues with it will be ironed out before it is accepted as a Ready-to-Use Set of C++ Guidelines. My personal most significant disagreements with [Core]:
    • I am still reasonably sceptical about the line of reasoning of “there will be smart tools which will enforce everything for us” – that is, until I see these tools working seamlessly in ALL development environments our team is using – and this IS going to take a while (in the worst case – up to “forever”). That being said, these guys DO have quite a bit of weight (to put it mildly ;-)), so they MAY be able push these tools through. Ideally I’d like to see these tools as a Highly Configurable compiler plugin (notice the Highly Configurable part).
      • Note that “Highly Configurable” part is Really Important. To ensure configurability, proposed [[suppress(tag)]] needs to be defined better (what is exactly the scope of [[suppress]]? Current compilation unit? Something else?)
        • In addition, I would certainly appreciate ability to add my own project-wide (or subproject-wide) rules. Even simple ones such as “we do NOT use dynamic_cast<>”, “we do NOT use specific-function”, and “here is the list of ALL the library functions we’re allowed to use” (with an emphasis on “We”), would be of Really Great Value.
      • Probably because of this “smart tools will do everything for us”, guidelines in [Core] are NOT separated based on their importance. I think it is a Pretty Bad Thing :-(: for example, I see having uninitialised variables as a MUCH more mortal sin than declaring variable before initialising it, so I do NOT like having them next to each other.
    • Hare thumb down:there are some attempts to impose completely arbitrary (='completely unmotivated') guidelinesIn spite of declaring that “They are not meant to define a single “one true C++” language.”, there are some attempts to impose completely arbitrary (=”completely unmotivated”) guidelines. The most egregious example of such completely arbitrary guidelines is NL.17 “Use K&R-derived layout” with “Reason” cited as “This is the original C and C++ layout.” (plus a bunch of other stuff which applies to several dozens of other popular layouts). I Really Hope that this guideline will be gone from [Core] before it is released.
    • “Use libraries wherever possible” is the same thing as saying “Use hammer whenever possible”, instantly leading to seeing the whole world as a bunch of nails. The whole issue of DIY vs Reuse is MUCH more complicated than such a blanket statement (in this book, I’ve dedicated the whole Chapter IV to this issue).
    • Rather minor thing, but I DO see value in camelCase (which BTW seems to be passionately disliked at least by some of the authors) – exactly because it is different from std:: stuff (see [[TODO]] section below for the rationale which goes beyond personal preferences).
      • BTW, while we’re at it – I do NOT think that personal preferences should be allowed to make it into a document positioned as-much-universally as [Core] one.
    • Note that it is Huge and the number of guidelines is in hundreds, so those 5 or so disagreements above do NOT indicate that I feel that [Core] is “bad”; on the contrary, it has LOTS of useful information (and up-to-date-for-C++11-and-above-too).
  • [Google]. Overall, I like MOST of the stuff within [Google].
    • Hare wondering if you are crazy:One my disagreement with Google is Google's dislike of exceptions.One exception (pun intended) is Google’s dislike of exceptions. Here I firmly stand on the [Core] positions, saying that exceptions are MUCH BETTER than error codes (at least for frequently changed app-level code).
    • Another disagreement (and a Very Strong One) is related to [Google]‘s recommendation to use << for formatting purposes. See [[TODO]] section below on it (to save your from foul language, I do NOT want to repeat those words about << more than once, but I can repeat my recommendation to use [fmtlib] instead).
    • Last but not least, I disagree with a requirement (at least [Google] can be read this way) to have a comment for each and every function. Requiring per-class comments are fine (though examples given in [Google] are waaaaaaaaay toooooooo vvvveeeerrrrbbboooossssseee) but requiring per-function comments easily leads to nonsense such as a comment “returns X” for a function “int getX()”. As discussed in [[TODO]] section below, such comments only IMPEDE readability instead of improving it.
  • [C++FAQ]. Not exactly positioned as a set of guidelines, C++ FAQ has quite a bit of discussion about
    • My biggest grief about [C++FAQ] is their suggestion to use those << operators for formatting. Once again, I cannot disagree in strong enough words here, see [[TODO]] section below for discussion on <<.
  • [JSF]. It was a reasonably good set of guidelines for its intended purpose, but as of now it is (a) outdated (there is no C++11 at least in those versions of [JSF] which I’ve managed to get), (b) way-too-heavy for everyday game- and business-app use (the one which is NOT life-and-death), (c) described in waaay too formal terms for my taste.

My Own One-Size-Fits-All Set

Sarcastic hare:after bashing all those Big Name guys for making those over-arching sets of guidelines, it is perfectly logical for me myself to do the same 😉 Ok, after bashing all those Big Name guys for making those over-arching sets of guidelines, it is perfectly logical for me myself to do the same 😉 . On the positive side, at least I will try to be short.

Principles

  • Make sure to take EVERY THIRD-PARTY GUIDELINE with a good pinch of salt
    • However, as soon as you agreed on a guideline for your project (and it became YOUR OWN GUIDELINE rather than a 3rd-party one) – it MUST be followed meticulously, and EACH AND EVERY deviation MUST be fixed sooner rather than later (and if you need an exception to a guideline – you MUST write the exception into the guideline itself).
  • Readability trumps everything else until proven otherwise.
    • Yes, it MIGHT be necessary to have barely readable highly optimized code – but ONLY AFTER it has been demonstrated to be a bottleneck
    • Overall, readability is closely related to the number-of-things-we-need-to-handle-in-one-single-place. Exceeding cognitive limits (also known as “7+-2” rule) is a Really Bad Thing (and everything which pushes us closer to the limit without Good Reason, is a Bad Thing too).
  • Non-enforceable rules are generally worse than no rules at all
    • Along the same lines, if we cannot enforce something – it is better to leave it as an honest comment rather than to pretend that it has some meaning by using a construct which looks as if it is more than a simple comment (yes, this DOES mean that I do NOT like owner<T*> defined as T* – at least until I have a tool to enforce it, preferring more honest /* owner */ T* instead; no, this argument does NOT apply to std::unique_ptr<T> which has enforceable semantics which is very different from simple T*).
  • There are rules/guidelines which exist only for the sake of consistency. While such guidelines are necessary, IMNSHO it is perfectly fine to have them on a per-project basis.
  • Didn’t I forget to tell “Take every 3rd-party guideline (the ones in this book included) with a Big Pinch of Salt?”

Enforcing Your Own Guidelines

You can write about it, or talk about it, as much as you want.

But without code enforcement, all that talking is just not going to work

— Chris Butcher, Engineering Director, Bungie —

No guideline is good unless it is somehow enforced. In this regard, the following things tend to help:

  • Hare thumb up:DO have YOUR OWN guidelines, those which you DO want to have. This way they're MUCH easier to enforceDO have YOUR OWN guidelines, those which you DO want to have. This way they’re MUCH easier to enforce (opposite to enforcing-somebody-else’s-stuff which you don’t understand and don’t care about).
  • DO have a policy that ANY violation of the guidelines which anybody notices, one of the following MUST happen:
    • the violation fixed, or
    • the guideline amended (including, but not limited to, adding an exception), or
    • an issue opened
  • DO use tools where applicable. On the other hand, DON’T allow tools to dictate the guidelines for your project. It is YOUR decision, YOUR responsibility, and it is YOU (and certainly NOT authors of the tool) who will be beaten hard if your project doesn’t work due to bad guidelines.
  • DO have your own project-wide header (or several ones for different purposes), and DO prohibit all the 3rd-party #includes except for these few headers for the rest of your project.
    • In the long run, it will save you quite a bit of time on dealing with sneaky non-authorized functions to be used. Also, while not 100%-efficient, this policy also tends to help against non-cross-platform functions within your cross-platform project.
    • BTW, such seemingly indiscriminate use of headers will NOT (as a rule of thumb) make your project compile slower. On the contrary, if you use precompiled headers, it will make your code compile faster
    • In addition to limiting #includes to your-project-headers, I also suggest to limit using directives to your-project-headers too. In other words, I suggest to adopt a guideline when all the using namespace std; SHOULD belong to your-project-wide-headers, and ONLY to your-project-wide-headers.
      • Surprised hare:whether we like it or not, each project effectively creates its own dialect of the languageRationale: whether we like it or not, each project effectively creates its own dialect of the language, and such universal headers tend to ensure the consistency of this dialect across the whole project, which is beneficial (in particular, for readability reasons). Usually, unique_ptr<> within a project doesn’t really mean “an instance of std::unique_ptr<>”, but instead has an idiomatic meaning of “that damn unique_ptr<> thing which everybody knows about”. And having such idioms defined at project level is generally a Good Thing™.
      • In addition, this kind of policy allows to play interesting (and useful) tricks. In one example, if your project-wide-headers were saying using std, and your project code was using standard shared_ptr<> but spelled as simple shared_ptr<> (relying on using std::shared_ptr<> within the project-wide header); then, if your code is inherently single-threaded (like ALL the code within Reactors is), at some later point you could create your own (single-threaded and therefore slightly more efficient, but having exactly the same semantics) version of our_own::shared_ptr<>. It will allow you to play some games with using directive within our project-wide-headers, to make sure that all the instances of shared_ptr<> within the project actually become instances of our_own::shared_ptr<> – all without ANY changes to the code-outside-of-project-wide-headers, and saving you a bit of CPU clocks (that’s without changing anything within the game-level or business-level logic).

Naming Conventions and Indentation Style

With naming conventions, for quite a long time the consensus revolved around the following:

  • You do need a naming convention and common indentation style
  • It does not really matter which naming convention or indentation style you use

Judging hare:from my perspective, any reasonable layout is good enough.However, with [Core] appearing, they seem to advise4 on one very specific layout (see “NL17. Use K&R-derived layout”), with a wording such as “Note a space between if and (“ (ouch!). In short – I have a very strong disagreement with this recommendation being a part of “guidelines-for-everybody”; in particular, am not buying the “Reason” of it being “the original C and C++ layout”; moreover, from my perspective, any reasonable layout is good enough.

As a result, my personal recommendation goes along the following lines:

  • DO have some reasonable naming convention
    • Side note re. naming convention: there MAY be some value in having your own code to have naming convention which is different from the standard one. In particular, it will show that a certain function call is obviously yours (and therefore is a much more likely suspect of misbehaving than a standard one56).
      • In the context of C++ (which has standard library functions with underscores), this opens a door to the UpperCamelCase or lowerCamelCase convention.
    • While we’re at it: I DO agree with [Core] that Hungarian notation is to be avoided (on the basis of worse readability). I also agree that prefixes for members (such as m_) are fine.
  • DO have some reasonable indentation style
    • There is a reasonably-good way to enforce it, via
      • (a) agreeing on using one of {[astyle]|[clang-format]|[uncrustify]}, and on set of command-line options for it
      • Hare pointing out:There is a reasonably-good way to enforce indentation style, via running astyle (with agreed options) before each commit(b) running the tool-you-agreed-on (with agreed options) before each commit (can be done manually, or even as a git hook)
      • I’ve done it myself, and DO prefer it over manual styling. In particular, this approach saves lots of diffs going back and forth when two developers have their own interpretations of style, and (almost subconsciously) are adapting the style to fit their interpretation, inserting or removing spaces, which makes difference reviews a nightmare 🙁 .

4 as noted above, I Really Hope this guideline will be gone when [Core] goes out of the draft
5 While the most-difficult-to-find-bugs tend to belong to standard libraries (all five of the most-difficult-to-identify bugs in my career were the 3rd-party ones from supposedly trustable sources, including several rather elusive ones in several different implementations of STL [TODO: C++R’98]), I have to admit that 99.(9)% of the bugs belong to our own code.
6 Yes, this is discriminating functions based on their names and/or origin; no, it does not qualify as “racial profiling”

 

Dealing with C Legacy and Correctness

A few things which you SHOULD NOT do (as a rule of thumb, even if you think you have a Really Good Reason to do it – while exceptions are possible, they’re soooo few and far between, that making a special exception for each one in your guidelines is justified):

  • DON’T use pointers when you mean an array. Use something like gsl::span<> from [Core] instead
    • Besides handling span<>, all pointer arithmetic SHOULD be outright prohibited
  • Assertive hare:DON’T use C-style cast, EVER. If you DO need a cast – DO spend time on figuring out which of C++ *_cast<>s you really mean (and maybe, you’ll find a way to avoid that cast at all)DON’T use C-style cast, EVER. If you DO need a cast (which you normally shouldn’t BTW) – DO spend time on figuring out which of C++ *_cast<>s you really mean (and maybe, you’ll find a way to avoid that cast at all)
    • DON’T cast any non-pointer types to pointers and vice versa
    • DO try to avoid casts altogether
  • DON’T use unions without proper correctness-ensuring wrapping. Yes, this applies to C++11 unions too.
  • DON’T allow uninitialized (garbage) variables in your program. It is a bad practice overall and it hurts determinism (which we’ve discussed in Chapter V) too.
  • Not really dealing with C legacy, but… DO use RAII.
    • Moreover, ALL the resources should use RAII. Among other things, this ensures basic exception safety.
      • In particular, “X* x = new X();” SHOULD be prohibited (in favor of std::unique_ptr<>).

General C++

  • Encapsulation is Good, coupling is Bad. DO hide implementation details wherever you can. It will save a LOT of time understanding and debugging code (and will help to introduce some optimizations later too).
  • If in your class you have any of (desctructor, copy or move constructor, [move] assignment operator) – most likely you need all five
    • For assignment operators – make sure that you handle self-assignment too
  • If you have any virtual function – most likely you need a virtual destructor too (even if empty)
  • Assertive hare:DON’T use global/static variables (constants are fine)DON’T use global/static variables (constants are fine). Using globals/statics/TLS makes your code convoluted, and has quite a few other problems too. As [C++FAQ] puts it: “the names of global variables should start with //” 😉 .
    • DON’T use singletons either (and on the same grounds too).
  • DO try to use “pure” functions (those ones which DON’T depend on anything but their own arguments, and DON’T have any side effects too) as much as possible. Without globals, it is not THAT difficult BTW.
    • While you’re at it, DO mark such functions as constexpr wherever your-oldest-compiler is happy with it. NB: C++14 is MUCH more lenient with regards to constexpr; as of beginning of 2016, such leniency (known as “extended constexpr” a.k.a. N3652, is supported in GCC v5+ and in Clang 3.4+, but is NOT supported in MSVC15; but by the time when you read this book, things MIGHT change7).
      • DON’T go into recursive trickery just to make your function constexpr in C++11 style, unless absolutely necessary. Wait for C++14 instead 🙁 .
    • DO use exceptions where applicable. If you have concerns about performance – you can safely throw these concerns away as long as the exception is not thrown (see discussion in [[TODO]] section above).

7 [C++1xFeatureSupport] usually has reasonably up-to-date information with regards to support of C++14 features by different compilers

 

Self-Documenting Code

Assumption is a mother of all {disasters|screw-ups|…}

— attributions vary —

I strongly prefer self-documenting code to comments. From my perspective,

self-documenting code is a code which not only states assumptions about the code, but also enforces them (if possible – in compile time, if not possible – in runtime).

It is this enforcing assumptions which gives self-documenting code a Big Fat Advantage over comments. One particularly nasty scenario when it comes to comments, is that comments become outdated. Whenever I’m looking at the comment – I cannot be sure that the comment is still valid.8 OTOH, when I’m looking at static_assert() – I can be quite sure that the condition within does stand; with runtime assert() things are a tiny bit more murky, but assuming that the code is routinely run/debugged in debug mode – they tend to enforce assumptions very well too.

Here go a few guidelines for self-documenting code (all these guidelines are enforcing too(!)):

  • Judging hare:Yes, I know that LOTS of developers out there think of const as of a nuisance, but I repeat: DO use const whenever applicableDO use const. Yes, I know that LOTS of developers out there think of const as of a nuisance, but I repeat: DO use const whenever applicable. Rationales for it are abound, google for them yourself if you have any doubts…
    • This also applies to declaring your member functions as const if they don’t modify this.
    • DO use constexpr for those things which you expect to be computed during compile-time.
  • DO use assert()s
    • In particular, DO write pre-conditions, post-conditions and invariants
      • Whether to use assert() (or more specialized things like Expects() and Ensures() from [Core]) to implement pre- and post-conditions – is up to you
    • DO use static_assert()s. Prefer static_assert() over assert() wherever possible
  • DO use override modifier

8 actually, I cannot even be sure that the comment was EVER valid

 

On Comments

As I’ve stated above, I strongly prefer self-documenting code to comments; that is, whenever it is possible to have enforceable self-documenting code. The next best thing is to have identifiers (especially function names) meaningful. And only then, comments start to play their part.

BTW, I am firmly against policies such as “you MUST have a comment for each and every function explaining what it does” – it inevitably leads to atrociously redundant things such as

//function int getX()
//obtains x
int getX() { return x; }

Hare with omg face:Even worse than simply being redundant, such code becomes cluttered to the point of being utterly unreadable, very quickly.Even worse than simply being redundant, such code becomes cluttered to the point of being utterly unreadable, very quickly 🙁 .

On the other hand, I am NOT the one to say “throw away ALL your comments”; what I am saying is just “whatever you can enforce OR you can communicate by meaningful identifiers, is better to be done this way”. For the rest – well, we still need to use comments 🙁 .

On Error Handling

Error handling is traditionally a major source of problems. I suggest the following approach (NB: this applies only to business apps, games included; for other domains optimum solutions can be different):

  • DO use exceptions for error handling
  • DO use RAII, so that you have at least basic exception safety
  • Whether to go for strong exception safety is up to you. However, unlike [Alexandrescu2000], I DO consider the argument “if we fail to allocate 20 bytes, we’re long dead anyway” as a very valid one for games and business apps.9
    • As a result, I won’t jump too high if you don’t make your game formally strong exception-safe in case of allocating of maximum 100-200 bytes (or “pretty much any constant-size struct” for that matter). Allocating variable buffers, especially those which can reach multiple megabytes, however, is a Very Different Story, as these things DO happen in practice (especially if the length came over the network(!)).

9 while I do agree with the authors  that this argument is not scientific, I myself, being not a PhD and merely an architect with 25+ years of real-world C++ experience, can afford to ignore scientific arguments about being non-scientific.

 

On Cross-Platform C++

To make sure that you don’t fall a victim of the vendor lock-in, I suggest to adopt the following guidelines:

  • DON’T use platform-specific code, EVER. An exception to this guideline MAY be granted for specific sub-projects, but by default you SHOULD keep your code free from the platform specifics.
    • Hare with hopeless face:It is a Really Bad Idea to have your game/business logic intermingled with platform-specific stuff.Yes, I know that it might sound as The Ultimate Heresy for about a half developers out there. Still, DON’T. It is a Really Bad Idea to have your game/business logic (which is inherently cross-platform, see Chapter I) intermingled with platform-specific stuff. At the very least, such logic-interspersed-with-platform-specifics:
      • pushes your cognitive limits towards that magic 7+-2 number 🙁
      • decreases chances to port to another platform, manyfold 🙁 🙁
  • DON’T use C++14 yet (at least those features which are NOT supported by ALL of your target compilers). Which as of beginning of 2016, translates into “if you’re unlucky to use MSVC for development – you should forget about most of C++14 features for the time being” 🙁 . [C++1xFeatureSupport] usually has quite an up-to-date picture for current state of feature support.
  • DO compile-and-run your app on at least two significantly different platforms, as soon as possible (at most – within the first few months of development). As soon as you have your app running on two significantly different platforms – the third one will go MUCH easier.
    • Note that for the purposes of this guideline, Win32 and Win64 DON’T qualify as significantly different (neither are pairs of Win64 and WinCE, Linux and MacOS X, and MacOS X and iOS). However, the pairs such as Win64 and MacOS, Linux and Windows, and iOS and Android, are different enough.

C++11

C++11 brings quite a few tricks to the table of a programmer. Some of them are quite important to be separately mentioned (especially for those of us who remember ye good ol’ days of C++98 and C++03):

  • DO define move constructors / move assignments
    • DO declare them (as well as swap()) as noexcept
  • DO use range-based for (as in for( auto& x: vec) { total += x; }).
  • DO use std::tuple() for return values.
    • If you did your job with regards to move constructors, it won’t cause any performance penalty, and will make the code much more obvious than pre-C++11-era pass-via-reference.
  • DO use auto, though DON’T overdo it (it is good only as long as it improves readability and doesn’t hurt it)
  • DO use enum class, most of the time it is better than old-school enum
  • If using lambdas, do it SPARINGLY. Lambdas do have their use cases, but they tend to be overused these days. In particular, multi-line lambdas MAY reduce readability in quite a few cases.10
    • Hare asking question:When dealing with lambdas, make sure to learn about capture modes – they are quite tricky.When dealing with lambdas, make sure to learn about capture modes – they are quite tricky.
      • In particular, make sure that you do understand that capturing this by value does NOT capture members-accessed-via-this by value (in other words, members are always captured by reference(!)11).
    • DO use std::unique_ptr<> for RAII (at least at those points where you’re about to use allocation anyway); DO NOT use std::auto_ptr<> (which you didn’t want to use even before C++11)
      • DON’T pass smart pointers to those functions which has nothing to do with ownership; pass your usual C-style naked pointers instead
      • Use std::shared_ptr<> ONLY if std::unique_ptr<> is not sufficient for your purposes.
        • When using std::shared_ptr<>, beware of loops! C++ has no garbage collection, and std::shared_ptr<> is based on a simple reference count, so loops consisting of shared_ptr<>s, will NOT be freed L.
    • DO use “=delete” to disallow certain default functions (instead of previous practice of declaring-private-and-never-implement)
    • DO use three things which were mentioned above: constexpr, override and static_assert().
    • There are lots of other improvements in C++11; however, they’re usually not that visible, so to keep this list out of danger to be TL;DR’ed, I’m stopping here.

10 There are obviously exceptions too; see, for example, Chapter V with quite a few multi-line lambdas used to IMPROVE readability
11 C++17 introduces *this capture, which allows to capture members by value

 

On Libraries

For libraries, I tend to have a Very Harsh Approach, which says

Even if a library is standard, it doesn’t necessarily mean that you can use it.

In particular, I am usually discouraging the use of the following parts of std:: library:

  • DON’T use thread-related stuff (unless explicitly allowed for a sub-project).
    • While thread-related things do have their uses for infrastructure-level code, using them at a game-logic or a business-logic level is looking for a mortgage-crizis size disaster (see, for example, reasoning in [[TODO:nobugs]]).
  • Disgusted hare:<< for formatting stinks so badly, that if you write it in UK, developers in Australia can still smell itDON’T use that used-in-each-and-every-C++-tutorial << for text formatting purposes. This one is admittedly my personal pet peeve, but I should say that I dislike it that much, that I was guilty of using type-unsafe printf() instead for quite a long time. Of course, lots of people out there will bash me for this heresy, but I still stand by my claim that << for formatting stinks so badly, that if you write it in UK, developers in Australia can still smell it.
    • My main problem with << is about it being utterly and completely unreadable. If you have any doubts, compare printf(“0x%08x\n”, i); to std::cout << “0x” << std::hex << std::setfill(‘0’) << std::setw(8) << i << std::endl;
      • And we even did NOT try to restore those formatting flags which we’ve set here (which will make the code Even More Ugly(!!), not to mention that these flags represent a completely unnecessary state, which is a Bad Thing per se)
    • However, to make things even worse (as if it is possible), << is also completely unsuitable for i18n, specifically for translation. We’ll discuss i18n in more detail in Chapter [[TODO]], but let’s note for now that:
      • For i18n, you need full sentences, and NOT just single words/parts
      • For i18n, you need positioned arguments (as order of parameters can easily be different in different languages)
      • Constructs based on operator <<, fail badly on both points above
    • Out of all the pro-<< arguments brought forward by [C++FAQ] (specifically, type safety, being less-error prone, extensibility, and inheritability) I am buying only one and a half: (a) type safety and (b) to a certain extent, extensibility.
      • Hare thumb up:Fortunately enough, it IS apparently possible to have the best of both worlds, and to combine readability and i18-ability of printf() with type safety and extensibility of <<Fortunately enough, it IS apparently possible to have the best of both worlds, and to combine readability and i18-ability of printf() with type safety and extensibility of <<. In short: use [fmtlib] a.k.a. {fmt} (former cppformat). Oh, and it appears to be faster-than-everything-else-except-for-printf() (and even compared to printf() they go neck-and-neck). And BTW, and {fmt} works seamlessly over either FILE*, or over std::ostream too (and you can throw in your own streams if you feel like it) – technically, it means {fmt} can be also made inheritable (not that it matters IMO).
    • BTW, with all my dislike to <<, I do NOT feel that strongly about std::istream/std::ostream themselves (that is, if we throw away <<-based formatting, and use only read()/write() functions).
  • Some of C standard library functions are Really Ugly 🙁 . In particular, DON’T touch anything which returns pointer to a static (even if it is actually TLS i.e. thread safe). These include strtok(), asctime(), and so on… For most of these functions, currently there are better alternatives within the same C standard library.

Phew. Most likely (in addition to the intentional omitting quite a few most obvious and commonly accepted guidelines) I’ve forgotten quite a few important ones, [[SO IF YOU SEE SOMETHING MISSING – PLS GIVE ME A SHOUT!]]

Sub-Project Guidelines

Femida hare:As I’ve already noted above, I am a strong proponent of “horses for courses” principleAs I’ve already noted above, I am a strong proponent of “horses for courses” principle, which in terms of guidelines, is translated into “guidelines for projects and for subprojects”.

As we’ve discussed in Chapter V, I tend to promote Reactor-fest architectural style, where everything is a Reactor. Ok, actually, not really everything is a Reactor, but just all business/app logic sits within Reactors (and the rest is logic-independent infrastructure code).

With this in mind (and as it was described in more detail in Chapter V), we have two quite different sub-projects: (a) logic within Reactors, and (b) infrastructure code supporting Reactors. Let’s take a look how we need to amend generic guidelines above, for these subprojects.

Infrastructure Code

For infrastructure-level code, most of the guidelines above stand, with the following amendments:

  • Thread-related libraries are allowed. Phew.
  • Moreover, system-specific code MAY be necessary. Ouch.
    • Alternatively, we MAY consider using library such as [libuv]. Being dedicated to non-blocking I/O, it is a natural fit to implement our non-blocking Reactors.

And that’s about it. All the other guidelines still apply to infrastructure code.

Reactor Code

Our Reactors (as described in Chapter V) have quite a few interesting properties. This, in turn, has some implications on the guidelines:

  • Not a change, but rather a re-iteration: still neither thread-related neither system-specific code within Reactors.
  • Hare with an idea:By their very nature of being incoming event processors, Reactors tend to have a quite well-defined semantics of “what to do in case of unexpected failure”.By their very nature of being incoming event processors (and yes, from this point of view classical game/simulation loop is just a processor of “rendering of last frame has completed” or “next network tick has arrived” event), Reactors tend to have a quite well-defined semantics of “what to do in case of unexpected failure”. In quite a few cases, we can just ignore the incoming event (such as network packet) which has caused the failure, logging the whole thing but surviving for the time being. This is especially true if the exception occurs within Validate or Calculate stages (i.e. when the Reactor state is guaranteed to be intact, see Chapter V for discussion of Validate-Calculate-Modify pattern).
    • As a result, I am arguing for replacing remaining-in-runtime asserts() (or equivalents) with exceptions
    • NB: this actually should be implemented by infrastructure-level code, but MAY need some understanding from Reactor-level developers too. Moreover, I am arguing for converting CPU exceptions (most popular ones being “reading nullptr” and “division by zero”) into C++ exceptions (such as with _set_se_translator() on Windows, and throwing exception from a signal on some *nix platforms), and handling them in the same manner. It has been seen to save the company hundreds of thousand dollars in downtime-costs-which-would-otherwise-happened. Unfortunately, only a very few *nixes (and last time I’ve checked, this didn’t include Linux) supported throwing C++ exception from the signal.
      • NB: of course, for Reactors running on those platforms which don’t support such throwing-C++-exception-from-signal, infrastructure code can resort to the good old setjmp()/longjmp(), but it has an obvious caveat. While setjmp()/longjmp() is MUCH better than nothing, it is MUCH worse than a way to convert CPU exception into C++ one (for setjmp()/longjmp() there is no stack unwinding, which means no release of RAII resources 🙁 ).
    • Reactors are also guaranteed to be at least “as if” they’re running within a single thread. This involves quite a few potential optimizations, including the following one:
      • DO consider using standard-std-containers-with-your-own-allocator. If we’re using our own allocator, we can (at some point in the future) make it thread-agnostic, avoiding those nasty LOCK-prefixed instructions,1213 and saving some time too
        • The same applies if you’re using 3rd-party STL such as EASTL (more on it in [[TODO]] section above)

12 While LOCK as such applies only to x86/x64, cost of atomics is significantly higher than an usual op, on all the multi-core/multi-CPU architectures
13 BTW, keep in mind that slowdowns from LOCK/atomics are MUCH more visible on multi-socket architectures, i.e. on servers

 

Summarizing Chapter 16

This concludes our Chapter XIII, dedicated to C++ in game context. Of course, it does NOT include everything you need to know about C++ (such discussion would take a few separate volumes by itself), but I hope that for an experienced C++ developer, it was still useful.

[[To Be Continued…

Tired hare:This concludes beta Chapter 16(c) from the upcoming book “Development and Deployment of Multiplayer Online Games (from social games to MMOFPS, with social games in between)”. Stay tuned!]]

Don't like this post? Comment↯ below. You do?! Please share: ...on LinkedIn...on Reddit...on Twitter...on Facebook

Acknowledgement

Cartoons by Sergey GordeevIRL from Gordeev Animation Graphics, Prague.

Join our mailing list:

Comments

  1. Wanderer says

    As always, you’ve got a really nice article summarizing common wisdom and different major opinions (Google, Core).

    There is one question that is usually left unmentioned by most of those style guides – packaging. Most of known guides (adopted to some extent in most of the teams I worked in) concentrate on code-block level (i.e. function, method) and class level. Of course, it’s the most important piece and when we are talking about small apps or some reusable libraries, this is clearly enough – you just put everything into a single lib/project and live happily.

    At the same time, when we come into a land of actual business apps (as “games” is clearly belong to this category) with many thousands lines of code involved, the packaging question becomes more important. How granular internal libs should be, how to make sure graph of internal libs is acyclic, i.e. it’s the next level of organization from code blocks to class, and from classes to libraries.

    Would love to hear your thoughts and best practices on this matter based on your experience.

    • "No Bugs" Hare says

      Finding interfaces and APIs which are “right for the job” is one of those things which I still cannot formalise :-(. At the moment, I consider it as one of those “black magic” (or “art”) things which distinguish good architect from a not-so-good one (probably THE most important such thing).

      The only thing which I can tell in this regard – is that I prefer to design things from top (“what we need to do?”) to bottom (“how we do it?”) – and implement from bottom to top. Which creates a gap during implementation, but as the most critical topmost APIs (and most frequently used bottommost ones) are already fixed – cost of mistakes at this medium layer (which actually IS business/game logic) is not THAT high – especially as (as business logic) it should be designed to withstand frequent changes anyway.

      Hope it makes at least some sense…

Leave a Reply to Wanderer Cancel reply

Your email address will not be published. Required fields are marked *