Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Array Iteration Patterns and Solutions
Apply practical patterns to handle signed/unsigned index conversions.
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:
- 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.
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!