Home > OS >  Can I somehow elegantly forbid using unsingned variables in my template function?
Can I somehow elegantly forbid using unsingned variables in my template function?

Time:12-07

Consider this piece of code:

template <typename T>
T abs(const T& n)
{
    if (!std::is_signed<T>::value)
        throw logic_error;
    if (n < 0)
        return -n;
    return n;
}

I want to completely forbid the usage of my function with unsigned variables, since it doesn't make sense, and probably the user doesn't even know that he uses an unsigned variable. For example, I can somewhat avoid this problem:

template <typename T>
T abs(const T& n)
{
    if constexpr(!std::is_signed<T>::value)
        n  = "";
    if (n < 0)
        return -n;
    return n;
} 

But if I call abs(4u), the compiler errors are not very obvious. Something like "can't apply = const char[1] to double". Can I make it somehow more obvious? Or just make multiple overloads?

CodePudding user response:

#include <type_traits>

template <typename T>
T abs(T n)
{
  static_assert(std::is_signed<T>::value, "Type must be signed.");
  if (n < 0) { return -n; }
  return n;
}

int main()
{
    return abs(3u);
}

CodePudding user response:

1. concepts

Since C 20 you can use concepts for this.

If you are fine with integer only parameters you could use std::signed_integral:

#include <concepts>

template <std::signed_integral T>
T abs(const T& n)
{
    if (n < 0)
        return -n;
    return n;
}

If you want to also allow double, etc... you'd have to make your own concept:

template<class T>
concept signed_value = std::is_signed_v<T>;

template <signed_value T>
T abs(const T& n)
{
    if (n < 0)
        return -n;
    return n;
}

This would result in a compiler error like this if you try to use an unsigned type:

error: no matching function for call to 'abs(unsigned int&)'
note: candidate: 'template<class T>  requires  signed_integral<T> T abs(const T&)'

Which makes it very clear that abs() only accepts signed types from just the signature.


2. SFINAE

If C 20 is not available, you could use SFINAE, although the error messages you would get are quite cryptic:

template <class T>
std::enable_if_t<std::is_signed_v<T>, T> abs(const T& n)
{
    std::cout << "HELLO" << std::endl;
    if (n < 0)
        return -n;
    return n;
}

But this would result in an error message like this:

error: no matching function for call to 'abs(unsigned int&)'
note: candidate: 'template<class T> std::enable_if_t<is_signed_v<T>, T> abs(const T&)'
note:   template argument deduction/substitution failed:
    In substitution of 'template<bool _Cond, class _Tp> using enable_if_t = typename std::enable_if::type [with bool _Cond = false; _Tp = unsigned int]':
    required by substitution of 'template<class T> std::enable_if_t<is_signed_v<T>, T> abs2(const T&) [with T = unsigned int]'

So, you need to be comfortable with SFINAE errors for this one.


3. good old static_assert

Alternatively, you could also just add a static_assert to your function.
This won't be visible in the declaration at all though, only in the definition of the function.

template <class T>
T abs(const T& n)
{
    static_assert(std::is_signed_v<T>, "T must be signed!");
    if (n < 0)
        return -n;
    return n;
}

4. using std::abs()

C already provides a std::abs() implementation that handles all the potential edge cases for you, so if you can, I would recommend using that one instead.

Your code also contains a potential bug if you pass in the minimum value for a given int type, e.g. INT_MIN.

  • The smallest value a 32bit int can represent is -2147483648
  • But the largest it can represent is only 2147483647

So, a call to your abs() function with the minimal value, e.g. abs(-2147483648) would result in undefined behaviour (std::abs() has the same behaviour, but it's still worth pointing out).

  • Related