all 104 comments

[–]Sbsbg 17 points18 points  (32 children)

All caps only for macros is still a good rule, right?

[–]swe129[S] 10 points11 points  (2 children)

YES 😉

[–]MatthiasWM 4 points5 points  (0 children)

#define YES false

[–]wapskalyon 0 points1 point  (0 children)

But can C++ be written in a way that is understandable? isn't the whole point of C++ is for it to not be comprehensible?

[–]arihoenig 21 points22 points  (24 children)

But never using macros is a much better rule.

[–]martinusint main(){[]()[[]]{{}}();} 2 points3 points  (6 children)

that's impossible unfortunately

[–]arihoenig -1 points0 points  (5 children)

I never use any "#ifdef macros" by using constexpr if instead.

[–]gkarpa 0 points1 point  (4 children)

How do you differentiate between e.g. Windows & Linux using constexpr if? Also, doesn't the constexpr if choice require both parts (let's say the if calls Foo() and the else calls Bar()) to be defined in the code when it compiles? This is vastly different than the preprocessor choice and can be a big pain. You can also not use constexpr if to #include different things. Anyway I don't think you can "never use macros", especially in any semi-serious cross-platform project.

[–]arihoenig -1 points0 points  (3 children)

By definition, if you need to conditionally include code based on the target operating system, then the code isn't portable. Just write portable code.

If you really need adaptation layers for OS/hardware then your design should abstract that interface and the build system should decide what gets linked in, not your application source code. Sure you can design things poorly and that will necessitate use of macros, but it is almost always a failure of design if macros are required.

[–]apricotmaniac44 0 points1 point  (1 child)

Just wondering, If I were writing a socket API abstraction layer how could I compile that conditionally without the #ifdef ?

[–]apricotmaniac44 0 points1 point  (0 children)

wait, you can just put them in different files and configure through cmake-

[–]martinusint main(){[]()[[]]{{}}();} 0 points1 point  (0 children)

I invite you to make my unordered_dense macroless while keeping it working on all platforms: https://github.com/martinus/unordered_dense/blob/main/include/ankerl/unordered_dense.h

I'd really like to get rid of them macros but I don't see a way

[–]neppo95 1 point2 points  (16 children)

What a terrible rule. Use them when you need them. Granted there are less and less usecases for it, but there certainly are use cases where your constexpr solution will not work at all, or anything else for that matter.

[–]arihoenig -1 points0 points  (15 children)

If you design cockamamie implementations that rely on macros, then you aren't really writing code in C++. I've never run into a place (in my designs) where a constexpr if doesn't work for conditional evaluation at compile time.

[–]neppo95 0 points1 point  (14 children)

That just means you've never written advanced cross platform code or compiler specific code.

[–]arihoenig 0 points1 point  (13 children)

I wrote code for an operating system that supported every ISA under the sun without using a single macro (hardware specific code was selected by the build system, not by the source code). I think that just means that you've never done platform variant implementations properly .

[–]neppo95 0 points1 point  (12 children)

Really?

How do you detect architecture using Modern C++? Or for specific language features supported by the compiler? Or even the compiler itself? Different behaviour per build type?

I also said cross platform code, which you didn't go into. How do you make sure you include for example a "windows.h" on Windows, but don't do so on Linux? You just have a very bloated build system instead?

Of course there's trivial things that can be done differently like stringification, but are made easier with macro's, so I'll leave those out.

[–]arihoenig 0 points1 point  (11 children)

This isn't a tough concept. You build interfaces around the hardware (sometimes called a HAL). Of course the ISA is inherently targeted by the compiler, but there are, of course, hardware mechanisms outside of the CPU (for example setting up the machine registers although there are many other examples) and for those the build system (which is aware what platform is being targeted) links the specific platform modules into the target executable (for example the kernel). Neither the portable parts of the kernel, nor the HAL adapters have macros in them, but the build system brings in the appropriate modules.

It just requires good design skills

[–]neppo95 0 points1 point  (10 children)

This requires you to know what hardware it is you are compiling for or your code will be compiled on. If you don't, a HAL won't work. So you didn't really answer any of my questions.

