神刀安全网

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

C++ Guidelines – Made-to-Measure vs One-Size-Fits-All Author: “No Bugs” Hare   Follow:  C++ Guidelines – Made-to-Measure vs One-Size-Fits-All C++ Guidelines – Made-to-Measure vs One-Size-Fits-All
Job Title: Sarcastic Architect
Hobbies: Thinking Aloud , Arguing with Managers , Annoying HRs ,
Calling a Spade a Spade , Keeping Tongue in Cheek

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

[[ This is Chapter XIII(c) from “beta” Volume 2 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 {|||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 C++ Guidelines – Made-to-Measure vs One-Size-Fits-All . In practice, while those guys indeed DO know what they’re doing,they DON’T know about your project and its specific needs.

C++ Guidelines – Made-to-Measure vs One-Size-Fits-All As soon as you have some Big Name Guideline, you will have quite a bit of developers taking The Guideline as a gospel To 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,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”).

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 C++ Guidelines – Made-to-Measure vs One-Size-Fits-All . 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
  • C++ Guidelines – Made-to-Measure vs One-Size-Fits-All DO start small, and add more guidelines later DO 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.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.

  • C++ Guidelines – Made-to-Measure vs One-Size-Fits-All Core is edited by Bjarne Stroustrup and Herb Sutter, which automatically makes it a ‘benchmark’ for all the C++ Guidelines . 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 :
    • 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 inare 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.
    • C++ Guidelines – Made-to-Measure vs One-Size-Fits-All there are some attempts to impose completely arbitrary (=’completely unmotivated’) guidelines In 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 frombefore 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 asone.
    • 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 thatis “bad”; on the contrary, it has LOTS of useful information (and up-to-date-for-C++11-and-above-too).
  • . Overall, I like MOST of the stuff within.
    • C++ Guidelines – Made-to-Measure vs One-Size-Fits-All 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 thepositions, 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‘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 useinstead).
    • Last but not least, I disagree with a requirement (at leastcan be read this way) to have a comment for each and every function. Requiring per-class comments are fine (though examples given inare 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.
  • . Not exactly positioned as a set of guidelines, C++ FAQ has quite a bit of discussion about
    • My biggest grief aboutis 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  <<.
  • . 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 ofwhich 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

C++ Guidelines – Made-to-Measure vs One-Size-Fits-All 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 3 rd -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 3 rd -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:

  • C++ Guidelines – Made-to-Measure vs One-Size-Fits-All DO have YOUR OWN guidelines, those which you DO want to have. This way they’re MUCH easier to enforce DO 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 #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
    • 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.
      • C++ Guidelines – Made-to-Measure vs One-Size-Fits-All whether we like it or not, each project has its own dialect of the language Rationale: whether we like it or not, each project has 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

C++ Guidelines – Made-to-Measure vs One-Size-Fits-All from my perspective, any reasonable layout is good enough. However, withappearing, they seem to adviseon 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 one).
      • 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 withthat 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 a set of command-line options for
      • C++ Guidelines – Made-to-Measure vs One-Size-Fits-All There is a reasonably-good way to enforce indentation style, via running astyle (with agreed options) before each commit (b) running astyle (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, astyle 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 C++ Guidelines – Made-to-Measure vs One-Size-Fits-All .

4 as noted above, I Really Hope this guideline will be gone whengoes 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 3 rd -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<> frominstead
    • Besides handling span<> , all pointer arithmetic SHOULD be outright prohibited
  • C++ Guidelines – Made-to-Measure vs One-Size-Fits-All 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)
  • C++ Guidelines – Made-to-Measure vs One-Size-Fits-All 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. Asputs 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 change ).
      • DON’T go into recursive trickery just to make your function constexpr in C++11 style, unless absolutely necessary. Wait for C++14 instead C++ Guidelines – Made-to-Measure vs One-Size-Fits-All .
    • 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.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(!)):

  • C++ Guidelines – Made-to-Measure vs One-Size-Fits-All Yes, I know that LOTS of developers out there think of const as of a nuisance, but I repeat: DO use const whenever applicable DO 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; }

C++ Guidelines – Made-to-Measure vs One-Size-Fits-All 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 C++ Guidelines – Made-to-Measure vs One-Size-Fits-All .

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 C++ Guidelines – Made-to-Measure vs One-Size-Fits-All .

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, 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.
    • 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.
    • C++ Guidelines – Made-to-Measure vs One-Size-Fits-All 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 C++ Guidelines – Made-to-Measure vs One-Size-Fits-All
      • decreases chances to port to another platform, manyfold C++ Guidelines – Made-to-Measure vs One-Size-Fits-All C++ Guidelines – Made-to-Measure vs One-Size-Fits-All
  • 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++ Guidelines – Made-to-Measure vs One-Size-Fits-All .[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 define 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.
    • C++ Guidelines – Made-to-Measure vs One-Size-Fits-All 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 (!)).
    • 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 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]]).
  • C++ Guidelines – Made-to-Measure vs One-Size-Fits-All << for formatting stinks so badly, that if you write it in UK, developers in Australia can still smell it DON’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(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.
      • C++ Guidelines – Made-to-Measure vs One-Size-Fits-All 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: usea.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 C++ Guidelines – Made-to-Measure vs One-Size-Fits-All . 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

C++ Guidelines – Made-to-Measure vs One-Size-Fits-All As I’ve already noted above, I am a strong proponent of “horses for courses” principle As 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. 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.
  • C++ Guidelines – Made-to-Measure vs One-Size-Fits-All 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 C++ Guidelines – Made-to-Measure vs One-Size-Fits-All ).
    • 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, and saving some time too
        • The same applies if you’re using 3 rd -party STL such as EASTL (more on it in [[TODO]] section above)

11 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

12 BTW, keep in mind that slowdowns from LOCK/atomics are MUCH more visible on multi-socket architectures, i.e. on servers

Summarizing Chapter XIII

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…

C++ Guidelines – Made-to-Measure vs One-Size-Fits-All This concludes beta Chapter XIII from the upcoming book “Development and Deployment of Multiplayer Online Games (from social games to MMOFPS, with social games in between)”. Stay tuned for beta Chapter XIV, where we’ll start discussing graphics (though ONLY as much as it is necessary for a multiplayer gamedev).]]

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » C++ Guidelines – Made-to-Measure vs One-Size-Fits-All

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址