使用无符号整数 vs. 检查是否为负数

24

假设我有一个类,其中包含一个整数,它应该始终为正数,并带有一个简单的构造函数:

class A {
    unsigned int x;
    public:
    A(unsigned int X) : x(X) {}
};

假设有人无意中创建了一个值为-5的A对象。当然,这是无效的,因为现在MSB不再表示数字符号,所以X将获得一个非常大的值。问题是,我现在无法确定该数字是负数还是无效的(也许可以通过按位操作?)。

我应该避免使用unsigned,改用普通的int吗?这样做,如果给定的值超出最大值或小于零,我就可以抛出异常。

希望听到一些建议。


注意:此翻译可能存在误差,仅供参考。

4
我会尽力进行翻译,以下是您需要翻译的内容:我总是根据类型的语义来选择。如果 x 应该只为非负数,则使用 unsigned - Violet Giraffe
2
@VioletGiraffe,但请记住,这对于存储值是有意义的。在进行算术运算时,要非常小心混合符号计算——它们可能会令人痛苦地惊讶。始终编译-Wall -pedantic以尽早捕获这些情况。 - sehe
使用无符号类型通常不是一个好主意。它从未匹配变量语义,因为您仍然需要检查最大值。代码更容易出现错误,并且充斥着强制转换。 - Jem
3
在某个时候,您必须假设使用您的类的程序员是成年人,并且能够为自己承担责任。如果x需要在特定范围内,则传统的边界检查对于有符号或无符号类型都有效。如果A的用户鲁莽,他可能会在构造函数中传递一个合理但完全错误的值(例如,从其他地方复制值而不进行检查)。 - DanielKO
5
如果A的用户将负数放入一个“unsigned int”中,那就是他自己的问题。接口清楚地表示数字必须是“unsigned”,客户端代码中发生的任何转换都不是你的责任。如果用户输入了“-5”,你可以假设他了解语言的工作原理并且确实想在其中输入“4294967291”或类似的值。如果该值对于你的“A”太大,则需要检查这种情况;如果它没有太大的问题,那么你的问题在哪里? - Christian Rau
3个回答

37

我想到了两种方法:

  1. Add an explicit conversion for the 'signed' types.

    #include <cassert>
    
    class A {
        unsigned int x;
        public:
        A(unsigned int X) : x(X) {}
        explicit A(int X) : x(static_cast<unsigned>(X)) {
            assert(X>=0); // note X, not x!
        }
    };
    
    int main()
    {
        A ok(5);
        A bad(-5);
    }
    
  2. Prohibit implicit conversions by deleting better overloads:

    A(int X) = delete;
    A(long X) = delete;
    A(char X) = delete;
    

    This will require all users to cast to unsigned before constructing the A instance. It's safe but clumsy.

请注意,这并不禁止从所有整数类型(例如枚举)进行隐式转换,因此您需要更多的工作来使其防错。
以下是一个基于SFINAE的简单示例,它接受所有隐式转换,但如果涉及有符号值,则除外。 在Coliru上查看
#include <type_traits>
#include <limits>

class A {
    unsigned int x;
    public:
    template<typename T, typename = typename std::enable_if<std::is_integral<T>::value, void>::type>
    A(T X) : x(X)
    {
        static_assert(!std::numeric_limits<T>::is_signed, "Signed types cannot be accepted");
    }
};

int main()
{
    A ok(5u);
    A bad(-5);
}

帮了很大忙,谢谢。最后一个方法是否提供负数的编译时检查? - Rouki
4
最后一种解决方案,我要引用Stroustrup的话:“一个把程序员当傻瓜对待的机构,很快就会有愿意并能够像傻瓜一样行事的程序员。” 如果强制类的用户始终将其转换为unsigned,甚至是字面量 0,那么您很快就会发现该类总是与显式的 (unsigned)somevalue 参数一起使用,从而失去了整个目的。 - DanielKO
1
@DanielKO,我只是在指出这一点。请注意这不是我“指定”的方法之一 :). 此外,“完全失去意义”是一个谬误,因为那样显然更加明显。另外,并没有必要将字面值0转化成你应该直接使用字面值0u(就像我的例子已经展示的那样!) - sehe
1
如果正常使用情况是数据源是已知为非负的 int 值,那么模式 A foo((unsigned)bar) 将无处不在;现在编译时保证已经无用了,因为你强制程序员每次使用 A 时都要撒谎。这使得正常使用情况变得不方便,并且由于此原因你最终会错过异常情况。 - DanielKO
1
@DanielKO 我倾向于说,如果这是“正常用例”,那么 A 应该只需要使用 int。 <whistle/> - sehe
显示剩余3条评论

7

比如说,如果有人不小心创建了一个值为-5的A对象

尽管让你的程序足够强大以接受这样的错误可能是个好习惯,但这个bug的根本原因是程序员粗心大意,没有启用足够的警告。

为了找出问题的根源,您需要确保代码编译时启用了所有警告,并考虑使用外部静态分析工具。


1

我可能同意DanielKO关于模式将无处不在的观点。请注意,sehe基于SFINAE的解决方案对于uint64-> uint32 截断不起作用。因此我的答案是:

class A {
 public:
  using value_type = unsigned int;

  template <class T>
  explicit A(T x): x_(boost::numeric_cast<value_type>(x)) { // can be check in debug only
    static_assert(std::is_integral<T>::value, "T must be integral");
  }

 private:
  value_type x_;
};

而且 实时示例


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接