Function checking if an integer type can fit a value of possibly different (integer) type
Asked Answered
H

7

21

Is it possible to create a templated function that checks if a primitive data type can fit a value of potentially different primitive data type? Let's limit the scope to integer types for the moment.

More precisely: Is it possible to create a "one fit all" templated functions yet without getting compiler warnings (boolean expression always true/false, signed/unsigned comparison, unused variable) and without disabling compiler warning checks? The functions should also limit as much as possible checks at runtime (all trivial cases should be excluded at compile time). If possible, I would prefer avoiding using extensions from C++11 and the like (unless a "quick" replacement for "old" C++ exists).

Note: "value" is not known at compile time, only its type.

Example of expected behaviour:

int main(int argc, char** argv) {
    for (int i = 1; i < argc; i++) {
        const int value = atoi(argv[i]);
        std::cout << value << ": ";
        std::cout << CanTypeFitValue<int8_t>(value) << " ";
        std::cout << CanTypeFitValue<uint8_t>(value) << " ";
        std::cout << CanTypeFitValue<int16_t>(value) << " ";
        std::cout << CanTypeFitValue<uint16_t>(value) << " ";
        std::cout << CanTypeFitValue<int32_t>(value) << " ";
        std::cout << CanTypeFitValue<uint32_t>(value) << " ";
        std::cout << CanTypeFitValue<int64_t>(value) << " ";
        std::cout << CanTypeFitValue<uint64_t>(value) << std::endl;
        }
    
}

Output:

./a.out 6 1203032847 2394857 -13423 9324 -192992929

6: 1 1 1 1 1 1 1 1

1203032847: 0 0 0 0 1 1 1 1

2394857: 0 0 0 0 1 1 1 1

-13423: 0 0 1 0 1 0 1 0

9324: 0 0 1 1 1 1 1 1

-192992929: 0 0 0 0 1 0 1 0

Test your code here or here.

Check the assembly generated here.

This question was inspired by this post

Hyades answered 20/6, 2013 at 21:37 Comment(5)
Mandatory viewing.Stepsister
What am I going to learn?Hyades
@KerrekSB Does the guy in that video have an account on Stackoverflow? That would be cool.Weekend
@0x499602D2: yesPlunk
Not answer, but just in case anyone else looking for a standard-ish solution for range checked type casting between numbers, check out boost::numeric_cast.Mcduffie
H
13

Using numeric_limits and types defined in stdint.h

More compact that my first solution, same efficiency.

Drawback: one additional header to be included.

#include <limits>
#include <stdint.h>

using std::numeric_limits;

template <typename T, typename U>
    bool CanTypeFitValue(const U value) {
        const intmax_t botT = intmax_t(numeric_limits<T>::min() );
        const intmax_t botU = intmax_t(numeric_limits<U>::min() );
        const uintmax_t topT = uintmax_t(numeric_limits<T>::max() );
        const uintmax_t topU = uintmax_t(numeric_limits<U>::max() );
        return !( (botT > botU && value < static_cast<U> (botT)) || (topT < topU && value > static_cast<U> (topT)) );        
    }

Assembly code generated (you can change T and U types)

Correctness test


