Expressiveness is what makes every developer’s life easier. Whether we read or write code, it is definitely easier if we don’t need to jump back and forth between files to get a sense of what’s the code all about.
Imagine you have following line of code in your codebase:
1
run< true, false >();
You don’t really know what true
and false
mean, right? That’s totally fine because this code is not expressive at all. If you try to call such function in your code, you would have the same problem. First, you would need to go to the function definition and remember the parameters order, and then write down that one tiny piece of code. In the end, there is a big chance you will miss the parameters order at some point in future.
In today’s post, I’ll try to explain how we could improve this code and make it expressive. Please note that both example and solution I propose are valid for runtime parameters too, not just template parameters.
First try - write down comments
Pretty naive way of improving the above example is to write down the comments about what each parameter means:
1
run< true /* addFoo */, false /* addBar */ >();
It’s better than without the comments for sure, but it’s far from perfect. Why? Because no one forces us to write these comments. We could make up codebase convention of writing such comments but, even then, someone might forget to add it.
The conclusion is that this approach is bad too so let’s check another one.
Second try - use aliases
Some of you might laugh at this try (because it doesn’t actually solve the problem) but it might happen in real life.
Someone might define a couple of aliases like:
1
2
using ShouldAddFoo = bool;
using ShouldAddBar = bool;
Then the definition could look like:
1
2
template< ShouldAddFoo addFoo, ShouldAddBar addBar >
void run() { /* ... */ }
instead of plain old way:
1
2
template< bool addFoo, bool addBar >
void run() { /* ... */ }
The definition looks better, that’s for sure. But what changes for the call sites? Absolutely nothing.
The call site can remain the same:
1
run< true, false >(); // or the other way around
No compile time error in case you miss the parameter order, not even a warning. Why so? Because alias is a weak type. It’s a plain replacement of a type it truly represents. Thus, it doesn’t solve our issue at all.
So what’s the solution? How to force users (ourselves or any other developer in our team), ideally at compile time, to use the interface correctly?
Strong typed interfaces
I won’t explain what a strong type is in this blog post as you may find lots of resources on this topic. I recommend you also reading a series of blog posts about strong types on FluentCpp.
So, how to make boolean interface mentioned above strong typed? Let’s define 2 scoped enums for that purpose.
1
2
enum class ShouldAddFoo : bool {};
enum class ShouldAddBar : bool {};
Now, we can use above enumerations in our interface:
1
2
template< ShouldAddFoo addFoo, ShouldAddBar addBar >
void run() { /* ... */ }
Function definition looks pretty. What about the call site? Let’s have a look.
1
2
run< ShouldAddFoo{ true }, ShouldAddBar{ false } >();
run< ShouldAddBar{ false }, ShouldAddFoo{ true } >(); // compile-time error
Now, the call site looks exactly as it should:
- it expresses the meaning of the parameters
- it prevents the wrong usage by issuing the compile-time error
Perfect! Are we done now? No, not yet.
We still need to find a way on how to use these parameters in our function’s body.
One handy approach to this problem is overloading the operator*
like:
1
2
3
4
5
6
7
8
9
10
11
12
template
<
typename Enum,
typename std::enable_if_t
<
std::is_same_v< std::underlying_type_t< Enum >, bool >
>* = nullptr
>
constexpr bool operator*( Enum const value )
{
return static_cast< bool >( value );
}
Then, you can use parameters in the function’s body like:
1
2
3
4
5
6
7
8
9
10
11
12
13
template< ShouldAddFoo addFoo, ShouldAddBar addBar >
void run()
{
if constexpr ( *addFoo )
{
std::puts( "addFoo" );
}
if constexpr ( *addBar )
{
std::puts( "addBar" );
}
}
Pretty neat and expressive, right?
If you want to make use of C++20’s concepts for the operator*
overload, you can write it like:
1
2
3
4
5
6
7
8
template< typename Enum >
concept BoolEnum = std::is_enum_v< Enum >
&& std::is_same_v< std::underlying_type_t< Enum >, bool >;
constexpr bool operator*( BoolEnum auto const value )
{
return static_cast< bool >( value );
}
Play around with the code on Godbolt.
Conclusion
In today’s blog post, we’ve found out how to make our code more expressive. Expressiveness isn’t just a floss. It really enhances our life’s quality. Thus, we should all set aside some time to think about ways to pretty up our code. Hope you find suggestions from this post useful in your daily life.
If you have other methods on how to tackle the issue described in this post, please write it down in the comments.
From now on, you can follow me on Twitter too!
Happy coding!