What Is size_t? Why C++ Loops Prefer size_t Over int
Subtitle / Abstract
When you iterate containers with a for loop, size_t is often safer and closer to the intended meaning than int. This post uses the ACERS structure to explain what size_t is, why it is used, the common pitfalls, and practical patterns for production C++.
Meta
- Reading time: 8-10 minutes
- Tags: C++, size_t, type system, loops, STL
- SEO keywords: size_t usage, size_t vs int, C++ loop initialization, size_t underflow
- Meta description: Explain size_t and why loops often use it, with safe patterns and engineering scenarios.
Target readers
- C++ beginners who are new to
size_t,sizeof, and containersize()return types - Mid-level engineers who have seen
-Wsign-comparewarnings or unsigned underflow bugs - Engineers writing cross-platform or high-performance C++
Background / Motivation
In C++ code, you often see loops like:
for (size_t i = 0; i < vec.size(); ++i) { ... }
Common questions:
- Why not use the more “obvious”
int? - What exactly is
size_t, and why is it unsigned? - Where are the pitfalls?
This article answers those questions.
A - Algorithm (Problem and Approach)
The question
Why use size_t for loop indices and sizes instead of int in C++?
This is fundamentally about type semantics and API consistency:
size_tis the standard type for object sizes and indicesintis a signed counter with different semantics
Basic example 1: container size and index
#include <vector>
std::vector<int> v{1, 2, 3};
for (std::size_t i = 0; i < v.size(); ++i) {
// i matches v.size() type; no signed/unsigned warning
}
Basic example 2: unsigned underflow
#include <cstddef>
std::size_t n = 0;
std::size_t x = n - 1; // not -1, but a very large positive number
Concept sketch:
size_t (unsigned) : 0 ---------------------> SIZE_MAX
int (signed) : -2^(N-1) ---- 0 ---- 2^(N-1)-1
Key point:
size_tcannot represent negative numbers; subtraction can wrap to a huge value.
C - Concepts (Core Ideas)
What is size_t?
size_tis an unsigned integer type that can represent the size of any object.sizeofreturnssize_t.- On 64-bit systems it is typically 64-bit; on 32-bit systems it is typically 32-bit.
#include <cstddef>
std::size_t n = sizeof(int);
What category does this belong to?
- Type semantics: use types to express “size/index”
- API consistency: matches container
size()signatures - Portability: guaranteed to represent any object size
Key model
sizeof(T) -> size_t- Range:
0 <= size_t <= SIZE_MAX SIZE_MAX = 2^N - 1(N is the bit width)
Practical steps (with commands)
- Include the header:
#include <cstddef>forstd::size_t. - Align with API: use
std::size_torcontainer::size_typefor sizes/indices. - Cache bounds: store
n = v.size()to avoid repeated calls and unsigned pitfalls. - Avoid unsigned underflow: do not write
v.size() - 1on possibly empty containers. - Reverse iteration: use
for (size_t i = n; i-- > 0;)orstd::ssize. - Enable warnings:
-Wsign-compareto surface issues early.
# g++ example
g++ -std=c++20 -Wall -Wextra -Wsign-compare main.cpp -o demo
./demo
Runnable example: safe size_t loops
#include <cstddef>
#include <iostream>
#include <utility>
#include <vector>
int main() {
std::vector<int> a{5, 2, 4, 6, 1};
for (std::size_t i = 0; i + 1 < a.size(); ++i) {
bool swapped = false;
std::size_t n = a.size() - i;
for (std::size_t j = 0; j + 1 < n; ++j) {
if (a[j] > a[j + 1]) {
std::swap(a[j], a[j + 1]);
swapped = true;
}
}
if (!swapped) break;
}
for (int x : a) std::cout << x << ' ';
std::cout << '\n';
// Safe reverse iteration
for (std::size_t i = a.size(); i-- > 0; ) {
std::cout << a[i] << ' ';
}
std::cout << '\n';
}
Why size_t is the better fit
- Clearer semantics:
size_tmeans “size/length”,intmeans “signed count”. - Larger range: on 64-bit systems,
intis usually 32-bit and may overflow on huge containers. - API matching:
vector::size()andstring::size()returnsize_t. - Fewer implicit conversions: mixing
intandsize_ttriggers-Wsign-compareand can break logic.
E - Engineering (Real-world Usage)
Below are three real engineering scenarios with background, rationale, and runnable examples.
Scenario 1: Large-scale batch processing (C++)
Background: At billion-scale data, container sizes can exceed 2^31.
Why it fits: size_t can represent the range and aligns with STL.
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
std::vector<int> data(5, 1);
std::size_t sum = 0;
for (std::size_t i = 0; i < data.size(); ++i) {
sum += static_cast<std::size_t>(data[i]);
}
std::cout << sum << '\n';
}
Scenario 2: Memory allocation and buffers (C)
Background: C APIs like malloc and memcpy use size_t for byte counts.
Why it fits: consistent across platforms and safe for large allocations.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t n = 5;
int *p = (int*)malloc(n * sizeof(int));
if (!p) return 1;
for (size_t i = 0; i < n; ++i) p[i] = (int)i;
for (size_t i = 0; i < n; ++i) printf("%d ", p[i]);
printf("\n");
free(p);
return 0;
}
Scenario 3: Cross-platform library APIs (C++)
Background: API functions take buffer length parameters.
Why it fits: size_t is the universal size type for callers on different platforms.
#include <cstddef>
#include <cstdint>
#include <iostream>
std::uint8_t checksum(const std::uint8_t* buf, std::size_t len) {
std::uint8_t acc = 0;
for (std::size_t i = 0; i < len; ++i) {
acc ^= buf[i];
}
return acc;
}
int main() {
std::uint8_t payload[] = {1, 2, 3, 4};
std::cout << static_cast<int>(checksum(payload, sizeof(payload))) << '\n';
}
R - Reflection (Deep Dive)
Time and space complexity
- The loop examples are typically O(n) time
- O(1) extra space
This is independent of int vs size_t; the difference is correctness and maintainability.
Alternative approaches
| Option | Pros | Cons | Use cases |
|---|---|---|---|
int index | Simple | Small range, signed/unsigned mismatch | Small data, teaching examples |
size_t index | Large range, API match | Unsigned underflow risk | Most size/index cases |
std::ssize | Signed, safe reverse | Requires C++20 | When negative values are meaningful |
| Iterators/range for | Safest | No index | When you do not need indices |
Why this approach is most practical
size_tis the standard size type with best compatibility.- Safe patterns avoid underflow pitfalls.
- Aligns naturally with STL APIs and avoids warnings.
Common questions and pitfalls
- Is
size_talways 64-bit? No, it depends on platform width. - Is
auto i = 0OK? It deducesint, notsize_t. - Why is
v.size() - 1dangerous? Underflows on empty containers. - Why is
for (size_t i = n - 1; i >= 0; --i)wrong?i >= 0is always true for unsigned. - Does
intavoid underflow? It avoids unsigned underflow but introduces range and conversion risks.
Best practices
- Prefer
std::size_torcontainer::size_typefor sizes and indices. - Cache
n = v.size()to avoid repeated calls and reduce risk. - For reverse loops use
for (size_t i = n; i-- > 0;)orstd::ssize. - Use range-for if you do not need indices.
- Enable
-Wsign-compareto surface bugs early.
S - Summary
Key takeaways
size_tis the standard type for object size and index;sizeofreturns it.- It matches
vector::size()and avoids signed/unsigned mismatch. - Its range is larger than
inton 64-bit systems. - Unsigned subtraction can underflow; write conditions to avoid it.
- Reverse iteration has safe patterns; do not use
i >= 0with unsigned.
References and further reading
- C++ reference:
std::size_t: https://en.cppreference.com/w/cpp/types/size_t - C++ reference:
std::ssize: https://en.cppreference.com/w/cpp/iterator/ssize - ISO C standard:
size_t: https://en.cppreference.com/w/c/types/size_t
Conclusion
size_t is not a mysterious type. It is the standard way C/C++ expresses sizes and indices. If you avoid unsigned underflow and use safe loop conditions, it is more robust and more consistent than int. Consider enabling -Wsign-compare and cleaning up mixed-sign usage in your codebase.
Call to Action (CTA)
Search your codebase for places where size() is mixed with int, switch to size_t, and run tests. If you have hit a bug related to this, share the case and learnings.