decrement size_t in loop

Pages: 12
Hi everyone,

I was recommended to turn on compiler warnings for debugging purposes.
The compiler frequently complained that I was comparing signed and unsigned integers in my loop heads. Thinking it to be better practice to turn all loop counters to size_t, I ran into a problem with a loop that is supposed to decrement to 0 and exit then. As size_t will never become -1 (instead it seems to become the largest possible value), this will turn into an infinite loop. Fortunately, the compiler warned me about this as well.

How can I write the following correctly?
for (size_t i = my_vector.size() - 1; i >=0; i -= my_decrement)

Best,
PiF
Last edited on
I would suggest to increment instead of decrement:
1
2
3
4
for (size_t i = 0; i < my_vector.size(); i += my_decrement)
{
  my_vector[my_vector.size() - 1 - i] = ....
}
You would code a decrementing size_t loop as below. Alternatively use a reverse iterator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <vector>
#include <iostream>

int main() {
	const std::vector nums { 1, 2, 3, 4, 5 };

	for (auto i { nums.size() }; i > 0; --i)
		std::cout << nums[i - 1] << ' ';

	std::cout << '\n';

	for (auto i { nums.rbegin() }; i != nums.rend(); ++i)
		std::cout << *i << ' ';

	std::cout << '\n';
}



5 4 3 2 1
5 4 3 2 1

Another option that works with arbitrary step sizes:
1
2
3
4
5
6
7
8
9
assert(my_vector.size() > 0);
for (size_t i = my_vector.size() - 1; /*true*/; i -= my_decrement)
{
	my_vector[i];
	if (i < my_decrement)
	{
		break;
	}
}


Note: If vector.size() is not a multiple of your step size, then the last index that will be visited is not 0.
Last edited on
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <vector>
#include <iostream>

int main() {
	constexpr size_t mydec { 2 };
	const std::vector nums { 1, 2, 3, 4, 5 };

	for (auto i { nums.size() }; i > 0; i -= i >= mydec ? mydec : i)
		std::cout << nums[i - 1] << ' ';

	std::cout << '\n';

}



5 3 1

Last edited on
for (size_t i = my_vector.size() - 1; i >=0; i -= my_decrement)

I would just use an int rather than a size_t, acknowledge then ignore the warning.

That line of code would fail at the start if my_vector.size() was zero.

Any subtraction with unsigned ints is fraught with danger (including i -= my_decrement) and they stop you relying on the normal laws of arithmetic. Personally, I'm not fond of them.



Last edited on
I should just use an int rather than a size_t, acknowledge then ignore the warning.


unless the number of elements exceeds the maximum positive value of a type int...

That line of code would fail anyway if my_vector.size() was zero.


Yes. that code had a previous assert to deal with this, but 0 size could be valid and IMO the code should work as expected with 0 size.

Yes - there are known 'issues' mixing signed and unsigned.

Since c++20 there is std::ssize() which returns a signed type (ptrdiff_t). But if compiled as 64-bit with int 32 bit there's still a potential issue with assigning a value of type ptrdiff_t to an int.

Consider as C++20:

1
2
3
4
5
6
7
8
9
10
11
12
#include <vector>
#include <iostream>

int main() {
	const std::vector nums { 1, 2, 3, 4, 5 };

	for (auto i { std::ssize(nums) - 1 }; i >= 0; --i)
		std::cout << nums[i] << ' ';

	std::cout << '\n';

}

One of the problems with an unsigned int is that it is well-nigh impossible to check the intrinsic conversion to it, since it can't overflow.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

unsigned area( unsigned width, unsigned height )
{
   // Check that the user input positive width and height ...
   // ... whoops, no way of doing that!

   return width * height;
}


int main()
{
   std::cout << area(  2, 6 ) << '\n';
   std::cout << area( -2, 6 ) << '\n';
}





unless the number of elements exceeds the maximum positive value of a type int...

Then use a long long instead of int.
Last edited on
Yes - but the issue here is that .size() returns unsigned type size_t which is 32/64 bit compile aware.

IMO what the standards committee should do is to introduce .ssize() for every instance of where .size() is currently available to return a signed type - and introduce type ssize_t as being the same as ptrdiff_t (which is 64/32 bit aware). Then we can all move forward using nice signed numbers.

But without having .ssize(), then:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <vector>
#include <iostream>
using ssize_t = ptrdiff_t;

