6
\$\begingroup\$

To summarize what I've been trying to do, I basically tried to make a safe narrow_cast operator, which casts to the Target type if and only if the value is unchanged (not narrowed) in the process, and throws an exception if the conversation will cause the value to change.

So I implemented this by making two templates, one for when std::numeric_limits of the Target type is specialized (by using std::numeric_limits<T>::is_specialized in conjunction with std::enable_if), and check if base is higher than the max value of target type of lower than the min value of target type, if it is, then throw an exception, elsewise, just do a static cast to the target.

And the second for when the numeric_limits are not specialized, by basically casting to the Target type, then casting back to the Base, and then comparing the old value with the new one. If both are the same, just return the Target casted value, if not, throw an exception.

But then, I got a response from the comments that my code might cause problems when Integer and Floating Point types are mixed, as even though a Floating point might fit inside an Integer range, it can still not be completely expressed by that type, so I stuck to ALWAYS using the second alternative.


Old Code [BAD, doesn't work properly when Integer and Floating Point types are mixed]

// utils.hh
// Contains globally used utility functions, templates, using declarations, etc.

#ifndef PHI_SRC_UTILS_HH
#define PHI_SRC_UTILS_HH

#include <string>
#include <vector>
#include <limits>

template <typename T>
using Vector = std::vector<T>;
using String = std::string;
using Size = std::size_t;

class bad_narrow_cast : public std::bad_cast
{
public:
    bad_narrow_cast(const char* message) : what_(message)
    {
    }

    char const *what() { return what_; }

private:
    char const *what_;
};

template <typename Target, typename Base> static inline
typename std::enable_if<std::numeric_limits<Target>::is_specialized,
Target>::type narrow_cast(Base const &base)
{
    if(base > static_cast<Base>(std::numeric_limits<Target>::max()) ||
        base < static_cast<Base>(std::numeric_limits<Target>::min()))
    {
        throw(bad_narrow_cast((String() + "Invalid narrowing conversation from type " +
        typeid(Target).name() + " to type " + typeid(Base).name()).c_str()));
    }

    return static_cast<Target>(base);
}

template <typename Target, typename Base> static inline
typename std::enable_if<!std::numeric_limits<Target>::is_specialized,
Target>::type narrow_cast(Base const &base)
{
    Target target = static_cast<Target>(base);
    Base narrowed_base = static_cast<Base>(target);

    if (base == narrowed_base)
        return target;

    throw(bad_narrow_cast((String() + "Invalid narrowing conversation from type " +
    typeid(Target).name() + " to type " + typeid(Base).name()).c_str()));
}

#endif

Fixed Code:

// utils.hh
// Contains globally used utility functions, templates, using declarations, etc.

#ifndef PHI_SRC_UTILS_HH
#define PHI_SRC_UTILS_HH

#include <string>
#include <vector>

#define UNUSED(x) (void)x

template <typename T>
using Vector = std::vector<T>;
using String = std::string;
using Size = std::size_t;

class bad_narrow_cast : public std::bad_cast
{
public:
    bad_narrow_cast(char const *message) : what_(message)
    {
    }

    char const *what() { return what_; }

private:
    char const *what_;
};

template <typename Target, typename Base> static inline
Target narrow_cast(Base const &base)
{
    Target target = static_cast<Target>(base);
    Base narrowed_base = static_cast<Base>(target);

    if (base == narrowed_base)
        return target;

    throw(bad_narrow_cast((String() + "Invalid narrowing conversation from type " +
    typeid(Target).name() + " to type " + typeid(Base).name()).c_str()));
}

#endif

But I'm still unsure if I'm doing things the right way, so, comments on the code is appreciated, it'd be really helpful to improve it more if possible

\$\endgroup\$
1
  • \$\begingroup\$ You could make use of the curly-brace style initialization, which already disallows narrowing conversions. \$\endgroup\$
    – JDługosz
    Jun 21, 2021 at 15:43

1 Answer 1

2
\$\begingroup\$

I've attempted this before, and it's a little more complicated than checking if the value round-trips correctly. Consider:

int x = -3;
unsigned int y = narrow_cast<unsigned int>(x);

This passes the round-trip test, but y has a different value than x. Before you know it, you'll be writing a lot of if constexpr with type traits in order to try to match the narrowing rules.

The Guidelines Standard Library has a narrow_cast that's explored in Understanding gsl::narrow implementation on Stack Overflow.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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