【问题标题】:acceptable fix for majority of signed/unsigned warnings?大多数签名/未签名警告的可接受修复?
【发布时间】:2010-09-21 12:14:18
【问题描述】:

我自己相信,在我正在处理的项目中,有符号整数是大多数情况下的最佳选择,即使其中包含的值永远不会是负数。 (更简单的循环反向,更少的错误机会等,特别是对于只能保存 0 到 20 之间的值的整数。)

大多数出错的地方是 std::vector 的简单迭代,过去通常这曾经是一个数组,后来被更改为 std::vector。所以这些循环通常是这样的:

for (int i = 0; i < someVector.size(); ++i) { /* do stuff */ }

因为这种模式经常被使用,关于有符号和无符号类型比较的编译器警告垃圾邮件的数量往往会隐藏更多有用的警告。请注意,我们绝对没有超过 INT_MAX 元素的向量,并且请注意,到目前为止,我们使用了两种方法来修复编译器警告:

for (unsigned i = 0; i < someVector.size(); ++i) { /*do stuff*/ }

这通常有效,但如果循环包含任何代码,如“if (i-1 >= 0) ...”等,则可能会静默中断。

for (int i = 0; i < static_cast<int>(someVector.size()); ++i) { /*do stuff*/ }

此更改没有任何副作用,但它确实使循环的可读性降低了很多。 (而且打字更多。)

于是我想出了以下想法:

template <typename T> struct vector : public std::vector<T>
{
    typedef std::vector<T> base;

    int size() const     { return base::size(); }
    int max_size() const { return base::max_size(); }
    int capacity() const { return base::capacity(); }

    vector()                  : base() {}
    vector(int n)             : base(n) {}
    vector(int n, const T& t) : base(n, t) {}
    vector(const base& other) : base(other) {}
};

template <typename Key, typename Data> struct map : public std::map<Key, Data>
{
    typedef std::map<Key, Data> base;
    typedef typename base::key_compare key_compare;

    int size() const     { return base::size(); }
    int max_size() const { return base::max_size(); }

    int erase(const Key& k) { return base::erase(k); }
    int count(const Key& k) { return base::count(k); }

    map() : base() {}
    map(const key_compare& comp) : base(comp) {}
    template <class InputIterator> map(InputIterator f, InputIterator l) : base(f, l) {}
    template <class InputIterator> map(InputIterator f, InputIterator l, const key_compare& comp) : base(f, l, comp) {}
    map(const base& other) : base(other) {}
};

// TODO: similar code for other container types

您所看到的基本上是 STL 类,其中返回 size_type 的方法被覆盖以仅返回“int”。需要构造函数,因为它们不是继承的。

如果您在现有代码库中看到这样的解决方案,作为开发人员您会怎么看?

您是否会认为“哇,他们正在重新定义 STL,多么巨大的 WTF!”,或者您是否认为这是一个很好的简单解决方案,可以防止错误并提高可读性。或者您可能更愿意看到我们花了(半天)左右的时间来更改所有这些循环以使用 std::vector::iterator?

(特别是如果此解决方案与禁止将无符号类型用于除原始数据(例如无符号字符)和位掩码之外的任何内容。)