int main() {
	constexpr ssize_t mydec { 2 };
	const std::vector nums { 1, 2, 3, 4, 5 };

	for (ssize_t i = std::ssize(nums) - 1; i >= 0; i -= mydec)
		std::cout << nums[i] << ' ';

	std::cout << '\n';

}

Last edited on
I understand now why Java, and maybe other languages, don't have unsigned types.
They seem to cause many problems.
Java does support unsigned integer arithmetic.
Class java.lang.Integer provides operations like divideUnsigned(), compareUnsigned etc.
I had to code a simple CRC in java about 3 years ago. The code was really screwy trying to use the required unsigned values in many places, tons of function calls that should have just been logic or arithmetic. You can force it to do it, but yuck. Its a lot better to have access to it directly when you need it -- the code with all that junk is very hard to read, and what did it gain you? You can still make the same mistakes with it, its just now in an ugly wrapper. This is like the C++ style casting argument -- its supposed to be annoying to type and aggravating to read, because WE decided that YOU shouldn't be doing it and it is offered only grudgingly. Or, closer to home, its like making a math tool class in C++ and refusing to use operator overloading so
a = b+c
becomes
a = b.add(c)
and more complicated statements explode into utter garbage.
Last edited on
Thanks for the examples how I could write my loop.

From the discussion, it seems like it is not obvious what "best practice" is in this case.
I believe I will really stick with signed ints for now and, as @lastchance put it,
acknowledge, then ignore the warning.
If the required number of loop iterations gets too large for a signed int, the resulting bug will be easier to spot than a bug resulting from the difference of two unsigned ints that should be negative but instead gets wrapped around to positive values. I do this a couple of times in my code and it would confuse the hell out of me.
Last edited on
There is a third form of for loop, known as range-based for loop:
https://en.cppreference.com/w/cpp/language/range-for

The idea of a reverse for loop is easy using numeric indexing or reverse iterators, as others have shown.

It wasn't until C++20 the idea of a C++ stdlib reverse range-based for loop could easily be done, 3rd party libraries such as Boost provided that functionality beforehand, though using one requires including/importing <ranges>:
https://www.fluentcpp.com/2020/02/11/reverse-for-loops-in-cpp/

I use range-based for loops all the time when I don't need to worry about indexing or dereferencing a pointer.

C++20 added a nice variant to range-based for loops, init statements. See the cppreference link for details.
They seem to cause many problems.


Oh yes - particularly if there is mixed signed/unsigned. The issue with a mixed relation is that signed is 'promoted' to unsigned. Even if the signed number is negative, it is 'promoted' to a large unsigned number before the operation (or condition) with the unsigned number is undertaken. This can give rise to many 'wrong' situations.

Consider:

-1 < 0
-1 < 0U

Now even children would probably say correctly that -1 is less than 0. But not in the mad world of unsigned through the looking glass. The first is indeed evaluated as true as expected (both are signed). However in the second, -1 is promoted to unsigned (a large number) as 0U is now unsigned, which is much greater than 0. So the second expression is evaluated as false!

1
2
3
4
5
6
7
#include <iostream>
#include <iomanip>

int main() {
	std::cout << "-1 < 0 " << std::boolalpha << (-1 < 0) << '\n';
	std::cout << "-1 < 0U " << std::boolalpha << (-1 < 0U) << '\n';
}



-1 < 0 true
-1 < 0U false


Nurse!
Last edited on
You could separate the counter from the index
1
2
3
4
for (size_t cnt = 0; cnt < my_vector.size();  cnt += my_decrement) {
    size_t i = my_vector.size() - cnt - 1;
    ...
}
As a finale to astound and confound (don't try this at work!) then:

1
2
3
4
5
6
7
8
9
10
11
12
#include <vector>
#include <iostream>

int main() {
	constexpr size_t mydec { 2 };
	const std::vector nums { 1, 2, 3, 4, 5 };

	for (auto i { nums.size() - 1}; i < nums.size(); i -= mydec)
		std::cout << nums[i] << ' ';

	std::cout << '\n';
}



5 3 1

I believe I will really stick with signed ints for now and, as @lastchance put it,
acknowledge, then ignore the warning.


You could also cast the result from .size() et al to an int using a static cast eg

 
int i {static_cast<int>(nums.size())};


This would remove the warnings and show in the code where these 'conversions' were being done.
Pages: 12