12

The C++ Core Guidelines has a narrow cast that throws if the cast changes the value. Looking at the microsoft implementation of the library:

// narrow() : a checked version of narrow_cast() that throws if the cast changed the value
template <class T, class U>
T narrow(U u) noexcept(false)
{
    T t = narrow_cast<T>(u);
    if (static_cast<U>(t) != u)
        gsl::details::throw_exception(narrowing_error());
    if (!details::is_same_signedness<T, U>::value && ((t < T{}) != (u < U{})))  // <-- ???
        gsl::details::throw_exception(narrowing_error());
    return t;
}

I don't understand the second if. What special case does it check for and why isn't static_cast<U>(t) != u enough?


For completeness:

narrow_cast is just a static_cast:

// narrow_cast(): a searchable way to do narrowing casts of values
template <class T, class U>
constexpr T narrow_cast(U&& u) noexcept
{
    return static_cast<T>(std::forward<U>(u));
}

details::is_same_signdess is what it advertises:

template <class T, class U>
struct is_same_signedness
    : public std::integral_constant<bool,
        std::is_signed<T>::value == std::is_signed<U>::value>
{
};
bolov
  • 72,283
  • 15
  • 145
  • 224
  • 2
    I don't know enough to make it an answer, but perhaps `narrow(-1)`? A `static_cast` back and forth would probably yield the same result (not sure if it's UB or not). – Kevin Oct 17 '18 at 21:12
  • 1
    It looks to me like *if they are not the same signedness and one is negative...* so you casting between *unsigned* and *signed* then it checks to see if *sign* information is lost? – Galik Oct 17 '18 at 21:24
  • I don't know exactly why it is written as it is, but just glancing at it I believe yours will (correctly? incorrectly?) return "true" for converting -0.0f to an integral zero, while the MS implementation will probably not compile for non-integral values. – Chuu Oct 17 '18 at 21:28

2 Answers2

17

This is checking for overflow. Lets look at

auto foo = narrow<int>(std::numeric_limits<unsigned int>::max())

T will be int and U will be unsigned int. So

T t = narrow_cast<T>(u);

will give store -1 in t. When you cast that back in

if (static_cast<U>(t) != u)

the -1 will convert back to std::numeric_limits<unsigned int>::max() so the check will pass. This isn't a valid cast though as std::numeric_limits<unsigned int>::max() overflows an int and is undefined behavior. So then we move on to

if (!details::is_same_signedness<T, U>::value && ((t < T{}) != (u < U{})))

and since the signs aren't the same we evaluate

(t < T{}) != (u < U{})

which is

(-1 < 0) != (really_big_number < 0)
==  true != false
==  true

So we throw an exception. If we go even farther and wrap back around using so that t becomes a positive number then the second check will pass but the first one will fail since t would be positive and that cast back to the source type is still the same positive value which isn't equal to its original value.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • Is the (paraphrased) statement "`int i = narrow_cast(std::numeric_limits::max());` will store `-1` in `i`" guaranteed by the standard? As far as I know it's implementation-defined, but will be true on almost every implementation. – John Ilacqua Oct 26 '18 at 00:23
  • 1
    Nitpicking: I don't believe the cast is truly _undefined_ behavior. I think you meant _unspecified_ behavior. Arithmetic signed integer overflows are undefined behavior, but a cast of an unrepresentable value is just unspecified. It's relevant because if it were actually undefined behavior, the optimizer would likely eliminate the tests altogether. – Adrian McCarthy Jun 18 '21 at 23:57
2
if (!details::is_same_signedness<T, U>::value && ((t < T{}) != (u < U{})))  // <-- ???

The above check is for making sure that differing signedness doesn't lead us astray.

The first part checks whether it might be an issue at all, and is included for optimization, so let's get to the point.

As an example, take UINT_MAX (the biggest unsigned int there is), and cast it to signed.

Assuming INT_MAX == UINT_MAX / 2 (which is very likely, though not quite guaranteed by the standard), the result will be (signed)-1, or just -1, a negative number.

While casting it back will result in the original value, thus it passes the first check, it is not itself the same value, and this check catches the error.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118