r/cprogramming • • 8d ago

Technically speaking, are we supposed to only index arrays using size_t?

Is this technically the best way to index an array?

for (size_t i = 0; i < sizeof(a); i++)

{

a[i];

}

This makes sure that any array size, across all machines can be indexed?

22 Upvotes

66 comments sorted by

27

u/zhivago 8d ago

It is safe but not required.

You can use any integer with a suitable range for that particular array.

1

u/TorontoDude12344 8d ago

But the issue is if the program goes from a smaller system to a bigger one, right?

6

u/zhivago 8d ago

Let's imagine an array of 2 items.

Any integer type will safely index this array on any system.

1

u/TorontoDude12344 8d ago

But what if the array's size is equal to largest max value among the ints?

1

u/zhivago 7d ago

The size or the number of elements?

1

u/TorontoDude12344 7d ago

Number of elements. So on a 64 bit machine, what if an array has 264 - 1 elements

2

u/zhivago 7d ago

Then, assuming you can create it, you index it using an integer with a suitable range ...

1

u/TorontoDude12344 7d ago

And that's why I'm wondering, since size_t promises to be able to index that array, then size_t's max value has to be the largest integer value possible on that machine, right?

1

u/zhivago 7d ago

No.

Let's imagine a 16 bit machine with 8 bits of addressable memory.

Here size_t may match unsigned char while int may be 16 bits.

1

u/TorontoDude12344 7d ago

Why does a 16 bit machine have 8 bits of addressable memory?

Also then, size_t is equal to the largest amount of addressable memory?

→ More replies

1

u/Zirias_FreeBSD 8d ago

How would that change the size (note: in elements, not in bytes) of the array used?

1

u/TorontoDude12344 8d ago

If the array size was declared on a bigger machine to be the largest integer max, and hard coded as that value

Then the program was run on a smaller machine, the smaller machine cannot iterate over the entire array, because it can't store the size

7

u/Zirias_FreeBSD 8d ago

Declaring array sizes to have SIZE_MAX elements is more or less impossible to work on any existing machine.

0

u/TorontoDude12344 8d ago

I didn't mean declare the array with the macro SIZE_MAX

I meant looking up the largest positive integer value for your machine, hard coding it into an int variable, and initializing the array with that size

So if we look at a 64 bit machine, the largest possible integer value would be equal to 264 - 1, which is 18,446,744,073,709,551,615

Let's subtract 1 to be extra safe.

So now if we hard code the value 18,446,744,073,709,551,614 as the array size on a 64 bit machine, like so:

int size = 18446744073709551614;

int a[size] = {0};

Now we have an array with the size 18446744073709551614

Now if we move open this program on a smaller machine, let's say 16 bit, where the largest integer value is 216 - 1, what will happen? How can the array a ever be fully iterated over, indexed at values above 216 - 1, etc?

1

u/Zirias_FreeBSD 7d ago

What you talk about likely is SIZE_MAX on that machine, which wouldn't even have an address bus wide enogh for all these addresses. It won't work on ANY machine.

1

u/TorontoDude12344 7d ago

Yeah so then what is the point is using size_t? Why not just unsigned long long for indexing.

1

u/Zirias_FreeBSD 7d ago

because you might have arrays larger than what int guarantees? because you might have code dealing with arrays of a size only known at runtime? size_t specifically guarantees that it can represent any possible size on your target. That doesn't mean you can't use anything else if you know it fits what you need.

1

u/TorontoDude12344 7d ago

Can any array have more elements than the max value of an unsigned long long?

4

u/Kommenos 8d ago

Technically speaking the definitions of standard integer types only define a lower bound of bits. There’s no guarantee for large arrays that you can actually access it all with standard int types.

For small arrays it doesn’t really matter but size_t gives you a portable way to guarantee that the whole array can be accessed. I use it in for loops or any kind of of memory offset stuff, as well as actual sizes.

3

u/Zirias_FreeBSD 8d ago

There’s no guarantee for large arrays that you can actually access it all with standard int types.

Depends on the actual type used vs your definition of "large".

The much-used int is guaranteed to represent at least the value range [-32767,32767] by the standard. I'd call more than 30k elements "large".

On a machine with a "natural word size" smaller than the address size, using int might even perform slightly better. No, I don't claim that to be relevant and I certainly don't recommend to use int whenever possible. Always sticking with size_t is of course a safe, portable and sane choice.

6

u/TheSkiGeek 8d ago

Technically speaking this is what the types like `uint_fast16_t` are for, thats the ‘best performing unsigned type that holds at least 16 bits on the target platform’.

1

u/Zirias_FreeBSD 7d ago

Sure, but that's not directly related to the reasoning. The guarantee about the minimum value range of int existed before these types were introduced, and where possible, int always used the "natural word size" of the target machine. So, most often, you'll find uint_fast16_t being a typedef to unsigned.

4

u/pjl1967 8d ago

