【问题标题】:Why are C++ (Visual Studio) Datatypes not stopping on their Limit?为什么 C++ (Visual Studio) 数据类型没有停止在其限制上?
【发布时间】:2019-12-12 05:59:57
【问题描述】:

通常情况下,如果数据类型停止工作(程序崩溃等)而不是在超出限制时没有任何信息,会更安全、更好(例如避免 UB)。

例如,unsigned char 的限制是 255,现在假设值 3000 作为 unsigned char 存储在文本文件中,并加载到 C++ 程序中。它不会给出任何错误或其他东西,而是unsigned char 只会做一些自动转换(或UB?)并给出另一个值,低于它的限制。但为什么?有什么用?

如果超出用于处理该值的数据类型,是否有任何方法可以使使用 Visual Studio 构建的程序停止工作(崩溃或发出警报等)?

与有符号/无符号数据类型相同,如果您使用无符号,但以某种方式加载“-1”值,无符号数据类型只是“接受”它,但会为您提供高于 0 的其他值。

【问题讨论】:

  • 请区分 Visual Studio - 它是一个 IDE - 和语言 - 在你的情况下似乎是 C++。
  • 出于好奇:如何将值 3000 作为无符号字符存储在文本文件中?这对我来说似乎是不可能的...... ;-)

标签: c++ types


【解决方案1】:

在大多数情况下,缺少检查的最重要原因是性能。 C++ 遵循原则不要得到你不买的东西。如果您需要检查,请编写一个自定义类型来进行检查。您甚至可以让它仅在调试模式下进行检查。 (例如,std::vector 在其 operator[] 中实现。)如果你想要一种安全的语言,C++ 是你最糟糕的选择(在 C 和汇编之后)。 ;-) 但是您可以选择许多其他高级语言。

对于调试检查,标准库中有一个宏:assert():

#ifdef NDEBUG
#define assert(condition) ((void)0)
#else
#define assert(condition) /*implementation defined*/
#endif

因此,/*implementation defined*/ 部分检查condition。如果结果为false,那么通常会调用abort(),这又会立即中止进程(通常会生成一个可以在调试器中评估的核心转储)。

如果超出用于处理该值的数据类型,是否有任何方法可以使使用 Visual Studio 构建的程序停止工作(崩溃或发出警报等)?

与有符号/无符号数据类型相同,如果您使用无符号,但以某种方式加载“-1”值,无符号数据类型只是“接受”它,但会为您提供高于 0 的其他值。

是的,有一种方法——使用自定义类型作为原始类型的包装器。

一个简单的例子来证明这一点:

#include <cassert>
#include <iostream>

struct UChar {
  unsigned char value;

  UChar(unsigned char value = 0): value(value) { }
  UChar(char value): value((assert(value >= 0), (unsigned char)value)) { }
  UChar(int value): value((assert(value >= 0 && value < 256), value)) { }
  UChar(unsigned value): value((assert(value < 256), value)) { }
  UChar(long value): value((assert(value >= 0 && value < 256), value)) { }
  UChar(unsigned long value): value((assert(value < 256), value)) { }
  UChar(long long value): value((assert(value >= 0 && value < 256), value)) { }
  UChar(unsigned long long value): value((assert(value < 256), value)) { }
  UChar(const UChar&) = default;
  UChar& operator=(const UChar&) = default;
  ~UChar() = default;

  operator unsigned char() { return value; }
};

#define PRINT_AND_DO(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__ 

int main()
{
  // OK
  PRINT_AND_DO(UChar c);
  PRINT_AND_DO(std::cout << "c: " << (unsigned)c << '\n');
  PRINT_AND_DO(c = 'A');
  PRINT_AND_DO(std::cout << "c: " << c << '\n');
  PRINT_AND_DO(UChar d = 'B');
  PRINT_AND_DO(std::cout << "d: " << d << '\n');
  PRINT_AND_DO(d = c);
  PRINT_AND_DO(std::cout << "d: " << d << '\n');
  // This will crash if NDEBUG not defined:
  //PRINT_AND_DO(UChar e(3000));
  //PRINT_AND_DO(c = 3000);
  PRINT_AND_DO(d = -1);
}

输出:

a.out: main.cpp:9: UChar::UChar(int): Assertion `value >= 0 && value < 256' failed.
UChar c;
std::cout << "c: " << (unsigned)c << '\n';
c: 0
c = 'A';
std::cout << "c: " << c << '\n';
c: A
UChar d = 'B';
std::cout << "d: " << d << '\n';
d: B
d = c;
std::cout << "d: " << d << '\n';
d: A
d = -1;
bash: line 7: 31094 Aborted                 (core dumped) ./a.out

Live Demo on coliru

注意:

这只是一个演示 - 不是生产就绪代码。例如。不会捕获加法溢出或减法下溢,因为转换运算符 operator unsigned char() 会导致包装器实例在使用未在 struct UChar 中重载的运算符时转换回 unsigned char(但支持 unsigned char)。要解决此问题,必须为UChar 重载更多运算符。但是,我相信演示足以展示原理。

【讨论】:

    【解决方案2】:

    您所描述的不是由 C++ 管理的(如您所见)。它被称为环绕。 IE。该数字被截断为变量可以保存的任何值,就好像一旦超过其“限制”,它就会回绕为零。 (至少对于正数)。

    没有被捕获的原因是拥有此功能可能很有用。

    话虽如此 - 如果您分配的常量数字对于变量来说太大,或者如果您将“更大的容器”类型分配给较小的一个(如 uint16 到 uint8)。 您可以将编译器配置为在出现此类警告时编译失败,因此您无需显式转换类型。

    【讨论】:

      【解决方案3】:

      一种编程语言,尤其是像 C/C++ 这样接近硬件的语言,通常只公开典型微处理器在其指令集中提供的功能。以 x86 为例,当您添加整数时(通常),如果发生溢出,处理器将设置 OV 标志,并为下溢设置 IIRC OV 和 SIGN 标志。

      通常,编译器不会在每次可能欠/溢出的算术运算之后生成额外的指令来检查这些标志。这会牺牲性能,而性能才是王道,看起来甚至高于正确性。

      您正在寻找的概念有两个方面:

      • 饱和算术:无论您做什么,变量都会被裁剪为其数据类型范围的最小值/最大值。不会引发任何异常。
      • 编译器显式检查下溢/溢流:我能想到的最接近的例子是 C# 中的 checked { } 构造,您应该注意默认情况下不启用它。

      P.S.:我还记得 x86 可能在某个时候在其指令集中添加了饱和算法,可能是通过 SIMD 扩展。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-10-20
        • 2012-02-17
        • 1970-01-01
        • 2011-06-21
        • 2017-05-30
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多