C++中的vector::size_type:有符号 vs 无符号;int vs long

7
我一直在不同的平台上编译我的应用程序进行测试,从64位系统切换到32位系统,暴露出了许多问题。我的函数中大量使用向量、字符串等,因此需要对它们进行计数。然而,我的函数也使用32位无符号数字,因为在许多情况下我需要明确消耗正整数。我遇到了一些看似简单的问题,例如std::min和std::max,这可能是更为系统性的问题。考虑以下代码:
uint32_t getmax()
{
    return _vecContainer.size();
}

看起来很简单:我知道向量不能有负数个元素,因此返回无符号整数完全合理。

void setRowCol(const uint32_t &r_row; const uint32_t &r_col)
{
    myContainer_t mc;
    mc.row = r_row;
    mc.col = r_col;
    _vecContainer.push_back(mc);
}

再次强调,这很简单。
问题:
uint32_t foo(const uint32_t &r_row)
{
    return std::min(r_row, _vecContainer.size());
}

这使我遇到了错误,例如:
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../include/c++/v1/algorithm:2589:1: note: candidate template ignored: deduced conflicting types for parameter '_Tp' ('unsigned long' vs. 'unsigned int')
min(const _Tp& __a, const _Tp& __b)

我做了很多调查,在一个平台上,vector::size_type 是一个8字节的数字。但是,按照设计,我使用的是无符号的4字节数字。这可能会导致问题出现,因为你不能从一个8字节的数字隐式转换为4字节的数字。

解决方法是老派的方式:

#define MIN_M(a,b) a < b ? a : b
return MIN_M(r_row, _vecContainer.size());

这很好用。但是系统性问题仍然存在: 在计划支持多个平台时,如何处理类似这样的情况?我可以使用size_t作为我的标准大小,但这会增加其他复杂性(例如在以后从支持64位数字的平台移动到支持32位数字的平台)。更大的问题是size_t是无符号的,所以我不能更新我的签名:

size_t foo(const size_t &r_row)
// bad, this allows -1 to be passed, which I don't want

有什么建议吗?

编辑:我曾经在某个地方读到size_t是有符号的,但后来被纠正了。目前看来这是我的设计限制(例如32位数字 vs 使用std::vector::size_type和/或size_t)。


5
如果您查看例如 这个 std::vector 参考链接,您会发现 size_type 通常是一个 std::size_t,其类型是实现特定的无符号整数。如果您需要表示非负大小的类型,则应该使用 std::size_t 或对于容器,直接使用 size_type - Some programmer dude
8
这不是问题,但通过const&传递数字类型是过度设计。直接按值传递即可。 - Pete Becker
2
“我知道向量不能有负数个元素,所以返回无符号整数是完全合理的。” - 不,不是这样的。标准库使用无符号类型表示大小是由于历史原因。不要自己这样做。除非进行位运算,否则始终使用有符号整数。如有必要,请将容器大小转换为 int。请参见 https://dev59.com/iWkw5IYBdhLWcg3wDWTL#10168569 - Christian Hackl
4
@ChristianHackl 是的,无符号数有问题,但是将其转换为int并不是正确的答案。 即使在64位系统上,int也经常是32位。 如果您正在处理char,那么您实际上可以在内存中拥有超过2 ^ 31-1个char,这会引起问题。 - Daniel H
3
如果您想将 size_t 强制转换为有符号类型,ptrdiff_tint 更好。在 LP64 实现中,size_t 是 64 位,但 int 是 32 位。据我所知,Google 建议使用 ptrdiff_t,然后检查结果是否为非负数,而 Microsoft 建议使用 rsize_t,然后检查结果是否低于某个最大值,以便检测环绕情况。 - Davislor
显示剩余3条评论
2个回答

6
一种处理这个问题的方法是使用:
std::vector<Type>::size_type

作为函数参数/返回值的基础类型,或者在使用C++14时使用auto来返回。

谢谢,不幸的是那样做行不通。再次强调,我的一些内部变量是uin32_t类型。如果我使用std::vector<T>::size_type,不能保证它仍然是32位。我刚意识到我从未提到过这一点;问题已更新。 - tendim
2
@tendim 那你有点运气不好,因为在这种情况下设计并不是最好的。你唯一能做的就是强制转换。当使用通用类型时,最好也使用平台无关的数据类型,不要只局限于 uint32_t - vsoftco
谢谢确认我迄今为止的想法.. 使用宏是我找到的“最干净”的解决方案。 - tendim

5
以一组小点滴的形式回答:
  1. 在使用函数模板时,可以显式指定类型,而不是依赖于编译器来推导类型。例如:std::min<std::uint32_t>(4, my_vec.size());

  2. 打开所有与有符号和无符号比较以及隐式窄化转换相关的编译器警告。在可能的情况下使用花括号初始化,因为它将把窄化转换视为错误。

  3. 如果您明确想要使用32位值(如std::uint32_t),我会尽量找到最少的地方来显式转换(即使用static_cast)“大小”到较小的类型。你不想到处都是强制转换,但如果你在内部使用库容器大小并且你想让你的API使用std::uint32_t,请在API边界上显式转换,这样类的用户就不必担心自己进行转换。如果您可以将转换保留在仅几个地方,则可以实际添加运行时检查(即断言),以确保大小实际上没有超出较小类型的范围。

  4. 如果您不关心确切的大小,请使用std::size_t,对于所有标准容器,它几乎肯定与std::XXX::size_type相同。它在理论上可能不同,但实际上并不会发生。在大多数情况下,std::size_tstd::vector::size_type更简洁,因此是一个很好的折衷。

  5. 许多人(包括C++标准委员会的许多成员)会告诉您即使对于大小和索引也要避免使用无符号值。我理解并尊重他们的观点,但我认为它们不足以证明在与标准库接口处产生额外摩擦的价值。无论是否是历史遗留问题,事实是标准库广泛使用无符号大小。如果您使用其他内容,则代码将充斥着隐式转换,所有这些都是潜在的错误。更糟糕的是,这些隐式转换使得打开编译器警告变得不切实际,因此所有这些潜在的错误仍然相对隐形。(即使您知道大小永远不会超过较小类型,被迫关闭有符号性和窄化的编译器警告意味着您可能会错过完全不相关的代码中的错误)。尽可能匹配您正在使用的API的类型,在必要时断言和显式转换,打开所有警告。

  6. 请记住,auto不是万能的。 for (auto i = 0; i < my_vec.size(); ++i) ...for (int i ...一样糟糕。但如果您通常更喜欢算法和迭代器而不是原始循环,则auto将帮助您走得更远。

  7. 在除法中,除非您知道分母不为0,否则不要进行除法运算。同样,对于无符号整数类型,除非您知道被减数小于或等于原始值,否则不要进行减法运算。如果您可以养成这


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