When discussing various programming languages, I’ve regularly heard people say that C++ is a rather ugly language. In this article I’ll be discussing a C++ code style that doesn’t resemble typical C++. Mainly, I’ll be talking about the macros and other technical aspects that contribute to this code style.

Example

An example code excerpt can be seen below. This code is taken from an actual project and not written purely as an example, so it’s not particularly idealized or impractical — it’s real code. The project in question was a checkers game I wrote to learn Unreal engine. The function below creates a list of non-attacking moves that a piece at the position pos could make. A partial glossary can be found beneath the code excerpt.


Here’s how I wrote it, and generally an overview of the code style I’ll be talking about in this article.

List<CheckersMove> get_legal_normal_moves(
      const ABoard* board,
      Vec2i pos,
      const CheckersRules& rules
) {
  constant EMPTY = CellOccupant::EMPTY;

  assuming (board != nullptr);
  assuming (board->tile_exists(pos));

  let piece = board->get(pos);
  let is_white = piece.is_white();
  let is_king = piece.is_king();

  List<CheckersMove> result;

  // For each direction...
  let &dirs = (is_king? ALL_DIRS : (is_white? WHITE_DIRS : BLACK_DIRS));
  let dir_count = (is_king? TOTAL_DIR_COUNT : NORMAL_DIR_COUNT);

  for (int i = 0; i < dir_count; i++) {
    let dir = dirs[i];

    // Get the neighbor...
    let neighbor = pos + ONE_STEP[dir];
    if (!board->tile_exists(neighbor))
      continue;

    // If it's empty, this is a valid move...
    if (board->get(neighbor).occupant != EMPTY)
      continue;
    result.push_back(CheckersMove{pos, neighbor, 0, {}});

    // If this piece isn't a flying king, that's the only possible move in this
    // direction...
    if (!is_king || !rules.can_king_fly)
      continue;

    // For flying kings we also check all the tiles behind the neighbor...
    var next_tile = neighbor + ONE_STEP[dir];
    while (board->tile_exists(next_tile)
           && board->get(next_tile).occupant == EMPTY) {
      result.push_back(CheckersMove{pos, next_tile, 0, {}});
      next_tile += ONE_STEP[dir];
    }
  }

  return result;
}

Below is what I would consider “normal C++”. Note that there’s almost no comments. That’s because it’s common for programmers to explain very little, and in my opinion how often you explain yourself is also a part of your code style.

std::vector<CheckersMove> getLegalNormalMoves(
      const ABoard* board,
      FIntPoint pos,
      const CheckersRules& rules) {
  assert(board != nullptr);
  assert(board->tileExists(pos));

  const GridCell piece = board->get(pos);
  std::vector<CheckersMove> result;

  const Direction* dirs =
      piece.isKing()? ALL_DIRS : piece.isWhite()? WHITE_DIRS : BLACK_DIRS;
  unsigned dirCount = piece.isKing()? TOTAL_DIR_COUNT : NORMAL_DIR_COUNT;

  for (int i = 0; i < dirCount; i++) {
    const Direction dir = dirs[i];
    const FIntPoint neighbor = pos + ONE_STEP[dir];

    if (!board->tileExists(neighbor)
        || board->get(neighbor).occupant != CellOccupant::EMPTY)
      continue;

    result.push_back(CheckersMove{pos, neighbor, 0, {}});

    if (!piece.isKing() || !rules.canKingFly)
      continue;

    // handle flying kings
    FIntPoint nextTile = neighbor + ONE_STEP[dir];
    while (board->tileExists(nextTile)
           && board->get(nextTile).occupant == CellOccupant::EMPTY) {
      result.push_back(CheckersMove{pos, nextTile, 0, {}});
      nextTile += ONE_STEP[dir];
    }
  }

  return result;
}

Vec2i is replaced with FIntPoint here because that’s what I was using as the definition of Vec2i in this project (in personal projects I always use the name “Vec2i” for 2D integer vectors and just typedef Vec2i to refer to the most sensible implementation for what I’m doing).


