Table of Contents

In this article, I’ll show another technique that avoids mixing signed and unsigned types.

In my article Integer Conversions and Safe Comparisons in C++20 we learned about cmp_* integer comparison functions that allow us to compare various types of integer types. The functions are safe because they help with mixing signed and unsigned types. In C++20, there’s another handy feature: the non-member std::ssize() function that returns a signed number of elements in a container.

Let’s see how to use it.

Use cases  

C++20 offers many ways to avoid working with raw array/container indices:

  • Algorithms (they can avoid raw loops),
  • Ranges and views (for example, views::reverse),
  • Iterators,
  • Range based for loop with an initializer (for example, to update the index count).

On the other hand, you may sometime need to write/read or fix a loop like:

void printReverse(const std::vector<int>& v) {
    for (auto i = v.size()-1; i >= 0; --i)
        std::cout << i << ": " << v[i] << '\n';
}

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

Can you see the error?

You can even try running that on Compiler Explorer

I get the following output:

4: 5
3: 4
2: 3
1: 2
0: 1
18446744073709551615: 0
18446744073709551614: 33
18446744073709551613: 0
...

Obviously, the code is wrong as it loops infinitely!

A developer who wrote the code worked in a signed “arithmetic” and assumed auto i = ... would give him a signed type. But since v.size() returns size_t then the i index will be also size_t.

In C++20 we have now an option to use ssize() - a non member function that returns a signed value:

void printReverseSigned(const std::vector<int>& v) {
    for (auto i = std::ssize(v)-1; i >= 0; --i)
        std::cout << i << ": " << v[i] << '\n';
}

Now, we stay in the “signed” camp and can use our regular arithmetic.

As a side note, you can stay “unsigned”, but you have to use the modulo 2 arithmetic:

void printReverseUnsigned(const std::vector<int>& v) {
    for (size_t i = v.size()-1; i < v.size(); --i)
        std::cout << i << ": " << v[i] << '\n';
}
Make sure you use compiler options like -Wall, -Wsign-conversion, -Wsign-comparison (GCC/Clang), or /Wall on MSVC to catch all errors and warnings related to mixing signs.

I also found this code in Firefox (using this code search):

// actcd19/main/f/firefox-esr/firefox-esr_60.5.1esr-1/gfx/angle/checkout/src/libANGLE/Program.cpp:1570:

const LinkedUniform &Program::getUniformByLocation(GLint location) const
{
    ASSERT(location >= 0 && static_cast<size_t>(location) <           
           mState.mUniformLocations.size());
    return mState.mUniforms[mState.getUniformIndexFromLocation(location)];
}

Since the location is signed, then we could use ssize() and avoid static casting:

ASSERT(location >= 0 && location < std::ssize(mState.mUniformLocations);

The proposal  

The proposal for the feature, that got accepted in C++20, is P1227R2, written by Jorg Brown. The paper also shows the following example:

template <typename T>
bool has_repeated_values(const T& container) {
  for (int i = 0; i < container.size() - 1; ++i) {
    if (container[i] == container[i + 1]) return true;
  }
  return false;
}

In that case, we’ll get an error when the container is empty, but it can also be fixed with ssize() or properly fixing the logic.

The need for signed size came from the design of std::span. Initially, it had signed indices and size, but to conform to the STL rules, it was changed to unsigned types.

ssize() joined other non-member functions available since C++11 and C++17:

  • size() since C++17
  • begin()/end() and cbegin()/cend(), since C++11
  • empty() since C++17

Thanks to ADL (Argument Dependent Lookup) you can also use those functions without writing std:::

std::vector vec = ...
if (!empty(vec)) {
    for (int i = 1; i < ssize(vec); ...) {
        ...
    }
}

Implementation  

If we look into MSVC code, we can see the implementation for ssize():

template <class _Container>
_NODISCARD constexpr auto ssize(const _Container& _Cont)
    -> common_type_t<ptrdiff_t, make_signed_t<decltype(_Cont.size())>> {
    using _Common = common_type_t<ptrdiff_t, 
                    make_signed_t<decltype(_Cont.size())>>;
    return static_cast<_Common>(_Cont.size());
}

template <class _Ty, ptrdiff_t _Size>
_NODISCARD constexpr ptrdiff_t ssize(const _Ty (&)[_Size]) noexcept {
    return _Size;
}

In short the functions return a compatible signed type, usually ptrdiff_t. ptrdiff_t is used for pointer arithmetic and it’s signed.

Both size_t and ptrdiff_t are usually 64-bit types on x86-64 and 32 bits on x86, so it’s a rare chance your containers will hold more elements than ptrdiff_t can represent. On x86 systems, you have 31 bits for the size, while on 64-bit systems, you have 63 bits (to represent a positive signed value). It’s highly unlikely that you’ll reach that size and won’t use full memory by that time.

Controversy  

During the research, I also found a controversy around having unsigned as a return type for size(). In some cases, unsigned can be considered as another “wrong default” for C++. The reason is related to the fact that pointer arithmetic might yield negative signed values, while we often have sizes that are unsigned. Mixing those types can lead to various hard-to-find runtime errors.

Some people prefer to be in the “unsigned” camp, while others are in the “signed” one. In real life, there are chances that you need to go outside your camp and use the opposite sign for a variable. In that case, be sure to use appropriate arithmetic functions to prevent errors.

Some notes:

Summary  

This short article showed another option to deal with sizes for various containers and arrays. If you have a signed type and want to compare it with size, then use ssize(), which returns ptrdiff_t, a signed type.

Regarding computations, follow the Arithmetic section in the C++ Core Guideline. Especially ES.100: Don’t mix signed and unsigned arithmetic and ES.102: Use signed types for arithmetic.

Back to you

Are you in the “signed” or “unsigned” camp? Have you got any issues with size() being unsigned? Share your feedback in the comments below.