【问题标题】:C++ Template for safe integer casts用于安全整数转换的 C++ 模板
【发布时间】:2009-06-15 21:48:34
【问题描述】:

我正在尝试编写一个 C++ 模板函数,该函数将在不同整数类型、不同宽度和可能有符号/无符号不匹配之间的强制转换中引发整数溢出运行时异常。出于这些目的,我不关心从浮点类型转换为整数类型,也不关心其他对象到对象的转换。我想这样做而不必编写大量特殊情况代码。这是我目前拥有的:

template< typename T, typename R > void safe_cast( const T& source, R& result )
{
    // get the maximum safe value of type R
    R rMax = (R) ~0;
    if ( rMax < 0 ) // R is a signed type
    {
        // assume that we're on an 8-bit twos-compliment machine
        rMax = ~( 0x80 << ( ( sizeof( R ) - 1 ) * 8 ) );
    }

    if ( ( source & rMax  ) != source )
    {
        throw new IntegerOverflowException( source );
    }

    result = static_cast<R>( source );
}

这样正确有效吗?

编辑:由于各种原因 stl 不可用,所以我不能使用 std::numeric_limits,而 Boost 中的任何东西都是正确的。

【问题讨论】:

  • 但是您可以将所需的代码从 numeric_limits 复制到您自己的模板化助手中。将所有内容分配给 uint64(或任何允许的最大大小)并在该类型内进行比较。
  • 这可能行得通,但是在复制这样的代码时确实需要了解许可条款。除了可能违反条款外,人们还可能无意中“感染”他们的代码,就像 GPL 一样。在执行此类操作之前,请确保两个许可证都兼容。通常的“我不是律师”免责声明适用。
  • 不能使用STL的各种原因是什么?

标签: c++ templates casting integer-overflow


【解决方案1】:

您可以使用std::numeric_limits 模板以更优雅的方式获取任何基本类型的最小和最大安全值(以及大量其他信息),例如std::numeric_limits&lt;T&gt;::max()。您需要包含&lt;limits&gt;

参考:http://www.cplusplus.com/reference/std/limits/numeric_limits/