Here’s a reference of what you need to know to be able to understand the excerpt properly:

  • List is an alias for std::vector.
  • Vec2i is a two element vector of integers (the linear algebra kind of vector)
  • let is (effectively) an alias for const auto.
  • var (used once near the end) is similarly an alias for auto .
  • assuming is a custom macro for sanity testing that’s a replacement for the standard assert. When the assumption is not true, it logs a message and triggers a breakpoint, but unlike assert it doesn’t abort. This means that the program won’t automatically crash when an assumption is false.
  • pos is short for “position”.
  • dir is short for “direction”.
  • In some checkers rulesets, kings can fly, which means that they can travel any distance in a single turn. In other rulesets, kings may only move a single tile. This function handles both cases.
  • For CheckersMove, only the first two members matter in this context: The first is the origin and the second is the destination of the piece that’s being moved.

Concerning the name “ABoard”, I’d like to note that this checkers game was made in Unreal Engine, which forces you to use Hungarian notation for Unreal-related classes. The capital A was mandatory.


For me, the code style on the right is slower to gloss through. The biggest differences are the comments and whether or not variable types are explicit. I’ll talk about the way I comment my code briefly near the end of the article. As for types: Although type information is very nice to have sometimes, in my opinion it also creates a lot of visual clutter which makes the code slower to read. I know of myself that — for most local variables — I do not need the type to be explicitly specified because I have an idea of what I’m looking at in my head. Moreover, the fact that so many programmers manage to make working software in languages like Python and Javascript (and in most cases even prefer those languages over C++ and Java) also goes to show that explicitly writing out the type of each variable is not really necessary.

Now, which one is better is very much a matter of personal preferences and you won’t hear me say that you have to like the code style on the left. However, if you did like the style I used, I will discuss how to write code similar to it in the rest of this article. I will also explain my reasoning throughout, which is useful because you can decide for yourself whether you agree with my reasoning and adjust everything to your own opinions.

First I will talk about the technical parts that are necessary for the code style on the left. This is mainly a lot of macros. Secondly I will briefly discuss some of the non-technical aspects of the coding style on the left.

The technical parts

The biggest difference between the two code excerpts in the introduction is made by a set of macros and typedefs. In this section I’ll show you their implementation and the logic behind it.

let, var & constant

Concise syntax for variable declarations

Personally, I’m not particularly fond of the verbosity of programming languages like C++ and Java. I used to think that explicitly specifying as much type information in the code as possible helps to make it more readable, but ever since I’ve gotten more experience in Javascript and Python, I’ve learned that most of the time it doesn’t help readability at all. Rather, the long type names often visually clutter the code, making it more difficult to read. Most of the time programmers can tell what types of variables they’re dealing with from the way the variables are being used, so it isn’t really desirable to be specifying types all the time.

Fortunately, C++ has had the auto keyword since C++11, which derives the type of a variable from the context. For example:

auto x = 10;
auto y = "abc";
auto z = find_material(y, x);

In the real world, you will likely also be using the keywords static and const regularly, which will lead to code that’s quite a bit less pretty:

static const auto FRUITS_SIZE = 4;
static const auto FRUITS[FRUITS_SIZE] = {"apple", "banana", "citrus", "durian"};

const auto randomized = randomize_order(FRUITS);

for (auto i = 0; i < FRUITS_SIZE; i++) {
    const auto &fruit = fruits[i];
    const auto uppercased = to_uppercase(fruit)
    print(uppercased);
}

Because I’m not very fond of the way that looks, I use a set of macros to make my function bodies a bit prettier. Here’s how it looks with macros:

constant FRUITS_SIZE = 4;
constant FRUITS[FRUITS_SIZE] = {"apple", "banana", "citrus", "durian"};

let randomized = randomize_order(FRUITS);

for (var i = 0; i < FRUITS_SIZE; i++) {
    let &fruit = fruits[i];
    let uppercased = to_uppercase(fruit)
    print(uppercased);
}

In my opinion it’s a world of a difference, and it really helps to make C++ look quite similar to most of the newer programming languages.

Here are the macro definitions:

// Slightly risky use of macros. You can #undef this when it's causing trouble.
#define var  auto
#define let  const auto
#define constant  static const auto

I mainly find the let macro very useful, because having such a short and easy way to make a const variable promotes using const variables all the more often. I also like to use short names for these macros — it’s less typing —, hence I used the names var and let.

In the example above I only used let, var and constant, but keep in mind that in practice you will generally combine these auto macros with explicitly stated types. For example in the loop there is no good reason to use var i = 0 over int i = 0. It’s entirely up to your own taste when you use explicit types and when you use these macros. I myself almost always use the macros in function bodies and I use explicit types everywhere else, such as in headers.