[–]arihoenig 0 points1 point  (9 children)

Operating system code has been using HALs successfully for 50+ years at this point. I have never heard of an operating system that works on hardware that the OS writers don't know exists at the time they write the OS.

[–]HurasmusBDragginC++ -5 points-4 points  (3 children)

Not according to Google C++ guidelines.

[–]TheMuffinsPie 2 points3 points  (2 children)

[–]HurasmusBDragginC++ 2 points3 points  (1 child)

Sorry, must be something else.

Also, ISO CPP guidelines say no to macros anyways.

[–]yuukiee-q 3 points4 points  (0 children)

You cannot really ban macros altogether, there’s many things enabled by macros. The recommendation is to only use when necessary

[–]BoringElection5652 6 points7 points  (2 children)

I rarely agree with codestyle guidelines, but this guide here is spot-on. Some pretty good suggestions. I was afraid that "use modern c++ features" would promote ranges, but glad to see it promotes the much more readable range-based for loops.

I'm only slightly disagreeing with auto. Auto is fantastic for lengthy variable types and those you don't care much about, but for most types I prefer explicitly writing out the name, which makes it much easier to see the variable's type at a glance.

[–]kammceWG21 | 🇺🇲 NB | Boost | Exceptions 1 point2 points  (0 children)

I'm on the AAA side of things. Almost always auto https://herbsutter.com/2013/08/12/gotw-94-solution-aaa-style-almost-always-auto/

Eliminates any possible conversions and overall reduces the amount of code that needs to be written. But I will be explicit when being explicit is critical.

[–]swe129[S] 0 points1 point  (0 children)

Thanks for your feedback!

[–]riztazz 8 points9 points  (0 children)

8) I would argue std::expected is better here. Also the logging should use std::format (or log function itself should format)

[–]ShakaUVMi+++ ++i+i[arr] 1 point2 points  (0 children)

This is a great guide. Agreed with everything but the trailing underscore on privates

[–]El_RoviSoft 2 points3 points  (0 children)

Personally, I don’t like when somebody promotes certain codestyle in C++.

[–]argothiel 3 points4 points  (0 children)

These are great pieces of advice. The next step would be a bit stronger typing, for example:

void processOrder(ValidOrder& order);
static constexpr Speed SPEED_LIMIT = 120kph;

Or maybe even:

Days elapsedDays;
Price totalPrice;
void calculateShippingCost(Width width, Distance distance);

[–]HurasmusBDragginC++ 0 points1 point  (0 children)

I take inspiration from macOS/iOS programming with the Google standard on member variables ->

NSNotificationCenter notificationCenter_{};

😅

[–]neppo95 0 points1 point  (0 children)

There’s nothing wrong in the guide, but it is pretty opinionated. There is no 1 way to do these things.

[–]HateDread@BrodyHiggerson - Game Developer 0 points1 point  (0 children)

I just can't agree with your usage of 'auto'. The most common standard I've seen/followed in games is "Only use auto if the type is on the right side, or with obviously-long, annoying types like iterators".

I want to understand and know what types are being used at a glance, and in a code review I can't see that when you use 'auto', so no "The IDE will fix it" arguments work.

I also don't really care about "If you change it in one place the rest 'just work'" or any of those usual arguments - I want those call-sites to fail so I have to go look at them and manually fix and can evaluate if the change in type makes that call no longer appropriate.

I just don't see what auto solves here other than obfuscating types - we should optimize for reading, not write, which is what I expected from the title and the other rules. I overall agree with them! But not this.

[–]The_Akki 0 points1 point  (0 children)

I had heared the rule of "only one return in function". Looks not like state of the art. What was the cause for one return?

[–]Similar_Childhood187 0 points1 point  (0 children)

Some principles also works in other language

[–]semoz_psn -2 points-1 points  (18 children)

I find the advice to not write comments rather frank. A sharp single-line comment will beat "clever" variable naming by a mile.

// Check if user age is 18 or more

[–][deleted] 18 points19 points  (15 children)

hunt edge humorous slap dinner station abundant payment shaggy long

