Hmm. Your solution is rather longish.

I had once started something along this lines (but only for signed types):

alias sbyte = SafeSigned!byte;
alias sword = SafeSigned!short;
alias sint  = SafeSigned!int;
alias slong = SafeSigned!long;
alias scent = SafeSigned!cent;

template isSafeSigned(T)
{
enum bool isSafeSigned = is(Unqual!T == sbyte) || is(Unqual!T == sword) || is(Unqual!T == sint) || is(Unqual!T == slong) || is(Unqual!T == scent);
}

template isNumericExt(T)
{
   enum bool isNumericExt = isNumeric!T || isSafeSigned!T;
}

struct SafeSigned(S) if(isIntegral!S && isSigned!S) // one of byte, short, int, long or cent
{
pure:
@safe:
@nogc:
nothrow:
   alias ST = this;
   enum S nan = S.min;

   static @property S min() { return -S.max; }
   static @property S max() { return S.max; }
   static @property S init() { return nan; }
   static @property S invalid() { return nan; }
   @property bool isNaN() { return val == nan; }
@property string stringof() { return isNaN ? "NaN" : val.stringof; }

   this(T)(const(T) x) if(isSafeSigned!T)
   {
      val = safeCast!S(x.val);
   }

   this(T)(const(T) x) if(isNumeric!T)
   {
      val = safeCast!S(x);
   }

   ST opAssign(T)(const(T) x) if(isNumericExt!T)
   {
      return this(x);
   }

   bool opEquals(T)(const(T) x) const if(isNumericExt!T)
   {
      static if(isSafeSigned!T)
         return (isNaN || x.isNaN) ? false : val == x.val;
      else static if(isFloatingPoint!T)
         return (isNaN || x.isNaN) ? false : val == x;
      else static if(T.sizeof < S.sizeof)
         return isNaN ? false : val == cast(S)x;
      else static if(isSigned!T) // T has bigger range
         return isNaN ? false : cast(T)val == x;
      else // overlap
         return (val < 0 || x > max) ? false : (cast(T)val == x);
   }

   ssign_t opCmp(T)(const(T) x) const if(isNumericExt!T)
   {
      static if(isSafeSigned!T)
         return (isNaN || x.isNaN) ? NaN : val.opCmp(x.val);
      else static if(isFloatingPoint!T)
         return (isNaN || x.isNaN) ? NaN : val.opCmp(x);
      else static if(T.sizeof < S.sizeof)
         return isNaN ? NaN : val.opCmp(cast(S)x);
      else static if(isSigned!T) // T has bigger range
         return isNaN ? NaN : (cast(T)val).opCmp(x);
      else // overlap
return isNaN ? NaN : (val < 0) ? -1 : (x > max) ? 1 : (cast(T)val).opCmp(x);
   }

   T opCast(T)() const if(isNumericExt!T)
   {
      static if(Unqual!T == Unqual!S)
         return val;
      else static if(is(Unqual!T == Unqual!ST))
         return this; // not really a cast
      else static if(is(Unqual!T == bool))
         return (isNaN || val == 0) ? false : true;
      else static if(isFloatingPoint!T)
         return isNaN ? invalid!T : cast(T)val;
      else // SafeSigned or Integral
return (isNaN || val < T.min || val > T.max) ? invalid!T : cast(T)val;
   }