Note: a constexpr version was written, but apparently it has some problems. See here and here.
Hyades answered 22/6, 2013 at 14:50 Comment(16)
The assembly is slightly longer if you go from int to short, but... wow. I'm impressed. You do lose constexpr unfortunately, but if it's important, it's easy to add that back.Plunk
@MooingDuck Thanks! :) I cannot see though in which way for the case int->short assembly generated is longer. And, sorry for asking so openly, do you think also the question could get a thump up?Hyades
already gave you +1. Here, I got it constexpr. (By longer I meant that CanTypeFitValue<short>(int); is 3 ops instead of just two)Plunk
@MooingDuck I meant +1 to the question, not the answer. Anyway thanks. How could it be only 2 ops? Thanks for constexpr version! I will edit the answer to include it!Hyades
If its converting to the same type or a bigger type, the value always fits, so that's zero ops. For the other cases, it checks if (ull)(value-min)<(ull)(max-min), and precalculates (ull)(max-min). If the value is -10, and converting to a signed char, then value-min = (-10)-(-128) = 118, and max-min = (127)-(-128) = 255, and since 118<255, it's valid. If value was -200, then value-min = (-200)-(-128) = -72. When cast to an unsigned long long (zero ops), that becomes ~1.84e+19, which is well over 255, and thus out of range. Obviously when min==0 then it can skip the first op.Plunk
@MooingDuck I still do not understand: if I convert from int to unsigned char, it does only one op, if I convert from int to signed char it does just one op more, and I cannot see how it could be less than that...Hyades
it's pretty tricky, I suggest making that a separate question. "Can someone explain how this assembly works and why it's generated from this C++?"Plunk
@MooingDuck But to me the assembly code generated is clear! I don't understand how you could reduce the ops. You speak about a cast of a negative number to unsigned long long that does not happen...Hyades
we must have had a communication error, I believe your code has the minimm number of ops. Casting from int to unsigned and similar merely changes which instructions are generated by the compiler, casts does not take an op.Plunk
Note that for casting to long long and unsigned long long the compiler generates more instructions, because the two instructions do not cover the case when it cannot be upcast. I coded a C++ version of what the compiler is generating for the general case here, and it incorrectly says that unsigned long long and signed long long both hold all values. Your code does not have this error.Plunk
I threw in a cast to intmax_t on the numeric_limits<T>::min( ) and a cast to uintmax_t on numeric_limits<T>::max( ) to get rid of the warnings. We know the intmax_t can hold the smallest integeral and uintmax_t can hold the largest and there should be no riskHenson
@Henson Which kind of warning were you getting? With which compiler?Hyades
@Hyades when T is unsigned I got a signed/unsigned comparison warning. So std::numeric_limits<T>::min() <= (intmax_t)std::numeric_limits<U>::min() became (intmax_t)std::numeric_limits<T>::min( ) <= (intmax_t)std::numeric_limits<U>::min() And the same but uintmax_t for the max's. The max min is 0 which is in the range of all integrals so casting to a signed will provable not change the value and with max the min max is >0 so a cast to uintmax_t is provably not going to change the value.Henson
@Henson I have removed the constexpr version, it has some problems. Which compiler are you using by the way?Hyades
@Hyades the big three, msvc 2015.2, clang 3.8, and gcc 5.2Henson
Even though I have been using this version here for a couple of years (thanks, by the way), I can confirm it does generate warnings when building on MSVC 2015. I just tested a x64 build and it will warn if the source value is uint64_t and dest is int. Also, I'd suggest you to edit your answer and put a link to stefan's. His awesome answer handles non integral types correctly. And, much like yours, can easily be transformed into constexprPhilologian
H
9

Using the features of C++14 (leave out constexpr for C++11 compatibility) and use of templates, this is what I came up with:

https://ideone.com/OSc9CI (updated version: now also accepts unsigned to signed, short and beautiful)

This basically uses std::enable_if extensively with type_traits std::is_unsigned and std::is_integral. Best to read from bottom up (as the decision tree builds up from there).

Obviously this is nearly all done compile time, so assembly should be fairly small.

This solution can handle integral and floating point target types as well as integral and floating point original types.

If the check isn't trivial (i.e. bounds of data type have to be checked), the actual_type value n is casted to typename std::common_type<target, actual_type>::type statically.

Every decision is_integral and is_unsigned and is_same is done at compile time, so no overhead from this at runtime. The check boils down to some lower_bound(target) <= value and / or value <= upper_bound(target) after the types are casted to a common type (to avoid warnings and prevent overflows).

#include <cmath> // necessary to check for floats too
#include <cstdint> // for testing only
#include <iomanip> // for testing only
#include <iostream> // for testing only
#include <limits> // necessary to check ranges
#include <type_traits> // necessary to check type properties (very efficient, compile time!)