【讨论】:

  • 7 年后的这一编辑让我将赞成票改为反对票。因为@jpo38 添加的示例不起作用。示例对:From=int, To=unsigned。来源==-1。
  • 实际上,自从我编辑了这篇文章后,我就看到并在我的代码中修复了它。但是忘记更新编辑了……对不起。我现在抛出 if static_cast&lt;From&gt;(static_cast&lt;To&gt;( source ) ) != source (基本上,如果演员丢失了一些信息......这很好用。某种程度上类似于蒂姆在下面提出的建议。从签名到未签名时,没有参考最大/最小愿望失败和方式周围。
【解决方案2】:

提升是一种选择吗?如果是这样,请尝试boost::numeric_cast<>。它似乎提供了您正在寻找的特征。

【讨论】:

    【解决方案3】:

    我认为无论您是否使用二进制补码,这些现在都有效。请在使用前进行广泛的测试。他们给出以下结果。每一行都给出一个断言失败(随意将它们更改为异常)

    /* unsigned -> signed, overflow */
    safe_cast<short>(UINT_MAX);
    
    /* unsigned -> unsigned, overflow */
    safe_cast<unsigned char>(ULONG_MAX);
    
    /* signed -> unsigned, overflow */
    safe_cast<unsigned long>(-1);
    
    /* signed -> signed, overflow */
    safe_cast<signed char>(INT_MAX);
    
    /* always works (no check done) */
    safe_cast<long>(INT_MAX);
    
    // giving these assertion failures results
    (type)f <= (type)is_signed<To>::v_max
    f <= (To)-1
    f >= 0
    f >= is_signed<To>::v_min && f <= is_signed<To>::v_max
    

    实施。首先是一些检查整数等级的实用程序(具有较高等级的类型将能够包含具有较低等级的类型的值,给定相同的符号。还有一些提升工具,能够找出一个常见的、安全的类型(这永远不会如果涉及无符号类型,则产生有符号类型,如果有符号类型将无法存储无符号类型的所有值)。

    /* ranks */
    template<typename> struct int_rank;
    #define RANK(T, I) template<> struct int_rank<T> \
        { static int const value = I; }
    
    RANK(char, 1); RANK(unsigned char, 1); RANK(signed char, 1); 
    RANK(short, 2); RANK(unsigned short, 2);
    RANK(int, 3); RANK(unsigned int, 3);
    RANK(long, 4); RANK(unsigned long, 4);
    #undef RANK
    
    /* usual arith. conversions for ints (pre-condition: A, B differ) */
    template<int> struct uac_at;
    template<> struct uac_at<1> { typedef int type; };
    template<> struct uac_at<2> { typedef unsigned int type; };
    template<> struct uac_at<3> { typedef long type; };
    template<> struct uac_at<4> { typedef unsigned long type; };
    
    template<typename A, typename B>
    struct uac_type { 
        static char (&f(int))[1];
        static char (&f(unsigned int))[2];
        static char (&f(long))[3];
        static char (&f(unsigned long))[4];
        typedef typename uac_at<sizeof f(0 ? A() : B())>::type type; 
    };
    
    /* signed games */
    template<typename> struct is_signed { static bool const value = false; };
    #define SG(X, TT) template<> struct is_signed<X> { \
        static bool const value = true;                \
        static X const v_min = TT##_MIN;               \
        static X const v_max = TT##_MAX;               \
    }
    
    SG(signed char, SCHAR); 
    SG(short, SHRT); 
    SG(int, INT); 
    SG(long, LONG); 
    #undef SG
    
    template<> struct is_signed<char> { 
        static bool const value = (CHAR_MIN < 0); 
        static char const v_min = CHAR_MIN; // just in case it's signed...
        static char const v_max = CHAR_MAX;
    };
    

    转换模板利用它们来确定每种情况需要做什么或不做什么。

    template<typename To, typename From, 
             bool to_signed = is_signed<To>::value, 
             bool from_signed = is_signed<From>::value,
             bool rank_fine = (int_rank<To>::value >= int_rank<From>::value)>
    struct do_conv;
    
    /* these conversions never overflow, like int -> int, 
     * or  int -> long. */
    template<typename To, typename From, bool Sign>
    struct do_conv<To, From, Sign, Sign, true> {
        static To call(From f) {
            return (To)f; 
        }
    };
    
    template<typename To, typename From>
    struct do_conv<To, From, false, false, false> {
        static To call(From f) {
            assert(f <= (To)-1);
            return (To)f;
        }
    };
    
    template<typename To, typename From>
    struct do_conv<To, From, false, true, true> {
        typedef typename uac_type<To, From>::type type;
        static To call(From f) {
            /* no need to check whether To's positive range will
             * store From's positive range: Because the rank is
             * fine, and To is unsigned. 
             * Fixes GCC warning "comparison is always true" */
            assert(f >= 0);
            return (To)f;
        }
    };
    
    template<typename To, typename From>
    struct do_conv<To, From, false, true, false> {
        typedef typename uac_type<To, From>::type type;
        static To call(From f) {
            assert(f >= 0 && (type)f <= (type)(To)-1);
            return (To)f;
        }
    };
    
    template<typename To, typename From, bool Rank>
    struct do_conv<To, From, true, false, Rank> {
        typedef typename uac_type<To, From>::type type;
        static To call(From f) {
            assert((type)f <= (type)is_signed<To>::v_max);
            return (To)f;
        }
    };
    
    template<typename To, typename From>
    struct do_conv<To, From, true, true, false> {
        static To call(From f) {
            assert(f >= is_signed<To>::v_min && f <= is_signed<To>::v_max);
            return (To)f;
        }
    };
    
    template<typename To, typename From>
    To safe_cast(From f) { return do_conv<To, From>::call(f); }
    

    【讨论】:

      【解决方案4】:

      您尝试过 SafeInt 吗?它是一个跨平台模板,将对各种整数类型进行整数溢出检查。可以在 github 上找到

      【讨论】:

        【解决方案5】:

        怎么样:

        template< typename T, typename R > void safe_cast( const T& source, R& result )
        {
            R temp = static_cast<R>( source );
            if (static_cast<T> (temp) != source
                || ( temp < 0 && source > 0)
                || ( temp > 0 && source < 0))
            {
                throw IntegerOverflowException( source );
            }
            result = temp;
        }
        

        然后你只是检查铸造是否有效。确保你恢复了你开始时的状态,并且标志没有翻转。

        编辑: 由于下面的评论搞砸了,这里是格式化的:

        int myint (-1);
        safe_cast( myint, mychar );
        safe_cast( mychar, myuchar ); // Exception is thrown here
        safe_cast( myuchar, myint );
        

        从 int 到 char 的转换工作正常。从 char 到 unsigned char 的转换会引发异常(应该如此)。我认为这里没有问题。

        【讨论】:

        • o, 与 0 比较时,当两种类型中的一种是无符号的而另一种不是时,编译器会发出警告(由于 g++ 中的数据类型范围有限,比较总是错误的),但它不会抛出并且数据将丢失:如果您使用从 -1 (int) 到 -1 (char) 到 0xFF (unsigned char) 到 int 的转换,您将不会得到 -1。演员表不是“安全的”,因为值会在路上发生变化。
        • 啊,我用不同的编译器测试过。当 temp 无符号时,警告是指“temp
        【解决方案6】:

        https://www.boost.org/doc/libs/1_71_0/libs/safe_numerics/doc/html/index.html 考虑安全数字http://rrsd.com/blincubator.com/bi_library/safe-numerics

        这个库为所有 C 原始整数类型提供了直接替换。导致错误结果的 C 操作 - 包括强制转换在检测到时被捕获。

        【讨论】:

          【解决方案7】:

          这个问题发布已经十多年了,我想要一个自包含并使用现代 C++ 的解决方案(std::optionalconstexprtype_traits)。这是我写的:

          /// Cast integer of type "From" to integer of type "To", as long as it fits. If it doesn't
          /// fit, return std::nullopt.
          template<typename To, typename From>
          constexpr std::optional<To> IntegerCast(From from) {
              static_assert(std::is_integral_v<From>, "IntegerCast only supports integers");
              static_assert(std::is_integral_v<To>, "IntegerCast only supports integers");
              static_assert(!std::is_same_v<To, bool>, "IntegerCast only supports integers");
              static_assert(!std::is_same_v<From, bool>, "IntegerCast only supports integers");
          
              constexpr bool fromSigned = std::is_signed_v<From>;
              constexpr bool toSigned = std::is_signed_v<To>;
              constexpr bool bothSigned = fromSigned && toSigned;
              constexpr bool bothUnsigned = !fromSigned && !toSigned;
          
              constexpr From fromMax = std::numeric_limits<From>::max();
              constexpr From fromMin = std::numeric_limits<From>::min();
              constexpr To toMax = std::numeric_limits<To>::max();
              constexpr To toMin = std::numeric_limits<To>::min();
          
              if constexpr (bothUnsigned) {
                  using Widen = std::conditional_t<(sizeof(From) > sizeof(To)), From, To>;
                  if (from > Widen(toMax)) {
                      return std::nullopt;
                  } else {
                      return To(from);
                  }
              } else if constexpr (bothSigned) {
                  using Widen = std::conditional_t<(sizeof(From) > sizeof(To)), From, To>;
                  if (from > Widen(toMax)) {
                      return std::nullopt;
                  } else if (from < Widen(toMin)) {
                      return std::nullopt;
                  } else {
                      return To(from);
                  }
              } else if constexpr (fromSigned && !toSigned) {
                  using Widen =
                          std::make_unsigned_t<std::conditional_t<(sizeof(From) > sizeof(To)), From, To>>;
                  if (from < 0) {
                      return std::nullopt;
                  } else if (from > Widen(toMax)) {
                      return std::nullopt;
                  } else {
                      return To(from);
                  }
              } else if constexpr (!fromSigned && toSigned) {
                  using Widen =
                          std::make_unsigned_t<std::conditional_t<(sizeof(From) > sizeof(To)), From, To>>;
                  if (from > Widen(toMax)) {
                      return std::nullopt;
                  } else {
                      return To(from);
                  }
              }
          }
          

          它带有一个 GoogleTest 中的测试套件

          TEST(IntegerCast, Basics) {
              constexpr uint64_t large64 = 10000000000000000000ull;
              static_assert(IntegerCast<uint8_t>(large64) == std::nullopt);
              static_assert(IntegerCast<uint16_t>(large64) == std::nullopt);
              static_assert(IntegerCast<uint32_t>(large64) == std::nullopt);
              static_assert(IntegerCast<uint64_t>(large64) == 10000000000000000000ull);
              static_assert(IntegerCast<int8_t>(large64) == std::nullopt);
              static_assert(IntegerCast<int16_t>(large64) == std::nullopt);
              static_assert(IntegerCast<int32_t>(large64) == std::nullopt);
              static_assert(IntegerCast<int64_t>(large64) == std::nullopt);
          
              constexpr int64_t largeNegative64 = -5000000000000000000;
              static_assert(IntegerCast<uint8_t>(largeNegative64) == std::nullopt);
              static_assert(IntegerCast<uint16_t>(largeNegative64) == std::nullopt);
              static_assert(IntegerCast<uint32_t>(largeNegative64) == std::nullopt);
              static_assert(IntegerCast<uint64_t>(largeNegative64) == std::nullopt);
              static_assert(IntegerCast<int8_t>(largeNegative64) == std::nullopt);
              static_assert(IntegerCast<int16_t>(largeNegative64) == std::nullopt);
              static_assert(IntegerCast<int32_t>(largeNegative64) == std::nullopt);
              static_assert(IntegerCast<int64_t>(largeNegative64) == -5000000000000000000);
          
              constexpr uint64_t small64 = 1;
              static_assert(IntegerCast<uint8_t>(small64) == 1);
              static_assert(IntegerCast<uint16_t>(small64) == 1);
              static_assert(IntegerCast<uint32_t>(small64) == 1);
              static_assert(IntegerCast<uint64_t>(small64) == 1);
              static_assert(IntegerCast<int8_t>(small64) == 1);
              static_assert(IntegerCast<int16_t>(small64) == 1);
              static_assert(IntegerCast<int32_t>(small64) == 1);
              static_assert(IntegerCast<int64_t>(small64) == 1);
          
              constexpr int64_t smallNegative64 = -1;
              static_assert(IntegerCast<uint8_t>(smallNegative64) == std::nullopt);
              static_assert(IntegerCast<uint16_t>(smallNegative64) == std::nullopt);
              static_assert(IntegerCast<uint32_t>(smallNegative64) == std::nullopt);
              static_assert(IntegerCast<uint64_t>(smallNegative64) == std::nullopt);
              static_assert(IntegerCast<int8_t>(smallNegative64) == -1);
              static_assert(IntegerCast<int16_t>(smallNegative64) == -1);
              static_assert(IntegerCast<int32_t>(smallNegative64) == -1);
              static_assert(IntegerCast<int64_t>(smallNegative64) == -1);
          }
          
          TEST(IntegerCast, Boundaries) {
              constexpr uint8_t maxUnsigned8 = 255;
              static_assert(IntegerCast<uint8_t>(maxUnsigned8) == 255);
              static_assert(IntegerCast<uint16_t>(maxUnsigned8) == 255);
              static_assert(IntegerCast<uint32_t>(maxUnsigned8) == 255);
              static_assert(IntegerCast<uint64_t>(maxUnsigned8) == 255);
              static_assert(IntegerCast<int8_t>(maxUnsigned8) == std::nullopt);
              static_assert(IntegerCast<int16_t>(maxUnsigned8) == 255);
              static_assert(IntegerCast<int32_t>(maxUnsigned8) == 255);
              static_assert(IntegerCast<int64_t>(maxUnsigned8) == 255);
          
              constexpr uint8_t minUnisigned8 = 0;
              static_assert(IntegerCast<uint8_t>(minUnisigned8) == 0);
              static_assert(IntegerCast<uint16_t>(minUnisigned8) == 0);
              static_assert(IntegerCast<uint32_t>(minUnisigned8) == 0);
              static_assert(IntegerCast<uint64_t>(minUnisigned8) == 0);
              static_assert(IntegerCast<int8_t>(minUnisigned8) == 0);
              static_assert(IntegerCast<int16_t>(minUnisigned8) == 0);
              static_assert(IntegerCast<int32_t>(minUnisigned8) == 0);
              static_assert(IntegerCast<int64_t>(minUnisigned8) == 0);
          
              constexpr int8_t maxSigned8 = 127;
              static_assert(IntegerCast<uint8_t>(maxSigned8) == 127);
              static_assert(IntegerCast<uint16_t>(maxSigned8) == 127);
              static_assert(IntegerCast<uint32_t>(maxSigned8) == 127);
              static_assert(IntegerCast<uint64_t>(maxSigned8) == 127);
              static_assert(IntegerCast<int8_t>(maxSigned8) == 127);
              static_assert(IntegerCast<int16_t>(maxSigned8) == 127);
              static_assert(IntegerCast<int32_t>(maxSigned8) == 127);
              static_assert(IntegerCast<int64_t>(maxSigned8) == 127);
          
              constexpr int8_t minSigned8 = -128;
              static_assert(IntegerCast<uint8_t>(minSigned8) == std::nullopt);
              static_assert(IntegerCast<uint16_t>(minSigned8) == std::nullopt);
              static_assert(IntegerCast<uint32_t>(minSigned8) == std::nullopt);
              static_assert(IntegerCast<uint64_t>(minSigned8) == std::nullopt);
              static_assert(IntegerCast<int8_t>(minSigned8) == -128);
              static_assert(IntegerCast<int16_t>(minSigned8) == -128);
              static_assert(IntegerCast<int32_t>(minSigned8) == -128);
              static_assert(IntegerCast<int64_t>(minSigned8) == -128);
          }
          
          TEST(IntegerCast, SameSizeDifferentSign) {
              constexpr uint8_t above = 200;
              static_assert(IntegerCast<int8_t>(above) == std::nullopt);
          
              constexpr uint8_t withinUnsigned = 100;
              static_assert(IntegerCast<int8_t>(withinUnsigned) == 100);
          
              constexpr int8_t withinSigned = 100;
              static_assert(IntegerCast<uint8_t>(withinSigned) == 100);
          
              constexpr int8_t below = -100;
              static_assert(IntegerCast<uint8_t>(below) == std::nullopt);
          }
          

          【讨论】:

            【解决方案8】:

            我是否正确假设在 R 已签名的情况下,您试图用除最后一位之外的所有 1 填充 rMax?如果是这种情况,那么您应该使用 0x80 (1000 0000) 而不是 0x10 (0001 0000)。

            此外,您的函数似乎不支持源的负数。

            编辑:

            这是一个稍微编辑过的版本,我已经测试过它可以从整数转换为字符:

            template< typename T, typename R >
            void safe_cast( const T& source, R& result )
            {
                // get the maximum safe value of type R
                R rMax = (R) ~0;
                if ( rMax < 0 ) // R is a signed type
                {
                    // assume that we're on an 8-bit twos-compliment machine
                rMax = ( 0x80 << ( ( sizeof( R ) - 1 ) * 8 ) );
                if(source >= 0)
                    rMax = ~rMax;
                }
            
                if ( (source >= 0 && ( source & rMax  ) != source) || (source < 0 && (source & rMax) != rMax) )
                {
                    throw new IntegerOverflowException( source );
                }
            
                result = static_cast<R>( source );
            }
            

            编辑:修复错误。

            【讨论】:

              【解决方案9】:

              我在 sweet.hpp 有一个名为 conv.hpp 的标头。它将测试所有整数类型的界限,并且还允许对整数进行字符串转换。

              short a = to<short>(1337);
              std::string b = to<std::string>(a);
              long c = to<long>(b);
              

              【讨论】:

                【解决方案10】:

                我一定错过了什么,但这不正是你想要的吗?:

                // using a more cast-like prototype, if I may:
                template<class to, class from> inline
                to safe_cast(from f)
                {
                   to t = static_cast<to>(f);
                   if ( t != f ) throw whatever; // no new!
                   return t;
                }
                

                【讨论】:

                • 不,如果您使用从 -1 (int) 到 -1 (char) 到 0xFF (unsigned char) 再到 int 的转换,您将不会得到 -1。演员表不是“安全的”,因为值会在路上发生变化。
                • 嗨,Dribeas,对不起,我不确定你在说什么。 . safe_cast(int(-1)) 不会溢出,并返回 fine 。 safe_cast(char(-1)) 改变符号(和值),并抛出。因此正确的行为。或者,你在说什么?
                • 假设 char 已签名,safe_cast(char(-1)) 会将 t 设置为 UCHAR_MAX(可能为 255)。那么 if(t != f) 会将 char 提升为 int,产生 -1,将 unsigned char 提升为 int,产生 255,因此它们不相等。但是在执行 safe_cast(-1) 时,它会将 t 设置为 UINT_MAX,然后 if 不会提升任何内容,并将 int 转换为 unsigned int (UAC),从而再次产生 UINT_MAX,并错误地认为强制转换成功。
                • 我很难相信; C++ 在这一点上实际上与 C 不同吗? C99 非常清楚地表明“当一个整数类型的值转换为另一个整数类型时……如果该值可以用新类型表示,则它是不变的。”因此——在 C 中——在你的例子中将 UCHAR_MAX 转换为 int 得到 255。
                • @stephentyrone,我的例子也是如此。 t 为 UCHAR_MAX,提升为 int 后为 255。具体指的是什么转换?
                猜你喜欢
                • 2010-12-22
                • 1970-01-01
                • 2013-02-28
                • 2017-12-06
                • 1970-01-01
                • 1970-01-01
                • 2012-08-23
                • 1970-01-01
                • 2011-03-15
                相关资源
                最近更新 更多