使用负整数和size_t一起是否安全?

12

我刚刚看到了一些C++代码。它使用条件来决定是向前还是向后遍历一个std::vector。编译器没有报错,但我认为size_t是无符号的。这样做是否危险?

vector<int> v { 1,2,3,4,5 };    
bool rev = true;

size_t start, end, di;
if (rev) {
    start = v.size()-1;
    end = -1;
    di = -1;
}
else {
    start = 0;
    end = v.size();
    di = 1;
}

for (auto i=start; i!=end; i+=di) {
    cout << v[i] << endl;
}

1
标准将std::string::npos定义为static const size_type npos = -1;,这基本上是一种类似的技巧。如果要严谨一些,您可能更喜欢:std::vector<int>::size_type start, end, di; - Galik
@Galic:我看不出这种严谨(你所说的)有什么作用,有什么优势。如果维护者费尽心思地通过实例化带有愚蠢大小类型的分配器来破坏事情,那么这种严谨只会增加他或她成功搞砸事情的机会。就我看来。 - Cheers and hth. - Alf
@Cheersandhth.-Alf 你说得可能没错。据我所知,标准规定std::vector<int>::size_type必须是无符号的,但我认为它可以与std::size_t的大小不同。虽然我想大多数实现都会将size_type设为与size_t相同。 - Galik
4个回答

10

使用无符号整数(size_t是无符号的)进行这种方式的定义是很明确的,具有环绕功能:这种行为是由标准保证的,而不是使用有符号整数,其中标准没有保证。

然而,这种方法是不必要的聪明。

作为一般规则,为了避免由于隐式包装升级到无符号整数而导致的问题,对于位级别的东西,请使用无符号整数,对于数字,请使用有符号整数。如果您需要与size_t相对应的有符号整数,则有ptrdiff_t。定义一个带有有符号结果的n_items函数,例如:

using Size = ptrdiff_t;

template< class Container >
auto n_items( Container const& c )
    -> Size
{ return end( c ) - begin( c ); }

只需这样,编译器就不会再提示无意义的警告了。


与太过巧妙的代码不同

vector<int> v { 1,2,3,4,5 };    
bool rev = true;

size_t start, end, di;
if (rev) {
    start = v.size()-1;
    end = -1;
    di = -1;
}
else {
    start = 0;
    end = v.size();
    di = 1;
}

for (auto i=start; i!=end; i+=di) {
    cout << v[i] << endl;

做例如

const vector<int> v { 1,2,3,4,5 };    
const bool reverse = true;  // whatever

for( int i = 0; i < n_items( v );  ++i )
{
    const int j = (reverse? n_items( v ) - i - 1 : i);
    cout << v[j] << endl;
}

n_items() 应该被缓存在循环变量中,以避免在每次循环迭代时潜在地、可能地依赖于容器类型调用/计算大小,从而导致昂贵的开销。 - underscore_d

3
每当我需要处理有符号类型时,我总是使用:
typedef std::make_signed<std::size_t>::type ssize_t; // Since C++11

作为std::size_t的有符号替代方案。

我很感激这个问题已经存在几年了,但我希望这将有助于其他人。致谢moodycamel::ConcurrentQueue


2
更标准的做法是使用ptrdiff_t,我认为它应该与您的ssize_t相同。 - Alan Baljeu

1
我无法保证这段代码的安全性,但我认为它的风格很差。更好的方式是使用支持正向或反向迭代的迭代器。
例如:
std::vector<int> v = { 1, 2, 3, 4, 5 };
bool rev = true;

if (rev)
{
    for (auto itr = v.rbegin(); itr != v.rend(); ++itr)
    {
        std::cout << *itr << "\n";
    }
}
else
{
    for (auto itr = v.begin(); itr != v.end(); ++itr)
    {
        std::cout << *itr << "\n";
    }
}

那段代码在g++ 4.8.2下无法编译。你能想到任何原因吗? - Cheers and hth. - Alf
1
我不确定(可能与*rend()begin()*不会评估为相同类型有关)。这是一个概念性的答案;我没有测试过。我会更新一些实际编译的内容。 - James Adkison
1
说到好的编程风格,你应该在循环中声明变量来缓存“end”迭代器,以避免在每次迭代时潜在地调用/实例化一个新的副本。;-) - underscore_d
@underscore_d 你检查过了吗?坚持使用常见的习语可以为编译器优化代码提供最好的机会,它们通常比人类做得更好。 :) - James Adkison
我理解你在抽象上的观点,但是在我看来,写for (auto it = c.begin(), end = c.end(); it != end; ++it)只需要很少的额外输入 - 而且很难想象优化器能做得更好 - 所以我更喜欢将其拼写出来。我认为我们可以做一些小事情来确保最佳性能,这并不算过早地进行优化,但这可能只是我的定义!(我不喜欢的是end应该是const,但它不是!但我倾向于避免在循环外声明它为const,除非已经有其他东西存在。) - underscore_d

0

在使用size_t时,使用负整数是否安全?

不是的,这是危险的。会发生溢出。

size_t a = -1;
std::cout << a << "\n";

输出:

4294967295 // depends on the system, largest value possible here

5
这不是危险的溢出,而是明确定义的环绕。将负数转换为size_t是一种常见的方式,用于从通常返回大小的函数中返回错误;例如C标准库中的函数mbrtoc32()就是这样做的。 - dpi

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