// the upper bound must always be checked
template <typename target_type, typename actual_type>
constexpr bool test_upper_bound(const actual_type n)
{
   typedef typename std::common_type<target_type, actual_type>::type common_type;
   const auto c_n = static_cast<common_type>(n);
   const auto t_max = static_cast<common_type>(std::numeric_limits<target_type>::max());
   return ( c_n <= t_max );
}

// the lower bound is only needed to be checked explicitely in non-trivial cases, see the next three functions
template <typename target_type, typename actual_type>
constexpr typename std::enable_if<!(std::is_unsigned<target_type>::value) && !(std::is_unsigned<actual_type>::value), bool>::type
test_lower_bound(const actual_type n)
{
   typedef typename std::common_type<target_type, actual_type>::type common_type;
   const auto c_n = static_cast<common_type>(n);
   const auto t_min_as_t = std::numeric_limits<target_type>::lowest();
   const auto t_min = static_cast<common_type>(t_min_as_t);
   return (c_n >= t_min);
}

// for signed target types where the actual type is unsigned, the lower bound is trivially satisfied.
template <typename target_type, typename actual_type>
constexpr typename std::enable_if<!(std::is_unsigned<target_type>::value) &&(std::is_unsigned<actual_type>::value), bool>::type
test_lower_bound(const actual_type n)
{
    return true;
}
    
// for unsigned target types, the sign of n musn't be negative
// but that's not an issue with unsigned actual_type
template <typename target_type, typename actual_type>
constexpr typename std::enable_if<std::is_integral<target_type>::value &&
                        std::is_unsigned<target_type>::value &&
                        std::is_integral<actual_type>::value &&
                        std::is_unsigned<actual_type>::value, bool>::type
test_lower_bound(const actual_type)
{
   return true;
}

// for unsigned target types, the sign of n musn't be negative
template <typename target_type, typename actual_type>
constexpr typename std::enable_if<std::is_integral<target_type>::value &&
                        std::is_unsigned<target_type>::value &&
                        (!std::is_integral<actual_type>::value ||
                         !std::is_unsigned<actual_type>::value), bool>::type
test_lower_bound(const actual_type n)
{
   return ( n >= 0 );
}

// value may be integral if the target type is non-integral
template <typename target_type, typename actual_type>
constexpr typename std::enable_if<!std::is_integral<target_type>::value, bool>::type
test_integrality(const actual_type)
{
   return true;
}

// value must be integral if the target type is integral
template <typename target_type, typename actual_type>
constexpr typename std::enable_if<std::is_integral<target_type>::value, bool>::type
test_integrality(const actual_type n)
{
   return ( (std::abs(n - std::floor(n)) < 1e-8) || (std::abs(n - std::ceil(n)) < 1e-8) );
}

// perform check only if non-trivial
template <typename target_type, typename actual_type>
constexpr typename std::enable_if<!std::is_same<target_type, actual_type>::value, bool>::type
CanTypeFitValue(const actual_type n)
{
   return test_upper_bound<target_type>(n) &&
          test_lower_bound<target_type>(n) &&
          test_integrality<target_type>(n);
}


// trivial case: actual_type == target_type
template <typename actual_type>
constexpr bool CanTypeFitValue(const actual_type)
{
   return true;
}