【问题讨论】:

    标签: c++ stl coding-style unsigned


    【解决方案1】:

    不要公开从 STL 容器派生。它们具有非虚拟析构函数,如果有人通过指向基的指针删除您的对象之一,则会调用未定义的行为。如果你必须导出例如从向量中,私下进行,并使用 using 声明公开您需要公开的部分。

    在这里,我只使用size_t 作为循环变量。它简单易读。评论说使用int 索引的发布者将您暴露为n00b 是正确的。然而,使用迭代器循环一个向量会使您成为一个稍微有经验的 n00b - 一个没有意识到向量的下标运算符是常数时间的人。 (vector&lt;T&gt;::size_type 是准确的,但 IMO 不必要地冗长)。

    【讨论】:

    • 正如 ChrisN 正确评论的那样, vector::size_t 不一定是 std::size_t 。所以如果你会做 size_t i = someVector.size() - 1 ,你不一定会得到 (size_t)-1 。这意味着你的循环条件被打破了。
    • 不,默认分配器的 size_type 始终是 size_t。
    • 是的,但是 vector::size_type 不是根据我的标准副本中的默认分配器定义的(我在上面自己的评论中也犯了这个错误)。它根据容器要求定义为实现定义的无符号整数类型。
    • 那是个好地方——我错过了,谢谢。但由于 size_t 永远不会比 size_type 窄,所以我看不出有什么问题。
    • 到达那里的一种方法是 size_t 足够大以容纳任何对象的大小,并且向量保证由连续存储支持,因此 size_type
    【解决方案2】:

    虽然我不认为“使用迭代器,否则你看起来很n00b”是解决问题的好方法,但从 std::vector 派生似乎比这更糟糕。

    首先,开发人员确实希望vector 是std:.vector,而map 是std::map。其次,您的解决方案不适用于其他容器或与容器交互的其他类/库。

    是的,迭代器很丑陋,迭代器循环的可读性不是很好,而 typedef 只会掩盖混乱。但至少,它们确实可以扩展,而且它们是规范的解决方案。

    我的解决方案?一个 stl-for-each 宏。这并非没有问题(主要是,它是一个宏,糟糕),但它理解了含义。它不如例如先进。 this one,但确实可以。

    【讨论】:

    • 顺便说一句,VC++ 有开箱即用的“for each”关键字。当然,它不是便携的,但如果你不关心便携性,它就在那里。
    【解决方案3】:

    我创建了这个社区维基...请编辑它。我不再同意反对“int”的建议。我现在认为它还不错。

    是的,我同意理查德的观点。你永远不应该在这样的循环中使用'int' 作为计数变量。以下是您可能希望使用索引执行各种循环的方法(尽管没有什么理由这样做,但有时这很有用)。

    转发

    for(std::vector<int>::size_type i = 0; i < someVector.size(); i++) {
        /* ... */
    }
    

    向后

    你可以这样做,这是完美定义的行为:

    for(std::vector<int>::size_type i = someVector.size() - 1; 
        i != (std::vector<int>::size_type) -1; i--) {
        /* ... */
    }
    

    很快,随着 c++1x(下一个 C++ 版本)的顺利推出,您可以这样做:

    for(auto i = someVector.size() - 1; i != (decltype(i)) -1; i--) {
        /* ... */
    }
    

    递减到 0 以下会导致 i 回绕,因为它是无符号的。

    但是未签名的会让 bug 潜入

    这绝不应该成为错误方式的论据(使用'int')。

    为什么不用上面的 std::size_t 呢?

    C++ 标准在23.1 p5 Container Requirements 中定义,T::size_type,对于T 是一些Container,这种类型是一些实现定义的无符号整数类型。现在,将std::size_t 用于上面的i 将使错误无声无息地潜入。如果T::size_type 小于或大于std::size_t,那么它将溢出i,或者如果someVector.size() == 0 甚至达不到(std::size_t)-1。同样,循环的条件也会被完全破坏。

    【讨论】:

    • 要使用的适当类型是 std::vector::size_type,而不是 std::size_t。
    • 你是对的。我将它与 string::size_type 混淆了,它总是 std::size_t。我会相应地改变它
    • 虽然我个人不相信在 C++ 中使用无符号的优点(即 std::vector::size_type 解决方案不会阻止将基于循环索引的表达式提升为无符号悄悄地)我接受这个答案,因为它很好地概述了一个人可以为他人做些什么恕我直言。
    【解决方案4】:

    一定要使用迭代器。很快您将能够使用“自动”类型,以获得更好的可读性(您关心的一个问题),如下所示:

    for (auto i = someVector.begin();
         i != someVector.end();
         ++i)
    

    【讨论】:

    • 嘿。一旦这种语法在主流 Linux 发行版(可能已经有?)、MinGW 和 Visual Studio 中可用,我肯定会使用它。看起来比 std::vector::iterator it = someVector.begin() 等好多了...
    • 哎呀,这里也有很多错别字,好吧,假设意思很清楚:-)
    • GCC 中还没有“auto”,甚至还没有最前沿的 beta AFAIK。 gcc.gnu.org/gcc-4.4/cxx0x_status.html
    【解决方案5】:

    跳过索引

    最简单的方法是通过使用迭代器、基于范围的 for 循环或算法来回避问题:

    for (auto it = begin(v); it != end(v); ++it) { ... }
    for (const auto &x : v) { ... }
    std::for_each(v.begin(), v.end(), ...);
    

    如果您实际上不需要索引值,这是一个很好的解决方案。它还可以轻松处理反向循环。

    使用适当的无符号类型

    另一种方法是使用容器的尺寸类型。

    for (std::vector<T>::size_type i = 0; i < v.size(); ++i) { ... }
    

    您也可以使用std::size_t(来自)。有些人(正确地)指出std::size_t 可能与std::vector&lt;T&gt;::size_type 不是同一类型(尽管通常是)。但是,您可以放心,容器的size_type 将适合std::size_t。所以一切都很好,除非您使用某些样式进行反向循环。我喜欢的反向循环样式是这样的:

    for (std::size_t i = v.size(); i-- > 0; ) { ... }
    

    使用这种样式,您可以安全地使用std::size_t,即使它比std::vector&lt;T&gt;::size_type 更大。其他一些答案中显示的反向循环样式需要将 -1 强制转换为正确的类型,因此不能使用更易于键入的 std::size_t

    使用有符号类型(小心!)

    如果你真的想使用带符号的类型(或者如果你的 style guide practically demands one),比如 int,那么你可以使用这个小函数模板来检查调试构建中的基本假设并使转换显式,以便你不要收到编译器警告消息:

    #include <cassert>
    #include <cstddef>
    #include <limits>
    
    template <typename ContainerType>
    constexpr int size_as_int(const ContainerType &c) {
        const auto size = c.size();  // if no auto, use `typename ContainerType::size_type`
        assert(size <= static_cast<std::size_t>(std::numeric_limits<int>::max()));
        return static_cast<int>(size);
    }
    

    现在你可以写了:

    for (int i = 0; i < size_as_int(v); ++i) { ... }
    

    或者以传统方式反向循环:

    for (int i = size_as_int(v) - 1; i >= 0; --i) { ... }
    

    size_as_int 技巧只比带有隐式转换的循环稍微多输入一点,您可以在运行时检查基本假设,使用显式转换使编译器警告静音,获得与非调试构建相同的速度因为它几乎肯定会被内联,并且优化的目标代码不应该更大,因为模板不会做任何编译器尚未隐式做的事情。

    【讨论】:

      【解决方案6】:

      你想太多了。

      最好使用 size_t 变量,但如果您不相信您的程序员可以正确使用 unsigned,请使用强制转换并处理丑陋的问题。找一个实习生来改变它们,之后就不用担心了。在错误时打开警告,并且不会出现新的错误。您的循环现在可能“丑陋”,但您可以理解为您的宗教立场对签名和未签名的后果。

      【讨论】:

        【解决方案7】:

        vector.size() 返回一个size_t var,所以只需将int 更改为size_t 就可以了。

        Richard 的回答更正确,只是对于一个简单的循环来说工作量很大。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2011-11-18
          • 2015-08-04
          • 1970-01-01
          • 2021-03-30
          • 1970-01-01
          • 2018-08-01
          相关资源
          最近更新 更多