A C++ codestyle for if you don't like the looks of C++
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 forstd::vector
.Vec2i
is a two element vector of integers (the linear algebra kind of vector)let
is (effectively) an alias forconst auto
.var
(used once near the end) is similarly an alias forauto
.assuming
is a custom macro for sanity testing that’s a replacement for the standardassert
. When the assumption is not true, it logs a message and triggers a breakpoint, but unlikeassert
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 declarationsPersonally, 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
andconstant
in the library’s header code may be substituted withauto
/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
andconstant
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 typesC++ 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
orstd::map
because the datastructuresstd::vector
andstd::unordered_map
perform better in most cases [wicht12, scb14]. Omittingstd::list
andstd::map
from the list of aliases gives me the opportunity to givestd::vector
andstd::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” forstd::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 lineusing 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++ ostreamsIt 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;
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++ exceptionsThe 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 .