An explicit cast for narrowing numeric conversions 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 &lt;cassert&gt;
#include &lt;exception&gt;
#include &lt;iostream&gt;
#include &lt;type_traits&gt;

namespace details {

template &lt;typename T, typename U&gt;
constexpr bool is_same_signedness = std::is_signed&lt;T&gt;::value == std::is_signed&lt;U&gt;::value;

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

template &lt;typename T, typename U&gt;
constexpr bool static_cast_changes_value(U u) noexcept
{
    const auto t = static_cast&lt;T&gt;(u);

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

    if (std::is_signed&lt;T&gt;::value != std::is_signed&lt;U&gt;::value &amp;&amp; ((t &lt; T{}) != (u &lt; U{})))
        return true;

    return false;
}

} // namespace details

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

struct narrowing_error : public std::exception {};

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

    return static_cast&lt;T&gt;(u);
}
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 &lt;typename T, typename U&gt;
constexpr bool can_fully_represent =
    std::is_same&lt;T, U&gt;::value ||
    ( std::is_integral&lt;T&gt;::value &amp;&amp; std::is_integral&lt;U&gt;::value &amp;&amp;
        ( ( std::is_signed&lt;T&gt;::value &amp;&amp; sizeof(T) &gt;  sizeof(U) ) ||
          ( is_same_signedness&lt;T, U&gt; &amp;&amp; sizeof(T) &gt;= sizeof(U) ) ) ) ||
    ( std::is_floating_point&lt;T&gt;::value &amp;&amp; std::is_floating_point&lt;U&gt;::value &amp;&amp; sizeof(T) &gt;= sizeof(U) );



Here's what I think should be sufficient:

template &lt;typename T, typename U&gt;
constexpr bool can_fully_represent =
       std::numeric_limits&lt;T&gt;::min() &lt;= std::numeric_limits&lt;U&gt;::min()
    &amp;&amp; std::numeric_limits&lt;T&gt;::max() &gt;= std::numeric_limits&lt;U&gt;::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&lt;T, U&gt;::value can be simplified to std::is_same_v&lt;T, U&gt;.