   ST opOpAssign(string op, T)(const(T) x) if(isNumericExt!T &&
(op == "+" || op == "-" || op == "*" || op == "/" || op == "%" || op == "^^" || op == "&" || op == "|" || op == "^" || op == "<<" || op == ">>" || op == ">>>"))
   {
      if(isNaN || x.isNaN) return this;
      static if(isSafeSigned!T)
      {
         return mixin("this "~op~"= x.val");
      }
      else static if(op == "^^")
      {
         if(x < 2)
         {
            if(x==0) val = 1;
            else static if(isSigned!T)
            {
if(x < 0 && abs(val) != 1) val = nan; // no inverse is integral else if(even(x)) val = 1; // x is negative and abs(val) == 1
            }
         }
         else
         {
            auto r = abs(val);
            if(r > 1)
            {
if(x > byte.max || r >= (S(1)<<(S.sizeof<<2)) || x > (maxpow(r)>>(5-bitlen(S.sizeof))))
                  val = nan;
               else
               {
                  r ^^= x;
                  if(r > max) val = nan;
else val = (val < 0 && odd(exp)) ? -cast(S)r : cast(S)r;
               }
            }
            else if(val == -1 && even(x)) val = 1;
         }
         return this;
      }
      else static if(op == "/" || op == "%")
      {
         if(!x) val = nan;
         else mixin("val "~op~"= x");
         return this;
      }
      else static if(op == "&" || op == ">>" || op == ">>>")
      {
         mixin("val "~op~"= x");
         return this;
      }
      else static if(Unqual!T == Unqual!S || T.sizeof < S.sizeof)
      {
         static if(op == "|" || op == "^")
         {
            mixin("val "~op~"= x");
         }
         else static if(op == "+" || op == "-" || op == "<<")
         {
            mixin("val "~op~"= x");
            asm { jno ok; }
            val = nan; // overflow, set to NaN
         }
         else static if(op == "*")
         {
            val *= x;
            asm { jnc ok; }
            val = nan; // overflow, set to NaN
         }
         ok:
         return this;
      }
      else // T has bigger range
      {
if(x >= min && x <= max) return mixin("val "~op~"= cast(S)x"); static if(op == "*" || op == "|" || op == "^" || op == "<<")
         {
            // will be definitely out of range
            val = nan;
         }
         else static if(isSigned!T)
         {
            static if(op == "+" || op == "-")
            {
mixin("val = cast(ST)( cast(SafeSigned!T)val "~op~" x )");
            }
         }
         else static if(op == "+")
         {
            val = (val<0) ? cast(ST)(x + cast(T)(-val)) : nan;
         }
         else static if(op == "-")
         {
            val = (val>0) ? -cast(ST)(x - cast(T)val) : nan;
         }
         return this;
      }
   }

// for non-commutative operations overload only the left side operator:
   ST opBinary(string op, T)(const(T) x) if((isNumericExt!T) &&
(op == "/" || op == "%" || op == "^^" || op == "<<" || op == ">>" || op == ">>>"))
   {
      static if(isFloatingpoint!T)
      {
         T r = this;
         mixin("r "~op~"= x");
         return cast(ST)r;
      }
      ST r = this;
      return mixin("r "~op~"= x");
   }

// for commutative operations use the bigger type as result type ST opBinary(string op, T)(const(T) x) if(((isSafeSigned!T && T.sizeof <= S.sizeof) || isSuperset(S, T)) && (op == "+" || op == "*" || op == "&" || op == "|" || op == "^"))
   {
      ST r = this;
      return mixin("r "~op~"= x");
   }
ST opBinaryRight(string op, T)(const(T) x) if((isSafeSigned!T || isIntegral!T) && (T.sizeof < S.sizeof) && (op == "+" || op == "*" || op == "&" || op == "|" || op == "^"))
   {
      ST r = this;
      return mixin("r "~op~"= x");
   }

   ST opUnary(string op)() if(op == "++" || op == "--")
   {
if(!isNaN) mixin(op~"val"); // ST.max+1 == ST.min-1 == NaN, so no extra conditions are necessary
      return this;
   }
private:
   S val = nan;
}

/// calculate the maximal power of the value that would fit in an ucent
/// for smaller types simply shift down the result
/// (e.g. divide by 4 to calc the maxpower that would fit in an uint)
ubyte maxpow(const ulong a) pure @safe @nogc nothrow
{
   assert(a>1); // no useful maxpower exists for 0 and 1
static immutable ubyte[14] mp = [ 127, 80, 63, 55, 49, 45, 43, 40, 38, 37, 35, 34, 33, 32 ]; return (a<139) ? ((a<31) ? ((a<20) ? ((a<16) ? mp[cast(ubyte)a-2] : ((a<18) ? 31 : 30)) : ((a<24) ? ((a<22) ? 29 : 28) : ((a<27) ? 27 : 26))) : ((a<57) ? ((a<41) ? ((a<35) ? 25 : 24) : ((a<48) ? 23 : 22)) : ((a<85) ? ((a<69) ? 21 : 20) : ((a<107) ? 19 : 18)))) : ((a<7132) ? ((a<566) ? ((a<256) ? ((a<185) ? 17 : 16) : ((a<371) ? 15 : 14)) : ((a<1626) ? ((a<921) ? 13 : 12) : ((a<3184) ? 11 : 10))) : ((a<2642246) ? ((a<65536) ? ((a<19113) ? 9 : 8) : ((a<319558) ? 7 : 6)) : ((a<4294967296) ? ((a<50859009) ? 5 : 4) : ((a<6981463658332) ? 3 : 2))));
}

