7
\$\begingroup\$

Inspired by gsl::narrow_cast, I created my own implementation with the addition of a couple of features:

  • a static assert on the types to ensure the cast is actually narrowing (if future changes to the code mean the cast is no longer narrowing, we don't want to still have a narrow_cast there)
  • the version without a runtime check still has an assert, so it's checked in debug mode
#include <cassert>
#include <exception>
#include <iostream>
#include <type_traits>

namespace details {

template <typename T, typename U>
constexpr bool is_same_signedness = std::is_signed<T>::value == std::is_signed<U>::value;

template <typename T, typename U>
constexpr bool can_fully_represent =
    std::is_same<T, U>::value ||
    ( std::is_integral<T>::value && std::is_integral<U>::value &&
        ( ( std::is_signed<T>::value && sizeof(T) >  sizeof(U) ) ||
          ( is_same_signedness<T, U> && sizeof(T) >= sizeof(U) ) ) ) ||
    ( std::is_floating_point<T>::value && std::is_floating_point<U>::value && sizeof(T) >= sizeof(U) );

template <typename T, typename U>
constexpr bool static_cast_changes_value(U u) noexcept
{
    const auto t = static_cast<T>(u);

    // this should catch most cases, but may miss dodgy unsigned to signed conversion or vice-versa
    if (static_cast<U>(t) != u)
        return true;

    if (std::is_signed<T>::value != std::is_signed<U>::value && ((t < T{}) != (u < U{})))
        return true;

    return false;
}

} // namespace details

// TODO: unchecked cast for types where some loss of precision (and therefore assertion failure) is expected?
template <typename T, typename U>
constexpr T narrow_cast(U&& u) noexcept
{
    static_assert(!details::can_fully_represent<T, U>, "we shouldn't be using narrow_cast for casts that aren't actually narrowing");
    assert(!details::static_cast_changes_value<T>(u));
    return static_cast<T>(std::forward<U>(u));
}

struct narrowing_error : public std::exception {};

template <typename T, typename U>
constexpr T narrow_cast_checked(U u)
{
    static_assert(!details::can_fully_represent<T, U>, "we shouldn't be using narrow_cast for casts that aren't actually narrowing");
    if (details::static_cast_changes_value<T>(u))
        throw narrowing_error();

    return static_cast<T>(u);
}
\$\endgroup\$
  • 1
    \$\begingroup\$ Do you really want to disallow non-narrowing uses of narrow_cast<>()? Consider that your narrow_cast<>() might be used in templates that want to allow safe narrowing of values, but might be instantiated with types such that it doesn't actually narrow. Then the use of that template will result in a compile-time error, unless it does something like if constexpr (is_narrower<T, U>()) t = narrow_cast<T>(u); else t = u; \$\endgroup\$ – G. Sliepen Oct 18 at 9:24
  • \$\begingroup\$ I considered that case, but I would like to be able to remove as many narrow casts as possible by changing types so the casts are no longer necessary, so the compile-time error is very useful for that. Original I was just going to stick with static_cast for that situation, but now I'm thinking it might be useful to have a maybe_narrow_cast to retain the runtime assert. \$\endgroup\$ – John Ilacqua Oct 18 at 9:45
  • \$\begingroup\$ Are you sure this code is intended to be C++11? You are using C++14 features. \$\endgroup\$ – L. F. Oct 19 at 11:42
  • \$\begingroup\$ @L.F. I was compiling with C++14, but when I went to post it I eyeballed it and thought that it was compatible with C++11, but didn't compile to check it. I'll change the tag. \$\endgroup\$ – John Ilacqua Oct 21 at 3:00
3
\$\begingroup\$

The first thing I noticed: you only support basic arithmetic types (integral and floating point). This isn't apparent from the name narrow_cast. Either enforce this with a static assert, or provide a mechanism to provide extensions for user defined types.

Here's what you write:

template <typename T, typename U>
constexpr bool can_fully_represent =
    std::is_same<T, U>::value ||
    ( std::is_integral<T>::value && std::is_integral<U>::value &&
        ( ( std::is_signed<T>::value && sizeof(T) >  sizeof(U) ) ||
          ( is_same_signedness<T, U> && sizeof(T) >= sizeof(U) ) ) ) ||
    ( std::is_floating_point<T>::value && std::is_floating_point<U>::value && sizeof(T) >= sizeof(U) );

Here's what I think should be sufficient:

template <typename T, typename U>
constexpr bool can_fully_represent =
       std::numeric_limits<T>::min() <= std::numeric_limits<U>::min()
    && std::numeric_limits<T>::max() >= std::numeric_limits<U>::max();

OK, maybe I overlooked some edge cases, but that's the idea.

The unchecked version uses a forwarding reference, whereas the check version does not. Try to keep the interface consistent.

std::is_same<T, U>::value can be simplified to std::is_same_v<T, U>.

\$\endgroup\$
  • \$\begingroup\$ Thanks for your comments. I agree with all of them, except that until I upgrade to C++17, I can't use the _v helpers. \$\endgroup\$ – John Ilacqua Oct 21 at 3:06
  • \$\begingroup\$ @JohnIlacqua Even if you can’t use _v, you can use is_same<T, U>{}. \$\endgroup\$ – L. F. Oct 21 at 9:24
  • \$\begingroup\$ I've used the braced syntax for type traits elsewhere in our codebase and gotten complaints from coworkers using MSVC that it doesn't compile. It'll accept is_same<T, U>{}() (it seems the implicit conversion to bool doesn't work for them?), but I think that's worse than ::value. \$\endgroup\$ – John Ilacqua Oct 21 at 22:38
  • \$\begingroup\$ @JohnIlacqua OK, that sounds like a bug. You can ask a question on Stack Overflow to include the details. Don't forget to post a minimal reproducible example :) \$\endgroup\$ – L. F. Oct 22 at 11:42
  • \$\begingroup\$ I decided to preempt the inevitable SO answer of "looks like an MSVC bug, file a bug report', and just filed a bug report: developercommunity.visualstudio.com/content/problem/793780/… \$\endgroup\$ – John Ilacqua Oct 25 at 3:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.