整型数据计算器

实际开发中常常遇见对一个整型变量进行+=、-=、*=、/=等操作,此时最担心的是计算完的值会超出原变量的数据类型所能表达的数据区间, 例如:

char a = 126;
a += 3;

// 或者

char b = -127;
b -= 3;

// 或者

unsigned int money = 1;
money -= 2;

对于上面的例子,最期望的结果是:

a += 3;
// 期望
a = 127;

b -= 3;
// 期望
b = -128;

money -= 2;
// 期望
money = 0;

试想如果超出类型所能表达的取值范围会发生什么样的后果,例如游戏中一个角色的金币变量的类型肯定是unsigned integral,像上面的例子money = 1,当扣减2个金币时那么结果是多少呢? 期望的是0,而实际结果为一个很大数,造成严重错误,这在要是在游戏线上运营时属于重大事故,造成货币通胀

还有一个例子,就是将运算完的结果作为循环的停止条件,这个问题就更恐怖了,莫名其妙,又不好查找问题。例如:

void CalculateResult(int param)
{
	unsigned int num = 1;
	num -= param;
	
	for(auto i = 0;i < num; ++i)
	{
		...
	}
}

上面的例子假设参数param是2,灾难发生了。那么既然有这么多严重的问题,可以写一个整型数据计算器(暂时这样称呼它),以后写代码就可以高枕无忧了

整型数据计算器


上面的例子描述了对一个整型数据+=、-=、*=、/=操作,整型数据计算器需要保证对原数计算完保证原数不会因为取值范围溢出而导致错误结果,这也就推导出:

  • 整型数据计算器与普通计算器不同

    普通计算器计算

      -127 - 3 = -130
    

    整型数据计算器计算

      int a = -3;
      char b = -127;
      // b += a;
      IntegralCaculatorIncrease(b,a);
    	
      b的值应该为-128
    
  • 整型数据计算器不满足交换律

    普通计算器计算

      -127 - 3 = -130
      // 交换成立
      -3 - 127= -130
    

    整型数据计算器计算

      int a = -3;
      char b = -127;
      // b += a;
      IntegralCaculatorIncrease(b,a);
    	
      b的值应该为-128
    	
      // 交换不成立
      int a = -3;
      char b = -127;
      // b += a;
      IntegralCaculatorIncrease(a,b);
    	
      a的值应该为-130
    
  • 整型计算器提供函数判断是否导致溢出

    有时上层可能需要知道对原数基本操作会不会导致溢出,例如:

      int a = -3;
      char b = -127;
      bool overflow = OverflowIncrease(b,a);
      if (overflow)
      {
          ...
      }
    

普通计算器


有时并不需要像整型数据计算器的功能,只是计算出正确的结果,和普通计算器一样,那么就可能导致返回的数据类型与输入的类型不一致的情况,这种情况最好使用auto

int a = -3;
char b = -127;

auto result = CaculatorAdd(a,b);

result的结果应该为-130

这个普通计算器返回的结果也只能表达在这个范围[int64::min,uint64::max]内的数据

普通计算器的计算结果有取值范围的约束,这也是和系统提供的计算器不同的地方,且只对整型使用,也是实现普通计算器的初衷

普通计算器可同时做加、减、乘、除运算

例如:

auto result = a + b * c - d;
// 期望更简化的使用计算器
auto result = Caculator(a) + b * c - d;
	
或者
	
auto result = a + Caculator(b) * c - d;

通过这个例子可得知普通计算器必须支持4个运算符重载

实现这个普通计算器的难点有如下:

  • 返回值不容易确定

    从上面分析得知返回值的取值范围[int64::min,uint64::max],也就意味着返回值要么是int64要么是uint64

  • 操作数两边符号类型不一致时需要确定以哪个符号类型为基准

    当运算的两个变量一个是有符号的,一个是无符号的,此时需要确定的是以有符号的为基准还是以无符号的为基准,这种情况只有参数是int64整型或者uint64整型时才会出现

目前还没有好的实现思路,各位同行如果有好的实现思路欢迎Email交流

整型计算器代码实现


下面列出了整型数据自增、自减、自乘、自除代码:

#include <iostream>
#include <limits>
#include <map>
#include <type_traits>

// trait type that stripping const、volatile、reference
template <typename T>
struct TraitTypeStrippingCVR
{
	using RealType = std::remove_reference_t<std::decay_t<T>>;
};