int main()
{
   int ns[] = {6, 1203032847, 2394857, -13423, 9324, -192992929};
   for ( const auto n : ns )
   {
      std::cout << std::setw(10) << n << "\t";
      std::cout << " " << CanTypeFitValue<int8_t>(n);
      std::cout << " " << CanTypeFitValue<uint8_t>(n);
      std::cout << " " << CanTypeFitValue<int16_t>(n);
      std::cout << " " << CanTypeFitValue<uint16_t>(n);
      std::cout << " " << CanTypeFitValue<int32_t>(n);
      std::cout << " " << CanTypeFitValue<uint32_t>(n);
      std::cout << " " << CanTypeFitValue<int64_t>(n);
      std::cout << " " << CanTypeFitValue<uint64_t>(n);
      std::cout << " " << CanTypeFitValue<float>(n);
      std::cout << " " << CanTypeFitValue<double>(n);
      std::cout << "\n";
   }
   std::cout << "\n";
   unsigned long long uss[] = {6, 1201146189143ull, 2397, 23};
   for ( const auto n : uss )
   {
      std::cout << std::setw(10) << n << "\t";
      std::cout << " " << CanTypeFitValue<int8_t>(n);
      std::cout << " " << CanTypeFitValue<uint8_t>(n);
      std::cout << " " << CanTypeFitValue<int16_t>(n);
      std::cout << " " << CanTypeFitValue<uint16_t>(n);
      std::cout << " " << CanTypeFitValue<int32_t>(n);
      std::cout << " " << CanTypeFitValue<uint32_t>(n);
      std::cout << " " << CanTypeFitValue<int64_t>(n);
      std::cout << " " << CanTypeFitValue<uint64_t>(n);
      std::cout << " " << CanTypeFitValue<float>(n);
      std::cout << " " << CanTypeFitValue<double>(n);
      std::cout << "\n";
   }
   std::cout << "\n";
   float fs[] = {0.0, 0.5, -0.5, 1.0, -1.0, 1e10, -1e10};
   for ( const auto f : fs )
   {
      std::cout << std::setw(10) << f << "\t";
      std::cout << " " << CanTypeFitValue<int8_t>(f);
      std::cout << " " << CanTypeFitValue<uint8_t>(f);
      std::cout << " " << CanTypeFitValue<int16_t>(f);
      std::cout << " " << CanTypeFitValue<uint16_t>(f);
      std::cout << " " << CanTypeFitValue<int32_t>(f);
      std::cout << " " << CanTypeFitValue<uint32_t>(f);
      std::cout << " " << CanTypeFitValue<int64_t>(f);
      std::cout << " " << CanTypeFitValue<uint64_t>(f);
      std::cout << " " << CanTypeFitValue<float>(f);
      std::cout << " " << CanTypeFitValue<double>(f);
      std::cout << "\n";
   }
}

This (new) version quickly decides (at compile time!) if checks are needed (concerning upper bound, lower bound and integrality) and uses the correct version (to avoid warnings about stupid >= 0 comparisons with unsigned types) (also at compile time). E.g. the integrality does not need to be checked if the target is float, the lower bound does not need to be checked if both types are unsigned etc.

The most obvious optimization (having equal types), is done with std::is_same.

This approach can also be extended to used-defined types with specialized templates. Checks such as std::is_integral will be negative on those types.

You can check that the assembler output is fairly small (except for the obvious case of floats) here or by invoking g++ with -S.

Hartman answered 27/6, 2013 at 11:39 Comment(14)
Interesting! So can you tell something more about the scope of your function? I mean, it solves the problem for integers and on top of that...Hyades
@Hyades can you please clarify your comment? Or was it about "this solves the problem for floats as well"?Hartman
Yeah, if it is just that, please put it in a short summaryHyades
@Hyades I thought that's what I did. Did you check the updated answer yet?Hartman
No I hadn't :) Ok, thanks for the update!, will read thoroughly within the end of the week...Hyades
@Hyades I added a pseudo code that hopefully visualizes the code.Hartman
@Antonio: I've rewritten my code (and the answer). It probably is much more intuitive now and does not use a massive decision tree. Please let me know if you like this or if you have question or remarks.Hartman
Hi Stefan, thanks for your efforts. I was thinking that many cases could be reduced to one if you used my answer to handle the case where both entries are integer, and where both entries are floating point with a very similar version (just in that case we do not need to check more than max value). I also suggest that you double check the assembly code generated using the link I provided (just put only one function instantiation in the main to see its code), just to guarantee it's always limited to 0-1-2 operations.Hyades
@Hyades There you go, updated. I'd say that's pretty much optimal. Floats are evil.Hartman
@Hyades Thanks for awarding me with the bounty. It's great to get such appreciation for having fun ;-)Hartman
Awesome answer. Produces pretty much the same assembly code as the accepted answer but can also handle non integral types. And by simply moving the value you assign to c_n in test_upper_bound directly to the return statement, your version can become constexpr (at least in c++14). Awesome!Philologian
@Hartman I tried the solution using the following input: CanTypeFitValue<int8_t>(uint32_t(120)), which returns 0 even though 120 fits into an int8_t. The assembler in this case is xorl %eax, %eax, so it will always return 0.Ankh
@Ankh given that this answer is eight years old, I won't go into debugging now. If you are convinced it doesn't work, please try to find out why and propose a fix.Hartman
@Ankh This is indeed an issue. It happens because the common type of int8_t and uint32_t is uint32_t, which messes with the check for the lower bound. I've updated the answer.Hartman
P
5

