c++c++11castingnarrowingcpp-core-guidelines

Understanding gsl::narrow implementation


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>
{
};

Solution

  • 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.