C++23: Narrowing contextual conversions to bool

Sandor Dargo - Jun 15 '22 - - Dev Community

In the previous article discussing new language features of C++23, we discussed if consteval. Today, we'll slightly discuss if constexpr and also static_assert. Andrzej Krzemieński proposed a paper to make life a bit easier by allowing a bit more implicit conversions. Allowing a bit more narrowing in some special contexts.

A quick recap

For someone less experienced with C++, let's start with recapitulating what the most important concepts of this paper represent.

static_assert

Something I just learned is that static_assert was introduced by C++11. I personally thought that it was a much older feature. It serves for performing compile-time assertions. It takes two parameters

  • a boolean constant expression
  • a message to be printed by the compiler in case of the boolean expression is false. C++17 made this message optional.

With static_assert we can assert the characteristics of types at compiler time (or anything else that is available knowledge at compile time.

#include <type_traits>

class A {
public:
// uncomment the following line to break the first assertion
// virtual ~A() = default;
};

int main() {
  static_assert(std::is_trivial_v<A>, "A is not a trivial type");
  static_assert(1 + 1 == 2);
}
Enter fullscreen mode Exit fullscreen mode

constexpr if

if constexpr is a feature introduced in C++17. Based on a constant expression condition, with constexpr if we can select and discard which branch to compile.

Take the following example from C++ Reference:

template<typename T>
auto get_value(T t)
{
    if constexpr (std::is_pointer_v<T>)
        return *t; // deduces return type to int for T = int*
    else
        return t;  // deduces return type to int for T = int
}
Enter fullscreen mode Exit fullscreen mode

If T is a pointer, then the template will be instantiated with the first branch and the else part be ignored. In case, it's a value, the if part will be ignored and the else is kept. if constexpr has been a great addition that helps us simplify SFINAE and whatever code that is using std::enable_if.

Narrowing

Narrowing is a type of conversion. When that happens the converted value is losing from its precision. Most often it's something to avoid, just like the Core Guidelienes says in ES.46.

Converting a double to an int, a long to an int, etc., are all narrowing conversions where you (potentially) lose data. In the first case, you lose the fractions and in the second, you might already store a number that is bigger than the target type can store.

Why would anyone want that implicitly?

But converting an int to a bool is also narrowing and that can be useful. When that happens 0 is converted to false, and anything else (including negative numbers) will result in true.

Let's see how the paper wants to change the status quo.

What is the paper proposing

In fact, the proposal of Andrzej might or might not change anything for you depending on your compiler and its version. On the other hand, it definitely makes the standard compiler compliant.

Wait, what?

Let's take the following piece of code.

template <std::size_t N>
class Array
{
  static_assert(N, "no 0-size Arrays");
  // ...
};

Array<16> a;
Enter fullscreen mode Exit fullscreen mode

According to - the pre-paper-acceptance version of - the standard, it should fail to compile because N which is 16 shouldn't be narrowed to bool. Still, if you compile the code with the major implementations, it will compile without any issue.

The paper updates the standard so that it matches this behaviour.

The other context where the paper changes the standard is if constexpr. Converting contextually a type to bool is especially useful with enums used as flags. Let's have a look at the following example:

enum Flags { Write = 1, Read = 2, Exec = 4 };

template <Flags flags>
int f() {
  if constexpr (flags & Flags::Exec) // should fail to compile due to narrowing
    return 0;
  else
    return 1;
}

int main() {
  return f<Flags::Exec>(); // when instantiated like this
}
Enter fullscreen mode Exit fullscreen mode

As the output of flags & Flags::Exec is an int, according to the standard it shouldn't be narrowed down to a bool, while the intentions of the coder are evident.

Earlier versions of Clang failed to compile this piece of code, you had to cast the condition to bool explicitly. Yet, later versions and all the other major compilers compiled the successfully.

There are 2 other cases where the standard speaks about "contextually converted constant expression of type bool", but the paper doesn't change the situation for those. For more details on that, check out the paper!

Conclusion

P1401R5 will not change how we code, it will not or just slightly change how compilers work. But it makes the major compilers compliant with the standard. It aligns the standard with the implemented behaviour and will officially let the compilers perform a narrowing conversion to bool in the contexts of a static_assert or if constexpr. Now we can avoid explicitly casting expressions to bool in these contexts without guilt. Thank you, Andrzej!

Connect deeper

If you liked this article, please

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .