Array Iteration Patterns and Solutions
Apply practical patterns to handle signed/unsigned index conversions.
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:
- Choosing the signed type
- Getting the length as a signed value
- 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.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Array Iteration Patterns and Solutions - Quiz
Test your understanding of the lesson.
Practice Exercises
Array Indexing with data() Method
Create a program that demonstrates the recommended approach for using signed loop variables with vectors. Use the data() method to access the underlying C-style array, which accepts both signed and unsigned indices without conversion warnings.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!