Do note that using these macros is slightly risky and may unintentionally cause compilation errors when there are already variables with the same names somewhere in the code or in a header. However, I do want to say that I haven’t had any issues so far when using these macros in my own personal projects. In the next section I’ll discuss the potential issues that may arise when using these macros, and then I’ll discuss how to prevent these issues as much as possible.

Potential problems when using these macros in a large project

Several issues may arise when you try to use these macros in a large project:

  • These macros might cause name conflicts when you include headers from external libraries. When you include these macros first and a library’s header second, then any appearances of the words var, let and constant in the library’s header code may be substituted with auto/const auto/static const auto, which could break the code.
  • If the macros are ever exposed in a public header (for example if you’re making a library), then users will also get var, let and constant defined when their source files include your public header. This will lead to compilation problems when the user has a variable, a type or a function with one of those names already, or when the user is including another public header (e.g. from another library) that contains one of those names.

Fortunately, issues with these macro will usually only lead to compilation errors and not to subtle bugs. For example, maybe some code already contained the line int var = 10;. When using these macros this line will be substituted with int auto = 10;, and this will fortunately give us a compilation error because a variable isn’t allowed to be called “auto”. If you’re wondering why I would consider that fortunate, please consider what a headache it would be to debug issues with these macros if they could lead to valid code. As it is, the compiler gallantly and swiftly points out name collision problems by halting with an error, which saves us the time spent finding the problem ourselves.

One exception to this is the unlikely situation in which there is already a type with the exact name “var”, “let” or “constant”, entirely in lowercase. If that occurs you could potentially get subtle bugs. For example, let’s consider the situation where you have a struct called “constant”. In a source file, the header defining this struct is included first, and the header featuring the constant macro is included second. This might be possible to compile, and any usages of constant in the source file will be referring to the macro, not the struct, which might not be intended. To avoid this problem entirely, don’t have any types (i.e. structs, classes or typedefs) named “constant”, “var” or “let”.

Solutions to the problems

As discussed in the previous section, using these macros in your project could lead to name collision issues. For small projects I would say that name collision issues are sufficiently unlikely that you should not worry about them unless it becomes evident that you should (i.e. because you run into name collision issues in practice). For larger projects I would advise to be more rigorous, and for that reason I would recommend adhering to the rules below.

To avoid name collision issues entirely and make it safe to use these macros in bigger projects, I would suggest doing the following. I must note that I have not tried it myself for a particularly large project.

  • Define all macros that are likely to cause name collisions in a separate header file. I personally call this macros.hpp.
  • Only include the macros.hpp file in .cpp files, never in headers and especially never in public headers (i.e. headers that are part of a library’s API).
  • Always include the macros.hpp header last (unless you’re working with something like Unreal Engine which forces you to include a certain header last, then make it second last).

The first two points are quite important and prevent the macro definitions from being included in any header file. As a consequence, no source file should have the macros defined unless the macros.hpp header is explicitly included. The last point is not as important as the others and only serves to prevent name collisions with other header files included in that .cpp file. For the sake of rigor I nonetheless recommend following the last point for any serious project that might wish to use these macros.

