Skip to main content

Abusing constexpr to implement gettext's _() macro with zero overhead.

You usually use the C library gettext with a _() macro that returns a localized version of the text passed in parameter.

For example:

puts(_("Hello"));

will result in a runtime search of the string "Hello", and _() eventually returns the localized version of the string. I assume the search is implemented using some sort of binary search, so it's probably extremely fast.
It's fast, but we can get rid of the search altogether using constexpr functions!

The trick is to use a constexpr function that at compile time gets the ID of the given string. Then it's a matter of doing an array lookup in the table of strings for the desired locale. The array lookup is still unavoidable because we don't know at compile time which locale the app will use.

In my case, _() is implemented using I18NStringIndex which looks like this:

constexpr bool I18N_EQ(char const * a, char const * b) {
 return std::string_view(a)==b;
}
constexpr int I18NStringIndex(const char* s) {
 if (I18N_EQ(s, "off")) { return 0; }
 if (I18N_EQ(s, "on")) { return 1; }
 if (I18N_EQ(s, "ok")) { return 2; }
 if (I18N_EQ(s, "Quick pause")) { return 3; }
 if (I18N_EQ(s, "Quick pause %s")) { return 4; }
 if (I18N_EQ(s, "Continue")) { return 5; }
 if (I18N_EQ(s, "Exit")) { return 6; }
 if (I18N_EQ(s, "Pause")) { return 7; }
 if (I18N_EQ(s, "Restart")) { return 8; }
 if (I18N_EQ(s, "Game Over")) { return 9; }
 if (I18N_EQ(s, "Play again")) { return 10; }
 if (I18N_EQ(s, "Cancel")) { return 11; }
 if (I18N_EQ(s, "Game is full")) { return 12; }
 if (I18N_EQ(s, "Local lobby")) { return 13; }
 if (I18N_EQ(s, "Ready")) { return 14; }
 if (I18N_EQ(s, "Waiting for other players...")) { return 15; }
 etc...

It is brutal, but the compiler still manages to transform I18NStringIndex("Game Over") to 9, and without noticeably slowing down the compilation!

It is used together with static arrays of strings, such as this one:

std::array<const char*, 138> kStrings_FR = {
"off",
"on",
"ok",
"Mini-pause",
"Mini-pause %s",
"Continuer",
"Quitter",
"Pause",
"Recommencer",
"Game Over",
"Rejouer",
"Annuler",
"Partie déjà complète",
"Partie locale",
"Je suis prêt !",
"En attente des autres joueurs...",
etc...

I18NStringIndex is generated by going over the code with a regex looking for patterns like _(".*"), and kStrings_LANG is generated using the .po files the translators filled.

One danger of this technique is that if for whatever reason the compiler can't run I18NStringIndex at compile time, then you pay a heavy price at runtime. Fortunately in C++20 you can specify the function to be consteval to make sure it does not happen.

Another downside of this technique is that whenever I18NStringIndex is re-generated, all the files containing _() need to be recompiled.
It's not a problem for me because I re-generate that function only when I'm about to send the .po files to the translators, which does not happen often.

Comments

Popular posts from this blog

PewPew Live's look in a nutshell

Occasionally someone will asked how I obtained the PPL look. In a nutshell: Draw everything with lines, including the text and the various icons. It's a lot of work, but besides looking unique it creates a consistent appearance which is a thing that a lot of indie games struggle with. The lines are screen-space projected lines with miter joins. Draw the lines with additive rendering. This means that if a red and green line overlap, the overlap will be yellow. There are a few things not drawn with additive rendering (like the background of buttons to improve readability), but they are exceptions. Add bloom. There's lots of different bloom implementations. Nowadays I use a bloom that is similarly to the one in  blender's eevee . If you see banding, use dithering. Optional: Add even more post-processing like (very slight) chromatic aberration, lens dirt, scan lines, curved monitor, and vignette. No post-processing, just lines Bloom! Ignore the missing bloom at the top All the...

A general state rollback technique for C++

I wanted to write this post for a while. It describes a C++ technique to implement rollback in the context of multiplayer games that I feel is quite interesting and useful. The tl;dr is: don't bother serializing individual objects, just rollback all the memory. Rollback-based multiplayer I've been working on a multiplayer version of PewPew, and for reasons that are outside of the scope of this post, I chose to implement multiplayer with deterministic lockstep and rollback. The basic idea behind rollback-based multiplayer is that the inputs of players are replicated to all the players. Whenever a player receives the inputs of another player, the state of the game is rolled back to the point where the input happened and fast-forwarded back to the present so that the state shown to a player takes into account the inputs of the other players. Because history is being re-computed, some events get undone. For example, it's possible a player saw themselves taking a bonus, but aft...

Ridiculously cheap depth of field effect for lines

I'm working on PewPew's sequel, for which I've revamped the graphics. Instead of drawing lines directly using OpenGL, each individual line segment is made up of two triangles whose vertexes are computed with shaders. Getting lines in 3D space to be properly displayed on a 2D screen is not trivial. In PewPew's sequel I use the screen-space projected lines, a technique very well described in the  Drawing Lines is Hard  post. The upside of drawing the lines yourself is that you are fully in control, which allows you to implement nice things such as joints, perspective, and even simulate depth of field. https://en.wikipedia.org/wiki/Depth_of_field Usually depth of field (DoF) in video games is implemented using a post-processing step that blurs the pixels with an intensity that is a function of the depth of the pixels. When we are rendering lines, we can approximate DoF directly when rendering the lines by having the vertex shader increase the width of lines and r...