Certainly

template <typename T, typename U>
constexpr bool CanTypeFitValue(const U value)
{return ((value>U(0))==(T(value)>T(0))) && U(T(value))==value;}

//      (         part1         ) && (      part2      )

Basically, this has two parts. The first part confirms that if a sign change happens (casting unsigned to signed or vice-versa, that the sign information isn't lost. The second part simply checks if value is cast to a T and back, that it retains it's value, and no bits have been lost.

FYI I'm not certain this is enough to tell if the value is maintained, but can't immediately think of a case with primitives that would fail. Both my answer and Casey's answer should work on user-defined numeric-like types so long as they provide conversion operators both ways between T and U.

Here's proof that it passes the tests you post in the question.

Plunk answered 20/6, 2013 at 23:40 Comment(1)
Antonio, despite the word constexpr, all the solutions I see on this page work just fine with runtime variables too. You might mistunderstand what constexpr does.Plunk
D
4

I've used something similar in the past to determine if T can represent the value u of type U exactly (replace constexpr with inline make this C++03):

template <typename T, typename U>
constexpr bool CanTypeRepresentValue(const U value) {
    return ((value > U()) == (static_cast<T>(value) > T())) &&
           (value == static_cast<U>(static_cast<T>(value)));
}

This should work well for conversions between integer types, but conversions between integer and floating-point types - or conversions from a floating-point type to a narrow floating-point type - are rife with undefined behavior and need a lot of range checking.

Dagnah answered 20/6, 2013 at 23:8 Comment(7)
value doesn't need to be known at compile time - use of constexpr here just makes it possible to compute the result at compile time if it is.Dagnah
Edited your example into my ideone code, had to use cin instead of argc/argv (If you can pass command line arguments at ideone, I don't know how.)Dagnah
Let's say both variables are int, the compiler will understand at compile time that the expression is binded to be true?Hyades
@Hyades Sure. But don't take my word for it, see for yourself at gcc.godbolt.org.Dagnah
Thanks for linking to this great website! Now hold on to your chair :) I have tried putting also my code with different data types (like unsigned int to int, int to char, and it produces by far smaller assembly code... I cannot get to paste it because the url get too long, even for google to create a short url...Hyades
"This should work at least for all arithmetic types..." You probably meant the integral types. Conversions between different floating point types and floating point types to integral types are dangerous and can easily cause undefined behavior.Cadi
@Cadi Hard to tell if this was an innocent mistake, or just me being horribly wrong. (Correction: I looked at the old test code; this was me being horribly wrong.)Dagnah
S
2

In C++20 just use std::in_range

std::cout << std::in_range<int8_t>(value)   << " "
          << std::in_range<uint8_t>(value)  << " "
          << std::in_range<int16_t>(value)  << " "
          << std::in_range<uint16_t>(value) << " "
          << std::in_range<int32_t>(value)  << " "
          << std::in_range<uint32_t>(value) << " "
          << std::in_range<int64_t>(value)  << " "
          << std::in_range<uint64_t>(value) << '\n';

Anything in the std namespace is standard and not some "extension"

Steinberg answered 14/7, 2021 at 1:46 Comment(1)
Very cool! Also the new comparison functions available are very nice, looking forward to their availability in the compilers! en.cppreference.com/w/cpp/utility/intcmpHyades
H
1

I propose a solution using numeric_limits

#include <limits>
using std::numeric_limits;

template <typename T, typename U>
    bool CanTypeFitValue(const U value) {
        if (numeric_limits<T>::is_signed == numeric_limits<U>::is_signed) {
            if (numeric_limits<T>::digits >= numeric_limits<U>::digits)
                return true;
            else
                return (static_cast<U>(numeric_limits<T>::min() ) <= value && static_cast<U>(numeric_limits<T>::max() ) >= value);
        }
        else {
            if (numeric_limits<T>::is_signed) {
                if (numeric_limits<T>::digits > numeric_limits<U>::digits) //Not >= in this case!
                    return true;
                else
                    return (static_cast<U>(numeric_limits<T>::max() ) >= value);
            }
            else ///U is signed, T is not
                if (value < static_cast<U> (0) )
                    return false;
                else
                    if (numeric_limits<T>::digits >= numeric_limits<U>::digits)
                        return true;
                    else
                        return (static_cast<U>(numeric_limits<T>::max() ) >= value);
        }
    }

Tested here (Sorry for using atoi :) ).

Hyades answered 20/6, 2013 at 21:37 Comment(6)
That seems a wee bit over complicatedPlunk
Your code says I can cast 0.5 to an int safelyPlunk
@MooingDuck Antonio's did say "Let's limit the scope to integer types for the moment." in the OP.Dagnah
oh, so it does. I misremembered that as limiting to primitives.Plunk
@MooingDuck Yeah, it's very complicated, but testing it here, it produces the smallest assembly code... I wonder if there is any way to obtain a methods that combines efficiency and conciseness...Hyades
@MooingDuck Please check new answerHyades
T
-1

The most explicit way is probably to use SFINAE and a function for each type. Something like this:

#include <limits>


template <typename T>
bool CanTypeFitValue(int) {
    return false;
}

template <typename T>
bool CanSignedNumericTypeFitValue(int value) {
    return (value >= std::numeric_limits<T>::min() && 
            value <= std::numeric_limits<T>::max());
}

template <typename T>
bool CanUnsignedNumericTypeFitValue(int value) {
    return (value >= 0 && 
            static_cast<unsigned>(value) <= std::numeric_limits<T>::max());
}

template <> bool CanTypeFitValue<int8_t>(int value) { 
    return CanSignedNumericTypeFitValue<int8_t>(value); 
}
template <> bool CanTypeFitValue<uint8_t>(int value) {
    return CanUnsignedNumericTypeFitValue<uint8_t>(value); 
}
// .....
//template <> bool CanTypeFitValue<SomeUserClass * > { 
//    return impl_details(value);
//};

It's also commonly used in STL/Boost etc.

The main idea is the function can be defined along with user defined type.

Trismus answered 24/6, 2013 at 21:4 Comment(3)
I suggest you read more carefully the question, and you try to compile your code with the links given. This will give warnings of comparing signed against unsigned when T is of unsigned type. It will probably not work because of this, especially for numbers close to the limit, like an unsigned int 4,000,000,000. Finally, it's not clear to me why you reduced the template parameters, forcing you to implement the function for any kind of input type for value. And you forgot to specify value as name of the input variable in the function.Hyades
Yes you're right, this function will fail on input value close to the limits. I wrote this only to show the idea.Trismus
You have only followed up part of my comments. I also suggest that you test your code here, leaving more or less unmodified the main function. You can then share your code, with the share button.Hyades

© 2022 - 2024 — McMap. All rights reserved.