9

我自己坚信,在我正在处理的项目中,有符号整数是大多数情况下的最佳选择,即使其中包含的值永远不会是负数。(更简单的反向循环,更少的错误机会等,特别是对于只能保存在 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?

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

4

7 回答 7

7

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

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

于 2008-11-09T12:58:33.427 回答
4

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

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

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

我的解决方案?一个 stl-for-each 宏。这并非没有问题(主要是,它是一个宏,糟糕),但它理解了含义。它不像这个那样先进,但可以胜任。

于 2008-11-09T14:19:05.090 回答
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 回绕,因为它是无符号的。

但是未签名会使错误陷入困境

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

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

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

于 2008-11-09T12:33:34.850 回答
3

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

for (auto i = someVector.begin();
     i != someVector.end();
     ++i)
于 2008-11-09T14:12:44.323 回答
3

跳过索引

最简单的方法是通过使用迭代器、基于范围的 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(来自 <cstddef>)。有些人(正确地)指出std::size_t可能与std::vector<T>::size_type(尽管通常是)不同的类型。但是,您可以放心,容器size_type将适合std::size_t. 所以一切都很好,除非您使用某些样式进行反向循环。我首选的反向循环样式是:

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

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

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

如果您真的想使用带符号的类型(或者您的样式指南实际上需要一个),例如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循环稍微多输入一点,您可以在运行时检查基本假设,您可以使用显式转换使编译器警告静音,您获得与非调试构建相同的速度,因为它几乎肯定会内联,并且优化的目标代码不应该更大,因为模板没有做任何编译器还没有隐式做的事情。

于 2013-08-22T00:23:55.280 回答
0

vector.size()返回一个size_tvar,所以只需更改intsize_t,它应该没问题。

理查德的回答更正确,除了一个简单的循环需要做很多工作。

于 2008-11-09T13:14:10.650 回答
0

你多虑了这个问题。

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

于 2008-11-09T13:14:17.497 回答