/// return true if T can represent every possible value of U.
/// e.g. isSuperset!(real, long) is false on systems with real.mant_dig < 63, else true
template isSuperset(T, U) if(isNumeric!T && isNumeric!U)
{
   static if(isIntegral!T)
   {
static if(isIntegral!U) // unsigned >= unsigned or signed > unsigned or signed >= signed enum bool isSuperset = is(Unqual!T == Unqual!U) || ((isSigned!T || isUnsigned!U) && (T.sizeof > U.sizeof)); else // floatingpoint types can't be represented by integrals in general
         enum bool isSuperset = false;
   }
   else // T is floatingpoint
   {
      static if(isUnsigned!U)
         enum bool isSuperset = (T.mant_dig > U.sizeof<<3);
      else static if(isIntegral!U)
         enum bool isSuperset = (T.mant_dig >= U.sizeof<<3);
      else
         enum bool isSuperset = (T.mant_dig >= U.mant_dig);
   }
}

/// returns the number of the highest set bit +1 in the given value or 0 if no bit is set size_t bitlen(T)(const(T) a) pure @safe @nogc nothrow if(isUnsigned!T)
{
static if(T.sizeof <= size_t.sizeof) // ubyte, ushort or uint, maybe even ulong
   {
      return a ? bsr(a)+1 : 0;
   }
   else static if(T.sizeof == 8) // ulong if size_t == uint
   {
      return a ? a>>32 ? bsr(a>>32)+33 : bsr(a)+1 : 0;
   }
}

/// get the absolute value of x as unsigned type. always succeeds, even for T.min Unsigned!T abs(T)(const(T) x) pure @safe @nogc nothrow if(isIntegral!T)
{
   static if(isSigned!T) if(x < 0) return -x;
   return x;
}

/// test if the lowest bit is set (also commonly used on signed values)
bool odd(T)(const(T) a) pure @safe @nogc nothrow if(isIntegral!T)
{ return !!(a&1); }

/// consider T.max to be NaN of unsigned types
/// and T.min to be NaN of signed types
@property bool isNaN(T)(const(T) x) pure @safe @nogc nothrow if(isIntegral!T)
{
   static if(isSigned!T)
      return x == T.min;
   else // unsigned
      return x == T.max;
}

/// add a property to numeric types that can be used as return value if a result is out of bounds
template invalid(T) if(isNumeric!T)
{
   static if(isFloatingPoint!T)
      @property enum invalid = T.init;
   else static if(isSigned!T)
      @property enum invalid = T.min;
   else // unsigned
      @property enum invalid = T.max;
}

/// returns the save (not invalid) minimum value for the given type
template smin(T) if(isNumeric!T)
{
   static if(isFloatingPoint!T)
@property enum smin = -T.max; // T.min is the smallest representable positive value!!
   else static if(isSigned!T)
      @property enum smin = T.min+1;
   else // unsigned
      @property enum smin = T.min;
}

/// returns the save (not invalid) maximum value for the given type
template smax(T) if(isNumeric!T)
{
   static if(isUnsigned!T)
      @property enum smax = T.max-1u;
   else
      @property enum smax = T.max;
}

/// return true if the numeric value x is in the range of the target type
/// - no matter if low bits get lost in the process
bool fitIn(T, U)(const(U) x) pure @safe @nogc nothrow if(isNumeric!T && isNumeric!U)
{
   static if(isSuperset!(T, U)) return true;
   else static if(isIntegral!T)
   {
      static if(isIntegral!U)
      {
         static if(T.min > U.min) if(x < T.min) return false;
         static if(T.max < U.max) if(x > T.max) return false;
         return true;
      }
      else return x >= T.min && x <= T.max;
   }
   else // T is floatingpoint
   {
      static if(isIntegral!U) return true;
else return x.isNan || x.isInv || (x >= -T.max && x <= T.max); // return false if a value would be converted to infinity
   }
}

/// returns x if it fits in the target types range, else invalid!T
T safeCast(T, U)(const(U) x) pure @safe @nogc nothrow if(isNumeric!T && isNumeric!U)
{
   return (!x.isNaN && x.fitIn!T) ? cast(T)x : invalid!T;
}

unittest
{
   int a = -128;
   auto b = saveCast!byte(a);
   assert(is(typeof(b) == byte));
   assert(b == -128);
   auto c = saveCast!ushort(a);
   assert(is(typeof(c) == ushort));
   assert(c.isNaN);
   real d = 1.0L;
   assert(saveCast!float(d) == 1.0f);
   d /= 3;
   assert(saveCast!float(d).isNaN); // would lose accuracy
}


Reply via email to