As we discussed last week, comparing numbers with different signs can be dangerous in C++. If you try to compare a signed with an unsigned integer, you might get a result that makes no sense if you approach the question from a mathematical point of view. At least, with the right compiler settings, you’d get a warning.
We also saw that C++20 offers an easy and safe way to compare numbers anytime and it will always return you the result you’d expect.
As such, the warnings also go away.
Whatever project I work on, introducing or using a stricter set of warnings and treating them as errors come up over time. An unsafe integer comparison is one of the nastiest warnings to fix because the fix is sometimes not evident but at the same time, usually, you have a lot of similar warnings.
In this article, I’ll share with you according to my experience the 3 most common ways -Wsign-compare
warnings are invoked.
Using a wrongly typed loop control variable
The simplest and the most frequent form of this abuse is when the loop control variable is wrongly typed. Most often it means that the loop control is either declared as int i = 0
or auto i = 0
which are both signed integers, but they are compared against the size of a standard container which is a size_t
.
1
2
3
4
5
std::vector<int> v;
for(int i = 0; i < v.size(); ++i) {
std::cout << i;
}
This is not very dangerous because on most platforms an int
is 4 bytes and a size_t
is an unsigned long long
that is 8 bytes. As such, in the comparison i
is statically cast to an unsigned long
or unsigned long long
which is safe as long as i
is positive. If i
is negative you will have surprises, but if you modify i
in the body of your loop, you have bigger design issues.
Another possible problem is that a signed int
and therefore i
cannot represent all the positive values size_t
can, so with numbers that require a larger type than int
to represent positive values, this comparison will fail. To be fair, that is rarely a problem.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <limits>
int main()
{
std::cout << "std::numeric_limits<int>::max(): " << std::numeric_limits<int>::max() << '\n';
std::cout << "std::numeric_limits<size_t>::max(): " << std::numeric_limits<size_t>::max() << '\n';
return 0;
}
/*
std::numeric_limits<int>::max(): 2147483647
std::numeric_limits<size_t>::max(): 18446744073709551615
*/
To fix this issue you simply have to change the declaration of i
to size_t i = 0
or to auto i = 0uz
(on C++23), auto i = 0u
is not a good option, read here why.
I also saw that some user-defined containers return int
or ssize_t
as a length, and the loop control variable is defined as size_t
. As long as the maximum - due to a potential error condition - is not negative, all is fine. But if the ssize_t length()
method might return -1
for example, you’ll end up with a loop that will go until one before the maximum value of a size_t
(std::numeric_limits<size_t>::max()-1
)
This is also easy to fix, you simply have to match the type of the loop index with the type of the other value in the stop condition.
Mixed conditions in loop control
A bit more complex and slightly less frequent case is when you set a more complex stop condition in a loop.
Take this example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
class Container {
public:
size_t length() const {
return 42;
}
};
int maxLength() {
return 576;
}
int main() {
Container c;
for (size_t i = 0; i < c.length() && i <= maxLength(); ++i) {
std::cout << i << '\n';
}
return 0;
}
You cannot fix such an error by modifying the type of the loop index. If i
is signed, the first condition will emit a signed/unsigned comparison warning, if it’s unsigned, then the second one will.
The fix depends on your possibilities in terms of modifying code.
If you can safely and rightly change the type of one of the variables in the expression, then do so. In our case that would either be the return type of Container::length()
or the return type of maxLength()
.
If you cannot do that for any reason, you have to make that comparison safe one way or another.
If you can, use the standard integer comparison tools that I wrote about last week. In the above example, the for
-loop control block would look like this:
1
for (size_t i = 0; i < c.length() && std::cmp_less_equal(i, maxLength()); ++i)
That’s safe and still quite readable.
If you don’t have access to it, backport it. You have a reference implementation on C++ Reference and you can check the actual implementations on GitHub (clang here, gcc here, msvc here).
If you don’t want to backport it… I think you really should… But let’s imagine that you don’t. In that case, use a static_cast
to cast one of the values in the conditional to overcome the warning. To choose your cast, you have to think wisely to make sure that nothing can go wrong considering the potential values of both variables.
In our case, we should ask ourselves the question if it’s safe to cast i
to an int
. For that we have to know if i
can have a value that is bigger than std::numeric_limits<int>::max()
. If it might be larger, then casting it to int
potentially leaves us with a negative value that we clearly don’t want. If we know for sure that the container in this case cannot be so large (because for example, it contains the number of countries in the world), we can go ahead. We can also cast it to a larger type. So for example instead of an int
we can use a long long
.
Or we have to consider if it’s safe to cast the other value, in this case, that would mean casting maxLength()
to a size_t
. Again, for that, we have to know if realistically it can return a negative value. If so, it’s an unsafe cast.
It’s better to backport those new integer comparison utilities.
Saving values into the wrong type
Another frequent root cause behind signed/unsigned comparisons is that one of the variables participating in the comparison is saved into the wrong type. The called method would return the right type, but it’s saved into a wrongly typed variable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <vector>
size_t foo() {
return 42;
}
int main() {
std::vector<int> v;
int bar = foo();
if (v.size() == bar) {
std::cout << "same size\n";
} else {
std::cout << "different size\n";
}
}
You’d be surprised to see how frequently this is the case and how ignorant we - developers - are.
As std::vector<T>::size()
returns size_t
and bar
is an int
, we have a warning. But in such cases, it’s easy to fix the problem, we simply have to update the declaration of bar
so that it matches the return type of foo
and the problem is gone.
Conclusion
In this article, we reminded ourselves how comparing signed with unsigned types can go wrong. Then I shared with you the 3 most common cases I encountered while fighting against -Wsign-compare
warnings.
They are often trivial to fix and they are often the result of ignorance and bad compiler settings. But there might be some cases when using the C++ intcmp utilities is the best way to go.
Next week, let’s continue with the three strangest cases I encountered battling signed/unsigned comparison warnings.
Connect deeper
If you liked this article, please
- hit on the like button,
- subscribe to my newsletter
- and let’s connect on Twitter!