template <typename T>
auto Abs(T&& t)noexcept
{
	using RealType = typename TraitTypeStrippingCVR<T>::RealType;
	using UnsignedRealType = typename std::make_unsigned<RealType>::type;
	if (t >= 0)
	{
		return UnsignedRealType(t);
	}

	constexpr RealType min_val = (std::numeric_limits<RealType>::min)();

	if (t == min_val)
	{
		return UnsignedRealType(t);
	}

	return UnsignedRealType((~t) + 1);
}

namespace integral_calculator
{
	using SignedInt64 = long long;
	using UnsignedInt64 = unsigned long long;

	class SafeIntegralCalculator
	{
	private:
		template<typename T1, typename T2,
			typename = typename std::enable_if_t
			<
			std::is_integral_v<typename TraitTypeStrippingCVR<T1>::RealType>&&
			std::is_integral_v<typename TraitTypeStrippingCVR<T2>::RealType> &&
			!std::is_same<typename TraitTypeStrippingCVR<T1>::RealType, bool>::value &&
			!std::is_same<typename TraitTypeStrippingCVR<T2>::RealType, bool>::value
			>
		>
			static auto SafeSum(T1&& augend, T2&& addend) noexcept
		{
			using T1RealType = typename TraitTypeStrippingCVR<T1>::RealType;
			using UnsignedT1RealType = typename std::make_unsigned<T1RealType>::type;
			using T2RealType = typename TraitTypeStrippingCVR<T2>::RealType;
			using UnsignedT2RealType = typename std::make_unsigned<T2RealType>::type;

			constexpr T1RealType max_val = std::numeric_limits<T1RealType>::max();
			constexpr T1RealType min_val = std::numeric_limits<T1RealType>::min();

			if (addend == 0)
			{
				return augend;
			}

			// add
			if (addend > 0)
			{
				// char a = 125;
				// int b = 1000;
				// char sum = (a + b) = 127
				if (augend >= 0)
				{
					T1RealType tmp = max_val - augend;
					if (addend >= tmp)
					{
						return max_val;
					}
				}
				// char a = -128;
				// int b = 1000;
				// char sum = (a + b) = 127
				else
				{
					UnsignedT1RealType t1_abs = Abs(augend);
					if (addend >= (max_val + t1_abs))
					{
						return max_val;
					}
				}
			}
			// sub
			else
			{
				// char a = -100;
				// char b = -125;
				// char sum = (a + b) = -128
				if (augend < 0)
				{
					// char a = -10;
					// int b = -128;
					// char sum = (a + b) = -128
					UnsignedT1RealType t1_abs = Abs(augend);
					T1RealType expected = t1_abs + min_val;
					if (addend <= expected)
					{
						return min_val;
					}
				}
				else
				{
					// unsigned char a = 255;
					// int b = -1000;
					// char sum = (a + b) = -128
					UnsignedT1RealType unsigned_t1 = augend;
					UnsignedT1RealType t1_min_abs = Abs(min_val);
					UnsignedT1RealType expected = unsigned_t1 + t1_min_abs;

					UnsignedT2RealType t2_abs = Abs(addend);
					if (t2_abs >= expected)
					{
						return min_val;
					}
				}
			}

			return T1RealType(augend + addend);
		}

