Being More Expressive in C++

With User Defined Literals

C++ provides developers with tools to add expressiveness to code that can make it easier for developers to understand. Two that I have found particularly useful are “type aliases”1 and “user defined literals”2 .

See the gist on GitHub

As an example, a developer might want to allocate some memory on the “stack”. For argument’s sake, they might choose to use a std::array for this purpose.

// 256 Kibibytes
auto buffer = std::array<std::uint8_t, 262144>{};

So they might write something like 256 * 102 as the size parameter to better convey their intention as, let’s face it, a number like 262144 might not be useful to developers who aren’t familiar with their powers of two.

This is where user-defined literals come in. We can add the simplest of literals to improve it’s readability:

namespace c9::memory::size_literals {
    
constexpr auto operator "" _kib (long unsigned int value) -> long unsigned int {
    return value * 1024;
}

} // namespace c9::memory::size_literals

We are using the IEC established suffix3 for byte quantities. Formally, kilobytes, megabytes, et. al. are defined in terms of base 10, whereas kibibytes, mibibytes, et al. are defined in terms of powers of 2. e.g a kilobyte is 1000 bytes, but a kibibyte is 1024 bytes; a megabyte is 1000000 bytes, but a mibibyte is 1048510 bytes.

Suddenly, the code becomes much more understandable, and clearly conveys that the size parameter is intended to convey an amount of memory:

using namespace c9::memory::size_literals;
auto buffer = std::array<std::uint8_t, 256_kib>{};

The using namespace declaration is required to expose the UDL and is implied for the rest of the article.

However, this has a bit of an issue. If the type parameter suddenly becomes something larger than a byte, the whole thing doesn’t make any sense. We can improve this: if we assume that our type parameter for an array representing memory is going to be a std::uint8_t4, we can bind this to an array with a templated using-alias:

template <std::size_t Size>
using memory = std::array<std::uint8_t, Size>{};

Then our declaration of a block of memory becomes:

auto buffer = memory<256_kib>{};

From this, a developer can clearly tell that their colleague is declaring a buffer of memory with a size of 256 kibibytes. There are, of course, considerations for naming your type: perhaps you have established conventions already, or what i’ve used is too generic. In this specific case, I might advocate for something like this (adding in a namespace qualifier for even more context):

auto buffer = memory::buffer_of<256_kib>{};

For me, this raises some interesting question about common naming conventions, but I might have to think about it some more, and maybe do an article later. But, strictly speaking, I think this is better English.

¯\(ツ)

You could definitely go wrong being too specific, e.g, calling it stack_buffer or similar because that’s “where” the automatic storage is. This wouldn’t be ideal because a developer could use new stack_buffer<1234>{}, in which case it would no longer be “stack” memory.

Now for some other use cases (at least for user-defined literals). Keep in mind that so far, with respect to UDLs, we don’t return any custom types, we’ve just used it to provide developers with context.

C++14 added literals for its std::complex type, which represents complex numbers and the std::chrono library has literals for various time periods. This I have found is one of the more useful ones. Compare:

#include <chrono>

using namespace std::chrono_literals;
auto duration = 2ms;

with:

#include <chrono>

auto duration = std::chrono::milliseconds(2);

Finally, within the realm if signal processing, you could add a UDL helper for representing frequency (or for that matter, delta time). Whether this returns a number so as to only add context, or returns a type like frequency, is up to the reader. e.g.:

auto freq = 440_Hz; // This is probably easier
auto delta = 0.00227_dt;

Footnotes

  1. cppreference: Type alias, alias template 

  2. cppreference: User-defined literals 

  3. Wikipedia: Binary prefix 

  4. Other relevant types could be std::byte, char