Arrays, loops, and sign challenge solutions

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.