		template<typename T1, typename T2,
			typename = typename std::enable_if_t
			<
			std::is_integral_v<typename TraitTypeStrippingCVR<T1>::RealType>&&
			std::is_integral_v<typename TraitTypeStrippingCVR<T2>::RealType> &&
			!std::is_same<typename TraitTypeStrippingCVR<T1>::RealType, bool>::value &&
			!std::is_same<typename TraitTypeStrippingCVR<T2>::RealType, bool>::value
			>
		>
			static auto SafeMultiply(T1&& multiplicand, T2&& multiplier) noexcept
		{
			using T1RealType = typename TraitTypeStrippingCVR<T1>::RealType;
			using UnsignedT1RealType = typename std::make_unsigned<T1RealType>::type;
			using T2RealType = typename TraitTypeStrippingCVR<T2>::RealType;
			using UnsignedT2RealType = typename std::make_unsigned<T2RealType>::type;

			constexpr T1RealType max_val = std::numeric_limits<T1RealType>::max();
			constexpr T1RealType min_val = std::numeric_limits<T1RealType>::min();

			if (multiplicand == 0 || multiplier == 0)
			{
				return T1RealType(0);
			}

			if (multiplier > 0)
			{
				// char a = 125;
				// int b = 1000;
				// char sum = (a * b) = 127
				if (multiplicand >= 0)
				{
					auto multi = max_val / multiplicand;
					if (multiplier > multi)
					{
						return max_val;
					}
				}
				// char a = -2;
				// int b = 1000;
				// char sum = (a * b) = -128
				else
				{
					auto multi = min_val / multiplicand;
					if (multiplier > multi)
					{
						return min_val;
					}
				}
			}
			else
			{
				if (multiplicand < 0)
				{
					// char a = -128;
					// int b = -128;
					// char sum = (a * b) = 127
					UnsignedT1RealType t1_abs = Abs(multiplicand);
					UnsignedT2RealType t2_abs = Abs(multiplier);
					auto expected_multi = max_val / t1_abs;
					if (t2_abs > expected_multi)
					{
						return max_val;
					}
				}
				else
				{
					// unsigned char a = 200;
					// char a = 64;
					// int b = -1000;
					// char sum = (a * b) = -128
					T1RealType expected_multi = min_val / multiplicand;
					if (multiplier < expected_multi)
					{
						return min_val;
					}
				}
			}

			return T1RealType(multiplicand * multiplier);
		}

		template<typename T1, typename T2,
			typename = typename std::enable_if_t
			<
			std::is_integral_v<typename TraitTypeStrippingCVR<T1>::RealType>&&
			std::is_integral_v<typename TraitTypeStrippingCVR<T2>::RealType> &&
			!std::is_same<typename TraitTypeStrippingCVR<T1>::RealType, bool>::value &&
			!std::is_same<typename TraitTypeStrippingCVR<T2>::RealType, bool>::value
			>
		>
			static auto SafeDivide(T1&& dividend, T2&& divisor) throw()
		{
			using T1RealType = typename TraitTypeStrippingCVR<T1>::RealType;
			using UnsignedT1RealType = typename std::make_unsigned<T1RealType>::type;
			using T2RealType = typename TraitTypeStrippingCVR<T2>::RealType;
			using UnsignedT2RealType = typename std::make_unsigned<T2RealType>::type;

			constexpr T1RealType max_val = std::numeric_limits<T1RealType>::max();
			constexpr T1RealType min_val = std::numeric_limits<T1RealType>::min();

			if (dividend == 0)
			{
				return T1RealType(0);
			}

			if (divisor == 0)
			{
				throw std::invalid_argument("bad divisor,it is 0");
			}

			// note: when dividend is unsigned and divisor is less 0,then the result is 0
			if (std::is_unsigned_v<T1> && divisor < 0)
			{
				// unsigned char a = 12;
				// char b = -1;
				// a /= b;
				return T1RealType(0);
			}

			if (dividend < 0 && divisor < 0)
			{
				// char a = -128;
				// char b = -1;
				// a /= b;
				UnsignedT1RealType t1_abs = Abs(dividend);
				UnsignedT2RealType t2_abs = Abs(divisor);
				UnsignedT1RealType real_reasult = t1_abs / t2_abs;
				if (real_reasult > max_val)
				{
					return max_val;
				}
			}

			return T1RealType(dividend / divisor);

		}

	public:
		template<typename T1, typename T2>
		static void SafeAutoIncrease(T1&& augend, T2&& addend)
		{
			augend = SafeSum(std::forward<T1>(augend), std::forward<T2>(addend));
		}

		template<typename T1, typename T2>
		static void SafeAutoMultiply(T1&& multiplicand, T2&& multiplier)
		{
			multiplicand = SafeMultiply(std::forward<T1>(multiplicand), std::forward<T2>(multiplier));
		}

		template<typename T1, typename T2>
		static void SafeAutoDivide(T1&& dividend, T2&& divisor)
		{
			dividend = SafeDivide(std::forward<T1>(dividend), std::forward<T2>(divisor));
		}
	};
}

这里需要特别注意自除运算里的这段代码:

// note: when dividend is unsigned and divisor is less 0,then the result is 0
if (std::is_unsigned_v<T1> && divisor < 0)
{
	// unsigned char a = 12;
	// char b = -1;
	// a /= b;
	return T1RealType(0);
}

当出现上述情况时返回结果按0处理,如有不同的处理意见欢迎发Email交流,或者可按照自己意愿自行修改,而且自除函数非异常安全