What Are the Sign Challenge Solutions for Arrays and Loops?

Because the C++ standard library uses unsigned types for container lengths and indices while signed integers are generally preferred for quantities, a tension arises when writing loops over containers. This lesson presents practical solutions to this common challenge.

In lesson 4.5, we discussed preferring signed integers for quantities. However, lesson 16.3 revealed that std::vector (and other standard containers) uses unsigned types (std::size_t) for lengths and indices. This creates tension when writing loops.

The problem with unsigned loop variables

Consider this attempt to print a vector in reverse:

#include <iostream>
#include <vector>

template <typename T>
void printReversed(const std::vector<T>& vec)
{
    for (std::size_t i{ vec.size() - 1 }; i >= 0; --i)  // i is unsigned
        std::cout << vec[i] << ' ';

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

int main()
{
    std::vector data { 10, 20, 30, 40, 50 };
    printReversed(data);

    return 0;
}

This prints the values in reverse, then exhibits undefined behavior. The problem: when i is 0 and decrements, it wraps around to a massive positive value (since unsigned types don't go negative). This creates an out-of-bounds index on the next iteration.

Additionally, i >= 0 is always true for unsigned types, creating an infinite loop.

Using signed loop variables

Signed integers avoid wraparound:

#include <iostream>
#include <vector>

template <typename T>
void printReversed(const std::vector<T>& vec)
{
    for (int i{ static_cast<int>(vec.size()) - 1 }; i >= 0; --i)
        std::cout << vec[static_cast<std::size_t>(i)] << ' ';

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

int main()
{
    std::vector data { 10, 20, 30, 40, 50 };
    printReversed(data);

    return 0;
}

This works correctly, but the casts clutter the code significantly.

Evaluating the solutions

There's no perfect solution—each approach involves trade-offs. Here are the options, ordered from least to most recommended:

Option 1: Disable sign conversion warnings (not recommended)

Simply turn off warnings about signed/unsigned conversions. This is the simplest but worst option, as it silences legitimate warnings too.

Option 2: Use unsigned loop variables

If you choose unsigned variables, several unsigned types work:

Using size_type with explicit template arguments:

#include <iostream>
#include <vector>

int main()
{
    std::vector data { 100, 200, 300 };

    for (std::vector<int>::size_type idx{ 0 }; idx < data.size(); ++idx)
        std::cout << data[idx] << ' ';

    return 0;
}

Using size_type in function templates:

#include <iostream>
#include <vector>

template <typename T>
void printAll(const std::vector<T>& vec)
{
    for (typename std::vector<T>::size_type idx{ 0 }; idx < vec.size(); ++idx)
        std::cout << vec[idx] << ' ';
}

int main()
{
    std::vector data { 100, 200, 300 };
    printAll(data);

    return 0;
}

Note the required typename keyword when size_type depends on a template parameter.

Using std::size_t directly:

Since size_type is almost always std::size_t, most developers use it directly:

for (std::size_t idx{ 0 }; idx < vec.size(); ++idx)
    std::cout << vec[idx] << ' ';

Unless you're using custom allocators (you probably aren't), this is reasonable.

Option 3: Use signed loop variables

Using signed variables is consistent with general best practices but requires addressing three issues:

  1. Choosing the signed type
  2. Getting the length as a signed value
  3. Converting the signed variable to an unsigned index

Choosing the signed type:

Use int for smaller arrays, std::ptrdiff_t for very large arrays, or define your own alias:

using Index = std::ptrdiff_t;

for (Index idx{ 0 }; idx < static_cast<Index>(vec.size()); ++idx)
    std::cout << vec[idx] << ' ';

In C++23, use the Z suffix for signed size literals:

for (auto idx{ 0Z }; idx < static_cast<std::ptrdiff_t>(vec.size()); ++idx)
    // ...

Getting signed length:

Pre-C++20, cast the result:

auto length{ static_cast<int>(vec.size()) };
for (auto idx{ 0 }; idx < length; ++idx)
    std::cout << vec[idx] << ' ';

C++20 provides std::ssize():

for (auto idx{ 0 }; idx < std::ssize(vec); ++idx)
    std::cout << vec[idx] << ' ';

Converting to unsigned for indexing:

Option A: Static cast at each use:

std::cout << vec[static_cast<std::size_t>(idx)] << ' ';

Option B: Use a short helper function:

template <typename T>
constexpr std::size_t toUZ(T value)
{
    static_assert(std::is_integral<T>() || std::is_enum<T>());
    return static_cast<std::size_t>(value);
}

// Usage:
std::cout << vec[toUZ(idx)] << ' ';

Option C: Index the underlying C-style array:

std::cout << vec.data()[idx] << ' ';

C-style arrays accept both signed and unsigned indices, avoiding conversion issues entirely.

Option 4: Index the underlying C-style array (recommended)

This combines signed loop variables with C-style array indexing:

int main()
{
    std::vector data { 100, 200, 300 };

    auto length{ static_cast<int>(data.size()) };
    for (auto idx{ 0 }; idx < length; ++idx)
        std::cout << data.data()[idx] << ' ';

    return 0;
}

Benefits:

  • Signed loop variables (consistent with best practices)
  • No custom types or aliases needed
  • Minimal readability impact from data()
  • No performance penalty in optimized code

Option 5: Avoid indexing altogether (best practice)

The cleanest solution: don't use indices at all. Range-based for loops and iterators provide index-free traversal:

#include <iostream>
#include <vector>

int main()
{
    std::vector data { 100, 200, 300 };

    for (const auto& value : data)  // Range-based for loop
        std::cout << value << ' ';

    return 0;
}

No indices means no signed/unsigned problems!

Best practice: Avoid array indexing with integral values whenever possible. Use range-based for loops or iterators instead.