Page 4 - Human Readable Magazine v01
P. 4
Y OU KNOW, THE C++ STANDARD IS AMAZING
SOMETIMES. IT CONTAINS HIDDEN GIFTS FOR THE MOST CAREFUL READERS.
ONE OF THESE GIFTS IS BURIED IN THE DEFINITION OF UNEVALUATED OPERANDS, BOTH SUCCINCT AND IMPORTANT:
In some contexts, unevaluated operands appear [...]. An unevaluated operand is not evaluated.
Let’s try to dissect it and understand what the standard offers us by means of this often-underrated tool.
INTRODUCTION
To be honest, I’ve cheated a little. The full quote contains another statement that should make it clearer what an unevaluated oper- and is:
An unevaluated operand is considered a full-ex- pression.
In other words, the unevaluated operands are the operands of some operators of the language. They are expressions in all re- spects, but such that they are never evaluated. The reason is be- cause those operators are there just to query the compile-time properties of their operands.
If this still doesn’t seem interesting to you, note that not being evaluated means not giving rise to side effects. Right now, the term SFINAE is probably showing up in your thoughts along with many other fancy things, and unevaluated operands are getting more and more interesting.
What are these operators then? Up to C++17, there are four op- erators the operands of which are unevaluated: typeof, sizeof, decltype, and noexcept.
C++20 will add a few other operators like them, but we have to wait a little longer for that.
So, why are these operators so special and what can we do with them?
decltype
If you’ve ever worked in modern C++, it’s likely that you used decltype at least once. This is probably the most used operator when doing SFINAE.
To sum up and to avoid speaking standardese too much, its goal is to inspect the declared type of an element or of an expression.
Let’s take a look at an example of use:
Here we are exploiting the tag dispatching idiom to literally select the right function to execute. As you can see, decltype is used to probe a compile-time feature of item, or better yet, of the type T that has a member function named func. The best part is that func isn’t actually executed in this context because (remember!) the whole expression is an unevaluated operand of decltype.
In other words, this trick can be used to favor an overload when a given type has a member function named func. In all other cases, the fallback is executed. A bit of templates, the deduction rules, and our beloved SFINAE do the rest.
noexcept
So far, so good. decltype is used in many codebases and you’ve probably already seen enough examples of it.
What about noexcept instead? Can we do something similar with it? Actually, yes. As an example, consider the case in which we want to provide two different implementations of the same function: the former throws exceptions in case of errors; the latter doesn’t make use of exceptions and returns error codes instead. The way we decide what function to use is by probing a given member and its noexcept-ness from the type we receive:
16 | Human Readable Magazine
Because of how std::enable_if_t works, the first function is se- lected only if T::func has the noexcept qualifier, and in this case the return type is void. Otherwise, the second function is picked up and the return type is int; that is our error code.
Again, remember that the operands of the noexcept operator are unevaluated and therefore the function call item.func() is only taken in consideration to probe its compile-time feature, and we have no actual side effects at runtime when entering the function.