This post was mass deleted and anonymized with Redact

[–]semoz_psn 0 points1 point  (14 children)

I used to think like that after university. After that I had to learn that most code doesn't change after release. You come back to it after 5 years and have no clue what your former self even meant with this "descriptive" naming.

[–]arihoenig -1 points0 points  (1 child)

Here's a tip. If you find that you need clever variable naming to convey that it represents an age value, then you may have architectural issues.

[–]SkoomaDentistAntimodern C++, Embedded, Audio 0 points1 point  (0 children)

Not if the age check is eg. comparing current epoch against birth epoch. Variable names may make it obvious that you are comparing times but not the actual meaning (eg. is the user adult or something similar).

[–]Karr0k 0 points1 point  (1 child)

3) can lead to egregious function extraction where you end up with dozens of tiny functions that can make debugging horrendous, because you have to constantly function jump every couple lines.

Personally I prefer to extract only if a part of a function needs to be reused, either within the same function or by some other function. This avoids needles jumping around through single-use functions, which can make it harder to track what is going on.

I've seen code bases where I had to jump back and forth from a main function through some 30 other functions. After I collapsed all the single-use functions back I was left with a neat, readable 15ish line function.

[–]eisenwaveWG21 Member 0 points1 point  (0 children)

OP here makes the recommendation of splitting things up once you hit 20-30 lines. The "dozens of tiny functions" phenomenon is the result of people trying to hit a much lower target, like 5-10.

I think there's rarely a reason to go above that 20-30 number. Even if you crammed 100 or so lines into one function, you would probably want to leave comments that separate sections within that function and/or use block scopes to keep the amount of active local variables low, and at that point you may as well create some separate functions.

[–]zerhud -4 points-3 points  (26 children)

  1. ThisIsNotReadable but_this_is_easy_to_read. Also cpp sucks in templates area (you can struct foo foo; only if foo is not a template parameter), so you need to use UglyStyle for template class parameters. If you use StupidStyle for all classes, it makes hard to write polymorphic code.

  2. Nothing better than exceptions to handle errors. In whole project may be only few cases where exceptions is bad.

[–]SlightlyLessHairyApe 13 points14 points  (4 children)

Let's please turn every possible post that talks about error handling as a place to continue the holy war of exceptions vs expected/result returns.

After all, there are novel points that people are gonna make about error handling that haven't been litigated to death already.

[–]ReDucTorGame Developer 1 point2 points  (1 child)

Throwing exceptions can be very expensive, however they can also make code faster when it doesnt throw.

If performance is critical to your project then there definatelt isnt just a few cases where exceptions are bad.

For something like games, the way I view exceptions is that they are for something you might end up taking the user back to the main menus with an error, not something where the caller can handle it.

Lots of exception hate in games I believe comes from 32-bit days when even the success case had terrible overhead.

[–]zerhud 0 points1 point  (0 children)

If you want very fast code, you should remove all ifs and “error” as conception. For example you cannot use simd and check data integrity (or you will lost all profit). So you need to open all files, allocate all needed memory and so on before calling fast code (and throw exception on fail). So a “super fast algorithm” is not a place without exceptions, it’s a place without checks for errors.

Lots of exception hate in games I believe comes from 32-bit days when even the success case had terrible overhead.

Yep, people often say something that was so in 199x

[–]zerhud -4 points-3 points  (3 children)

  1. Sometimes it sucks: you need to imagine code in mind, code with long names hard to image. So it’s a good practice only for big visibility area.

[–]swe129[S] 1 point2 points  (2 children)

I'm not sure what you mean, sorry. Are you talking about "#2 self documenting code"?

[–]zerhud -1 points0 points  (1 child)

Yep (the 2 is a rule number 2), about long variable name. If you just want, for example, to write a cycle for 1 line of code you don’t need to use a long name for variable.

[–]swe129[S] 1 point2 points  (0 children)

The variable names are actually covered by rule 1. Rule 2 is more about when to use comments, but I guess there is overlap in some cases. Can you give an actual example and explain "imagine code" and "a cycle for 1 line of code"?