Page 12 - HRM-00-v1
P. 12

Y OU KNOW, THE C++ STANDARD
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 operand is:
An unevaluated operand is considered a full-expression.
In other words, the unevaluated operands are the operands of some operators of the language. They are expressions in all respects, but such that they are never evaluated. The reason is because those op- erators 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 evalu- ated 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 inter- esting.
What are these operators then? Up to C++17, there are four operators 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:
template<typename T>
auto inspect(int, T &&item) -> decltype(item.func(), void()) { /* ... */ }
template<typename T>
void inspect(char, T &&item) { /* ... */ }
template<typename T>
void inspect(T &&item) { inspect(0, std::forward<T>(item)); }
// ...
inspect(std::string{«example”});
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 giv- en 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 proba- bly 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 for- mer 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:
template<typename T>
auto inspect(int, T &&item) -> std::enable_if_t<noexcept(item.func())> {
// ...
throw; }
template<typename T>
int inspect(char, T &&item) {
// ...
return 0; }
template<typename T>
auto inspect(T &&item) { return inspect(0, std::forward<T>(item)); }
 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:
                          12 | Human Readable Magazine
 



























































   10   11   12   13   14