The great disadvantage of these measures is that you can no longer use the macros in header files. This especially stings for defining template functions, because template functions must be fully defined in a header. (SIDENOTE: Technically there is a way to put template function bodies in a `.cpp` file but it has a very big drawback and I've never seen it used in practice.) That’s why I recommend not using the rules listed in this section for small projects. It’s nicer to be able to use the macros wherever you please, even in headers. However, even if you have to follow these rules and can’t use the macros in header files, it generally beats not being able to use them in any files at all.

You can also add short aliases for macros with long names to your macros.hpp file. For example let’s say you have a macro called MYLIBRARY_THROW and you use this macro a lot in .cpp files, it might be nice to be able to add something along the likes of #define THROW MYLIBRARY_THROW (or throw_ instead of THROW). This lets you use THROW in the .cpp files, which can save a bit of typing and visual clutter.

List, Map and String

Let's rename our frequently used STL types

C++ isn’t exactly famous for the quality of its naming. One well-known type, where even the original author himself has expressed his regrets about its name, is std::vector [step12, step14]. This name has a few problems: This name isn’t obvious — nowadays it isn’t anyone’s first guess for this kind of datastructure if they haven’t used C++ before (however it does match what Scheme and Common Lisp did, so this is kind of a matter of C++ just being old) —, it isn’t especially memorable — if you’ve been using C++ for a while you’ll have remembered it, but not because it’s all that easy to remember —, and lastly it’s frankly wrong — it’s wrong because it actually has almost nothing to do with vectors from linear algebra and doesn’t provide any linear algebra related functionality at all.

I think that’s enough justification to start renaming things. Fortunately, C++ does give you all the programming languages features you need to rename anything you want without incurring any performance penalties, except for functions (renaming functions is not as easy). I put all of these aliases in a header called typedefs.hpp, and then indirectly include that header in most of my source files. Ironically my typedefs.hpp does not include a single typedef statement, since using statements can do everything typedef can do and more.

Here are the aliases that make the biggest impact on my code:

#include <string>
#include <unordered>
#include <vector>

using uint = unsigned int;

using String = std::string;
template<class ...C> using List = std::vector<C...>;
template<class ...C> using Map = std::unordered_map<C...>;

Here’s my reasoning for these aliases:

  • I myself never use std::list or std::map because the datastructures std::vector and std::unordered_map perform better in most cases [wicht12, scb14]. Omitting std::list and std::map from the list of aliases gives me the opportunity to give std::vector and std::unordered_map the shorter and more obvious names, which is an opportunity I’ll gladly take. If you prefer, you can also use the name “Dict” for std::unordered_map, since it’s the same type of datastructure as dicts in Python.
  • I prefer not to have to type std:: all the time, not only because it takes longer to type but also because I think it’s ugly visual clutter. However, I don’t want to use the line using namespace std; because that would put too many symbols into the global namespace and might even lead to name collisions with future versions of C++. For this reason, I add an alias only for the STL types that I frequently use.
  • I capitalize the first letter for the more complex types. That’s just a matter of consistency. Something nice about this capitalization for types is that you can name variables “list” or “map” in lowercase when you’re missing the inspiration for coming up with more contextual variable names.

Besides those aliases, you should also add aliases for the other types you commonly use. I also use the following:

using byte = unsigned char;

template<class ...C> using Unique = std::unique_ptr<C...>;
template<class ...C> using Shared = std::shared_ptr<C...>;
template<class ...C> using Weak = std::weak_ptr<C...>;

template<class ...C> using Array = std::array<C...>;
template<class ...C> using Function = std::function<C...>;
template<class ...C> using Pair = std::pair<C...>;
template<class ...C> using Tuple = std::tuple<C...>;
template<class ...C> using Limits = std::numeric_limits<C...>;

The first three aliases help to make smart pointers a little bit less typing. Smart pointers are very useful but their names are rather long given how often I use them. The rest is mostly just taking types out of the std namespace so that I almost never have to type std::.


Section references

step12: Alex Stepanov — recording of Spoils of the Egyptians, lecture 2 (relevant section starting at approx. 6 minutes and 20 seconds in) — https://youtu.be/etZgaSjzqlU?t=380

step14: Alex Stepanov, Daniel Rose — From Mathematics to Generic Programming — relevant quote:

The name vector in STL was taken from the earlier programming languages Scheme and Common Lisp. Unfortunately, this was inconsistent with the much older meaning of the term in mathematics and violates Rule 3; this data structure should have been called array. Sadly, if you make a mistake and violate these principles, the result might stay around for a long time.

Alex Stepanov, original author of the C++ STL

wicht12: not formally peer reviewed: Baptiste Wicht — C++ benchmark – std::vector VS std::list VS std::deque — https://baptiste-wicht.com/posts/2012/12/cpp-benchmark-vector-list-deque.html

scb14: not formally peer reviewed: Anonymous (Super Computing Blog) — Ordered map vs. Unordered map – A Performance Study — http://supercomputingblog.com/windows/ordered-map-vs-unordered-map-a-performance-study


print($(my_int))

Let's replace C++ ostreams

It seems to me that people who come to C++ after learning another programming language usually don’t like C++’s best-known options for forming strings from static text and variables. It is quite important that programmers can conveniently put together a string, especially for logging messages.

Let’s look at various ways of logging a string of text and variables in different programming languages, so that we can compare between different languages. Here’s the normal way of logging a line of text in C++:

std::cout << "Hello! My name is " << name << ", I am " << height << " cm long "
          << "and I dearly enjoy " << hobby << ". My location is "
          << "[" << x << "," << y << "," << z << "]." << std::endl;
SIDENOTE: Despite this being the standard way of printing text in C++, this code excerpt may not visibly print anything in Microsoft's Visual Studio unless you're specifically making a console application. Frankly, it's weird that Visual Studio doesn't show console outputs somewhere in its GUI by default, since logs are important for developers and all other IDEs I've used do show the logs, but it is as it is.

That same line in Javascript’s template strings (from ECMASCRIPT 16):

console.log(`Hello! My name is ${name}, I am ${height} cm long and I dearly `
            + `enjoy ${hobby}. My location is [${x},${y},${z}].`);

And in good old C:

printf("Hello! My name is %s, I am %g cm long and I dearly enjoy %s. "
       "My location is [%g,%g,%g].", name, height, hobby, x, y, z);

Most other languages have something very similar to Javascript’s template strings and C’s string formatting.

In my opinion, Javascript’s template strings are easy to read and C’s format strings are also relatively easy to read, although you do need to look back and forth between the format string and the argument list. On the other hand, C++ ostreams are similar to template strings in terms of functionality but without the merit of being easy to read. Ostreams also require a few extra steps if you want to put the result into a string variable or use it as a function argument.

Overall I’m not especially fond of C++ ostreams, so here’s how I do it in C++:

print("Hello! My name is "+$(name)+", I am "+$(height)+" cm long and I dearly "
      "enjoy "+$(hobby)+". My location is ["+$(x)+","+$(y)+","+$(z)+"].");

Did you know “$” is a valid identifier character in C++, meaning that you can use it in function and variable names? Now you do. We’re putting this obscure fact to the best use I can think of by making the function $(...) a fancy alias for std::to_string(...).

The first and most obvious benefit is that this is a lot more concise than ostreams. Secondly, using this alias for to_string, an expression like "Hello, I'm "+$(name) will simply have a string as the result, so you can use these expressions directly as a function argument (unlike C++ ostream expressions).

Here’s the implementation for $(...):

namespace string_trickery {
    // (C) Adam Nevraumont, used under Creative Commons Attribution license
    template<class Object>
    String as_string(Object&& object) {
        using std::to_string;
        return to_string(std::forward<Object>(object));
    }
}

/** Turns anything into a string. A short alias for to_string.
 *
 * You can easily add support for your own types by defining a function in the
 * global namespace with the signature `String to_string(YourType)`. */
template<class T>
String $(T&& t) {
    return string_trickery::as_string(std::forward<T>(t));
}

What’s nice about this implementation is that is makes it very easy to add a custom to_string function for your own types:

struct YourCustomType {
    int your_custom_member;
}

inline String to_string(const YourCustomType& custom_thing) {
    return "{ your_custom_member: "+$(custom_thing.your_custom_member)+" }";
}

When you have a to_string function for YourCustomType, you can simply use YourCustomType with $(...) anywhere you want, like this:

let your_thing = YourCustomType{10};
print("An example of turning your thing into a string: "+$(your_thing));
print("$(...) will automagically find & use your to_string function.");

Lastly, to make these examples work, you will also want a print function. For our print function we won’t have to do much. We can just delegate the work to std::cout (or printf if you prefer):

inline void print(const String& message) {
    std::cout << message << std::endl;
}

An extra macro

If you want, you can make this even shorter by adding a macro for + to_string(...) +, so that you don’t have to type two pluses each time you want to insert a variable into a string. That way the code would look like this:

print("Hello! My name is "$(name)", I am "$(height)" cm long and I dearly "
      "enjoy "$(hobby)". My location is ["$(x)","$(y)","$(z)"].");

Now it almost looks the same as official support for template strings!

I personally prefer to keep $(...) as just a fancy alias for std::to_string(...), so I don’t actually use a macro like that myself. However, what you do in your code is entirely up to you.

If you end up using it, you do need to be wary of the fact that a macro like that won’t automatically work when the variable is at the end of the string. For example the code print("Hi, I'm "$(name)) would not compile (because it would be turned into print("Hi, I'm " + to_string(name) + ), which has a loose plus near the end). This isn’t a complete dealbreaker because you could use the line print("Hi, I'm "$(name)"")" instead, which would compile correctly and will give the same result, but you may find it a bit inelegant.

assuming (input.size() > 3)

A customized replacement for “assert”

Standard C asserts are a very useful tool for preventing programmers from writing buggy code, because they can be used to make your assumptions explicit and to verify that the assumptions you’re making are indeed always true. In C++, standard asserts are used as follows:

#include <cassert>

void your_function(int* a) {
  assert(a != nullptr);
  // Do things with a...
}

void main(uint argument_count, char* arguments[]) {
  int my_int = 1;
  your_function(&my_int);  // No problems!
  your_function(nullptr);  // The "assert" invocation will make this crash.
  // (but only in debug mode)
}

The nice thing about the assert macro, compared to normal if statements, is that assert does not lead to any performance overhead in release builds. In release builds, an assert statement does nothing at all. Your assert conditions are only checked in debug builds of the program, which means that you can do checks with assert that you might prefer to skip in a release build for performance reasons.

The downsides of assert are as follows:

  • assert normally isn’t very explicit about where things went wrong. This isn’t a real problem when we use a proper debugger, because when the assert condition is false, the debugger will stop the program’s execution and show you where things went wrong. However, some programmers rely on print statements for debugging, in which case this is quite a problem. SIDENOTE: Frankly, I highly recommend using an IDE with a debugger built in. Commandline debuggers and print statement debugging are immensely tedious in comparison.
  • assert will immediately crash the program when its condition is false. I personally have never had an issue with this, because when an assumption isn’t being upheld I want to debug what went wrong anyways, but I have heard from others that this functionality prevents them from using it.

To address these issues, I provide an implementation for a custom “assuming” macro in the file linked below. This does the same thing as assert, except it doesn’t crash the program and it prints quite clearly where it is in the code when the assumption fails.

I chose for the name “assuming” because it makes it more clear what the statement is useful for (making your assumptions explicit), and also because “assert” is a relatively obscure English word that foreigners may not know the meaning of — I’m Dutch and I can tell you that I wasn’t exactly sure what the verb “to assert” meant when I learned programming.

The implementation of assuming is relatively long. We need to take a bit of a detour by defining a few other macros first to gradually build up the full functionality of assuming. Instead of including all this code in this post, I’ve put it in a separate file you can find here: [assuming.hpp]

I also suggest adding a second macro assuming_msg, which takes a string as an argument and also prints that string when the assumption fails. Sometimes you may use assumptions for validating that other developers are using your code correctly, and if they’re not it can be helpful to tell them what they’re doing wrong when that isn’t clear from the default failure message.

afterwards { delete my_array; }

For safely combining manual memory allocations and C++ exceptions

The afterwards pseudo-statement essentially lets you run a piece of code just before the current scope is exited. This is great for all your clean-up code, because the code in the afterwards statement will run no matter if the scope is being exited because the function is ending normally or because an exception is unwinding the stack.

Sometimes this concept is referred to by the name “scopeexit” or “defer” (named after the defer statement from Go, which does something similar but not quite the same). I personally like to use the name “afterwards” for it, because the word “afterwards” is simple English (as opposed to “defer” which is a pretty obscure word in my opinion) and it makes it more clear what the statement does. The downside is that it’s a much longer word than “defer”, but I personally don’t use “afterwards” often enough that the length becomes a serious issue.

Here’s a code excerpt from a small OpenGL project to make it all a little more clear. I won’t explain OpenGL here, but just note that we’re doing the clean-up inside the afterwards pseudo-statements and not at the end of the function.

Shader::Shader(
    const String& vertex_path,
    const String& fragment_path,
    const List<String>& attributes
) {
    uint vertex_shader = compile_vertex_shader(vertex_path);
    afterwards { glDeleteShader(vertex_shader); };
    
    uint fragment_shader = compile_fragment_shader(fragment_path);
    afterwards { glDeleteShader(fragment_shader); };
    
    this->id = glCreateProgram();
    glAttachShader(id, vertex_shader);
    glAttachShader(id, fragment_shader);
    
    bind_attributes(id, attributes);
    
    glLinkProgram(id);
    check_linking_outcome(id);
}

In the excerpt above, the function check_linking_outcome can throw an exception. Normally this isn’t safe, because while unwinding the stack the clean-up part of a function is normally skipped and glDeleteShader wouldn’t be called, but because we’re using the afterwards pseudo-statement it’s perfectly fine. After running check_linking_outcome(id) the program will run glDeleteShader(fragment_shader) and then glDeleteShader(vertex_shader).

By the way, if you use afterwards multiple times in a scope their order of execution is perfectly stable and predictable: the pieces of code inside afterwards pseudo-statements are executed in opposite order of their appearance, meaning the last afterwards is executed first, and the first one is executed last. This is because afterwards internally creates a local variable and runs the given code in its destructor, and C++ guarantees that local variables are destroyed in the reverse order of their creation[c++20].

I’m not entirely sure about how big the performance impact of afterwards is. It’s probably quite small, but I would hold off on using it in performance-critical sections of code without benchmarking it or analyzing the generated assembly. However, personally I’ve been using it just fine in non-performance-critical parts of my programs.

Here’s the implementation:

// This class runs a given lambda function when it's being destroyed.
template<class CustomFunction>
class OnDestruction {
public:
  OnDestruction(CustomFunction custom_function)
      : custom_function(custom_function) {}
  ~OnDestruction() { custom_function(); }
  CustomFunction custom_function;
};

// PREFIX_CONCATENATE concatenates its two arguments as code (not as a string).
// The C preprocessor is mystical and mysterious so just believe me when I say
// this stuff is necessary.
#define PREFIX_CONCATENATE_IMPL(x, y) x ## y
#define PREFIX_CONCATENATE(x, y) PREFIX_CONCATENATE_IMPL(x, y)

// This macro turns into code like "OnDestruction afterwards_on_line_23 = [&]".
// We append the name with __LINE__ to prevent multiple uses of afterwards from
// having the same name.
#define PREFIX_AFTERWARDS OnDestruction \
    PREFIX_CONCATENATE(afterwards_on_line_, __LINE__) = [&]()

// If you have a separate macros.hpp file, put this next line in there:
#define afterwards PREFIX_AFTERWARDS

BEWARE that this implementation only allows for one use of afterwards per line of code. If you don’t do anything weird with your code, that won’t be an issue, but that might be a problem if you’re minifying your C++ (for some reason?) or obfuscating it (notably only if you’re obfuscating the C++ source code itself and not just the generated binaries), or if your personally-preferred code style does not include any new-line characters.


Section references

c++20: ISO/IEC standard 14882:2020 — “Programming Languages — C++”

In section 8.7, regarding local variables (referred to as “objects with automatic storage duration”):

On exit from a scope (however accomplished), objects with automatic storage duration (6.7.5.4) that have been constructed in that scope are destroyed in the reverse order of their construction.


Briefly about style

After all these macros, I’ll part with a few thoughts on the non-technical aspects of code style.

First of all, I won’t be dictating a choice for capitalization or indentation. Typically people never stop talking about these two subjects when any discussion on code style starts, but in my opinion all of the popular choices for capitalization and indentation are completely fine, as long as you’re consistent with them. Some people swear by one choice and dislike reading code with a different style choice, but I’m under the impression it’s mostly a matter of what you’re used to. I think many developers simply use the same code style as the one used by a technology they’re using (the programming language or an important library), which is a sensible choice.

I do wish to briefly note that I’ve noticed a small trade-off between camel case and snake case: I find that snake case is slightly easier to read than camel case, but more difficult to type (especially for inexperienced typers) because of all the underscores — the underscore key is somewhat out of the way so pressing it all the time takes some getting used to.

Now, instead of talking about that kind of typical style guide stuff, I’ll share a few thoughts that you don’t see as often in style guides.

Use comments to describe your big steps

In non-trivial function bodies, you’re probably breaking a big task into a couple of relatively small steps. Sometimes, it’s completely trivial to derive what you’re doing from the code itself. Sometimes, it takes a minute to understand what you’re actually trying to accomplish with a piece of code. Specifically in the second situation I think it can be a major boon to describe the big steps you’re taking in comments. An example of this can be seen in the introduction.

When you describe the big steps of a big function, a reader can quite quickly figure out what each chunk of two to twenty lines of code accomplishes, which means they won’t have to read the code itself (which is oftentimes slower and more difficult to do than reading a plain English comment).

Of course, this does come with a downside. Writing comments takes extra time, especially if you have to think for a while about what you’re going to write. Personally I’m very used to describing my big steps — I basically write down in comments what I’m already thinking while writing the code —, so I’m hardly slowed down by it. However, how easy or difficult it is to write good comments varies from person to person. You may find it much more difficult to quickly describe the steps in a way that’s actually useful to readers. How useful your comments are depends on how lucid and insightful your writing is, and if your writing isn’t very lucid, perhaps you also wouldn’t get quite as much benefit from describing your steps.

Don’t be stingy with local variables

Some people seem to try to make as few local variables as possible, however an extra local variable can be a great help in making a function body easier to understand. Local variables can be used to break big formulas down into small parts, and you can also put the result of a boolean expression into a local variable so that you can use its variable name to explain what the result of that expression represents.

Here’s a short example of putting a boolean expression into a local variable. It’s taken from the checkers project I referred to in the introduction. If you look at the excerpt, it’s obvious the variable is not strictly necessary (it isn’t reused later in the code), but in this case it helps because you don’t have to spend your time trying to derive what the boolean expression is doing. This also lets me use a slightly trickier boolean expression, because the expression itself doesn’t need to be as readable (since you have a solid hint at what the code is doing from the variable name).

bool piece_belongs_to_current_player =
  (state->board->get(cell).is_white() == state->is_whites_turn);
if (!piece_belongs_to_current_player)
  return;

Concerning the use of unnecessary local variables, you should know that modern C++ compiler optimize the code heavily and as a result using an unnecessary local variable usually has zero impact on the resulting binaries when you’re using optimizations. For clarity I wish to note that what I’m saying applies to simple types like integers and booleans, but of course, adding an extra std::vector requires an extra allocation, which is less efficient.

The rest of this section is a quick demonstration of the fact that it doesn’t matter if you use extra local variables. I only ran this demonstration with Clang++ 9 but I’m sure you would get similar results with G++ and MSVC++. We’ll be comparing the binaries generated by two different C++ source files and show that they’re identical.

Here’s the test program, which uses minimal local variables.

#include <iostream>
#include <cstring>

const int NUMBERS[] = {10, 30, 40, 80, 100};
const int NUMBERS_COUNT = 5;

int main(int argument_count, char** arguments) {
  // a is just some kind of external input, to prevent the compiler from
  // pre-computing things.
  int a = strlen(arguments[1]);
  
  // The actual meat of the program...
  int results[NUMBERS_COUNT];
  
  for (int i = 0; i < NUMBERS_COUNT; i++) {
    result[i] = NUMBERS[i] * a + NUMBERS[i] / a - a*a;
  }
  
  // Output the results for the sake of having a side-effect.
  for (int i = 0; i < NUMBERS_COUNT; i++)
    printf("%d", results[i]);
}

For the second program, we’ll add some local variables in the middle section:

...
int results[NUMBERS_COUNT];

for (int i = 0; i < NUMBERS_COUNT; i++) {
  // By the way, this isn't an example of how to use local variables in a
  // sensible way. It's just to show that it's no issue to use as many of them
  // as you please.
  int factor = NUMBERS[i] * a;
  int div = NUMBERS[i] / a;
  int square = a*a;
  
  results[i] = factor + div - square;
}
...

Then compile an optimized build of them in whichever way you usually compile C++. Once they’re both compiled, we can compare the binaries. Here’s a simple way to compare the binaries on Linux:

# Disassemble "./binary1" and put the results in "./disassembly1"
objdump -d ./binary1 > ./disassembly1
objdump -d ./binary2 > ./disassembly2
# Compare the disassembly files
diff ./disassembly1 ./disassembly2

If right, there should be no difference between the program code of the two versions.

Parting words

When you put all of that together, you should get something that looks quite a lot like the excerpt in the introduction. Of course, feel free to pick and choose only the ideas from this article that you thought were good, and you’re also free to adapt these ideas to your own tastes and preferences.

All original code in this article I provide under the Creative Commons CC0 license. Once in this article a fragment of code is under another license, but that’s indicated in the code excerpt itself.

I hope you’ve found this article helpful. For discussion and issues, I can be reached at [redacted].