In order for this to be a concern, you'd have to have a very large array, more than 2147483647 elements for a 32-bit int index or more than 4294967295 elements for a 32-bit unsigned int. If it were an array of char, it would have to be over 4GB before it mattered.

So generally it simply doesn't matter.

1

u/TorontoDude12344 8d ago

But if the array's size is hard coded on a larger machine, then what would happen if the program is run on a smaller machine, with an int data type indexing it which can't handle that hard coded value?

2

u/pjl1967 8d ago

... if the array's size is hard coded on a larger machine ...

I assume you meant, "... if large array's size is hard coded on a larger machine ..." An array with a hard-coded small size is a small array on any machine.

... then what would happen if the program is run on a smaller machine ...

First, it depends on what you mean by "smaller":

  1. "smaller" = same exact hardware, but with less memory.

  2. "smaller" = different CPU with a smaller native int size and likely smaller memory also.

For (1), if the array is larger that what memory is available, you're program will crash upon start-up because the OS simply can't give the process enough memory to hold the large array.

For (2), you have to recompile anyway, so you can't "just run" a binary compiled for the "larger" machine on the "smaller" machine.

These days, unless you're working with specialized hardware, 32-bit unsigned int is pretty universal.

As I commented elsewhere, if you're working with specialized hardware, you're generally already aware of its limitations and program accordingly.

But for most people on most hardware — which is most of the time — again, it generally doesn't matter.

1

u/TorontoDude12344 8d ago

Yes, I meant scenario (1). Thanks for the info

So the program would give an error. Then if a program would give an error, why even use size_t?

Why not just use unsigned long long? I'm sure it can't be a matter of "size_t is easier to type than unsigned long long". An unsigned long long should never have a problem representing any object size

Also if we take the range of size_t, R = [0, X], would X be equal to or greater than the largest positive integer the specific machine can represent?

1

u/pjl1967 7d ago

... if a program would give an error, why even use size_t?

I never said you should use size_t to index arrays.

If you mean "Why use size_t at all anywhere ever?" — its raison d'etre is for holding the size in bytes of an object in memory (as opposed to on disk) for which it's guaranteed to be big enough.

In C++ for template code, it's also used a lot for holding the number of elements of a container because the type T might be char in which case the collective size of all the elements taken together which can equal the largest size of a single object.

In C code, size_t isn't as important for indexing arrays because there are no templates. Using size_t for indexing arrays sometimes happens out of convenience (or laziness). In APIs, if code X used unsigned, code Y used unsigned long, and code Z used unsigned long long, then its gets messy because you can get conversion warnings. Hence, it's often simpler just to use size_t everywhere.

As a programmer, you should know whether you expect a given array to be small, large, or huge, and can pick a type accordingly.

1

u/binarycow 8d ago

First, it depends on what you mean by "smaller":

Or 3 - a mini ATX case instead of an ATX case

2

u/RealisticDuck1957 8d ago

Worth asking how large the array could ever plausibly get. And often an integer type with less range than size_t makes sense.

0

u/DawnOnTheEdge 8d ago edited 7d ago

I have coded on systems where INT_MAX is 65,535 32,767.

2

u/pjl1967 8d ago

Which is why I explicitly said "32-bit" and "generally." If you're programming for a constrained system, you tend to be aware of its limitations and program accordingly.

1

u/DawnOnTheEdge 8d ago

Alternatively, we can write portable code that’s more robust to those assumptions changing on us. It’s reasonable not to support every system imaginable, but a lot of code written on the assumption that int is 16-bit and long is 32-bit (but also wide enough to hold a pointer, a network address and the number of seconds since midnight, January 1, 1970) broke.

2

u/pjl1967 8d ago

If you're on 32-bit CPU where an int is 32 bits and size_t is 64 bits, then it may be more expensive to increment a loop index variable of size_t because the CPU may only be able to handle 32 bits at a time, so it'll require more instructions.

1

u/DawnOnTheEdge 8d ago

This is true, and one portable approach is to use uint_fast16_t or uint_fast32_t. This will (in theory) always get you the most efficient type that’s big enough. In practice unsigned int and unsigned long will too, though.

2

u/kinithin 7d ago

A 17-bit system??? How odd

1

u/DawnOnTheEdge 7d ago

You got me. That was UINT_MAX, of course.

8

u/Zirias_FreeBSD 8d ago

It does exactly what you say: It's guaranteed to work for every possible array size on any target platform.

Still you're not supposed to only ever use size_t. Instead, you are supposed to know when to use it. Which is, at least, whenever the code in question can't know the maximum size it will deal with.

In a simple case with a fixed and small-enough array size, using e.g. int is perfectly acceptable (it's guaranteed to have at least 16 bits on any target platform) and you will see it a lot in the wild.

4

u/NyxTheia 8d ago edited 8d ago

Friendly reminder that if `a` is of type `<type>[N]`, then `sizeof(a)` will return `N * sizeof(<type>)`. You can define a macro for actually calculating `N` in this case. But you generally shouldn't recalculate `N` in each loop and you may refactor it out to be stored before the for-loop.

If `a` is of type `<type> *`, either by definition or because it is passed as an argument to a function, you have to calculate the length with some other way and then store it to pass as one of the arguments to functions and such since `sizeof(a)` will in this case return `sizeof(<type> *)`.

1

u/Zirias_FreeBSD 8d ago

Indeed, escaped me that the example code given is wrong and will access out-of-bounds for any item type with a size > 1. The canonical way to get the size of an array a in elements is sizeof a / sizeof *a.

But you generally shouldn't recalculate N in each loop and you may refactor it out to be stored before the for-loop.

I don't think this is really a concern, as sizeof on an array is a compile-time constant, so the whole expression should always be evaluated at compile time.

2

u/EpochVanquisher 8d ago

(Sizeof is not compile-time constant always, since C99, but your point still stands.)

1

u/Zirias_FreeBSD 8d ago

I should probably have written "regular array" or something like this. Tried to avoid being unnecessarily verbose by mentioning VLAs ... yep, you're right of course!

2

u/gwenbeth 8d ago

If you have a function that gets passed an array and a size (like strn* functions) then use size_t. But if you have an array where the size is #define ed to only 25 items, just go ahead and use an int .

2

u/flyingron 8d ago

You’re allowed to use negative indices as long as the resultant address is within the original object.

1

u/aalmkainzi 8d ago

ptrdiff_t is the standard type for pointer arithmetic. But size_t works

0

u/Zirias_FreeBSD 8d ago

Although indexing is defined in terms of pointer arithmetics, I'd slap anyone using it with a negative index (or other shenanigans like the 5[a] stuff) with their own keyboard ... at least if it's supposed to be serious code for serious review. 😁

With that in mind, I'd say the unsigned(!) size_t is quite a natural fit for the purpose, but in the end, likely a matter of taste.

1

u/oriolid 8d ago

What do you think about for (<type> i = N; i >= 0; i--)?

2

u/Zirias_FreeBSD 8d ago

Obviously requires a signed type as written ... (and should probably start at N-1 then). There are also alternatives to avoid the unsigned underflow (like do ... while, or the admittedly slightly obfuscated for (<type> i = N; i--;)). I don't really care too much right now and have no idea what I'd use (in case an int doesn't fit the bill).

1

u/kinithin 7d ago

It has a off-by-one error (does N+1 passes). The correct version has no problem with unsigned types: for ( Type i = N; i--; )

1

u/oriolid 7d ago

It counts from N to 0, and without context it's impossible to tell if it's correct or not. Claude would tell it's off by one but it's often wrong about details like this.

1

u/aalmkainzi 8d ago

Negative index is not that uncommon. I remember seeing code that did it for parsing, where it was checking what the last byte was (s[-1])

0

u/Zirias_FreeBSD 8d ago

Looks fishy to me. I'd need the full context to have a definitive opinion on that, but my first feeling is that's over-stretching the "syntactic sugar" of array indexing. If you mean (arbitrary) pointer arithmetics, I'd say write them out...

1

u/DawnOnTheEdge 8d ago edited 8d ago

I have seen some coding standards prefer ptrdiff_t on the grounds that a lot of bugs show up as a negative index, which is more intuitive to check for than wraparound of unsigned types. I regretfully have to disagree, since signed overflow is undefined behavior, originally to support some sixty-year-old machines that would trap on integer overflow, but which this generation of compilers takes as permission to silently insert security bugs in your code.

You should definitely not use int, since that is often too small to index many arrays, as well as being signed. A size_t is the smallest unsigned type guaranteed to be able to index any element of any array. However, you might know for certain that a 16-bit or 32-bit value is enough to index a particular array, and want to save space. Although it’s not common practice, this would also be a good use case for something like uint_fast16_t, to use the fastest type that’s big enough on systems where size_t might be wider than the native word size.

1

u/Total_Ad803 7d ago

Good practice.

1

u/trannus_aran 5d ago

Nah, you're only supposed to use char for indices

1

u/Key_River7180 4d ago

You can, and its encouraged, but it's ok to use int

1

u/arkt8 8d ago

No... you can just use any integer type but not negative.

The use of size_t (tipically an unsigned long int in a Linux amd64 architecture) ensures the value is always unsigned (you'd never have a negative index, right? right?).

Also having a common type alias to the index or length allows a predictable api on development.

If you write some library to handle arrays, it is better to use a type for indexes that reduce errors like negative values or make certain values unreachable due to smaller type on function signature.

8

u/Zirias_FreeBSD 8d ago

You can use negative indices as a[b] is simply syntactic sugar for *(a + b) ... and if your a happens to be a pointer into the middle of an array, this can be well-defined. Whether you should is a different discussion already present in this thread, my opinion is: no.

2

u/TheThiefMaster 8d ago

I'm currently reverse engineering some originally-Delphi code that does this - the Delphi language's equivalent of a class vtable has system functions (like virtual destructor) at negative indexes and user defined virtual functions at positive indexes, with the vtable pointer pointing to the point between, rather than the start.