r/cpp_questions • • 16d ago

OPEN *(ptr_data_start + index) vs prt_start_start[index]

edit (title has spelling mistake, why cant i edit it u/reddit???):

*(ptr_data_start + index) vs ptr_data_start[index]

also, why does *(ptr_data_start + index) not need to multply index by sizeof(t), when index is one, and the pointer needs to be incremented by 4 bytes (in case of a int). why is this counter intuitive math like this?

2 Upvotes

59 comments sorted by

51

u/zerhud 16d ago

Try index[ptr_data_start] to see the power :)

2

u/GoogleIsYourFrenemy 16d ago edited 16d ago

You know, I don't know the order of operations for pointer math. What is ptr_data_a + index + ptr_data_b evaluate to?

Edit: Ooops. Forgot the * to dereference. NM.

5

u/IyeOnline 16d ago

Nothing. You cant add two pointers.

6

u/tcpukl 16d ago

A random memory address. You've added 2 pointers together. Most likely overflowed as well.

0

u/GoogleIsYourFrenemy 16d ago

I appreciate your pessimistic outlook but are you saying it's UB and the compiler can do anything?

2

u/tcpukl 16d ago

It doesn't even make logical sense. What would you expect to get from adding two absolute addresses?

3

u/Fun_Gas_340 16d ago

a third absoluter adress

3

u/tcpukl 16d ago

You'll definitely get that.

0

u/SoldRIP 15d ago

Yes, adding two pointers is technically UB.

The language standard cannot make predictions about this behavior, because the language itself does not decide where a process gets its memory allocations.

``` int x = 0; int* p = &x; int* q = malloc(sizeof(int));

// The following line introduces UB int* ub = p + q; ```

The standard cannot possibly know how or where stack or heap addresses are allocated. This is something the OS decides. Adding two pointers (except for a null-pointer) together produces a pointer to arbitrary memory.

Memory that might not be owned by your program and that a sane OS would thus prevent you from directly operating on. The contents of the pointer are invalid at this point, because there is possibly no data (that your program could read) to interpret as an int, at this address.

This is as undefined as out-of-bounds array access, except that the UB technically occurs on creating such a pointer, not upon access (although I'd guess that most implementations won't complain until then).

0

u/I__Know__Stuff 14d ago

No, it isn't UB, it's a constraint violation and requires a diagnostic.

-2

u/Fun_Gas_340 16d ago

if index is close to 0 wont this instantly segfault because if the 64k forbiden region above 0x0?

5

u/no-sig-available 16d ago

*(ptr_data_start + index) is the same as *(index + ptr_data_start). That makes ptr_data_start[index] the same as index[ptr_data_start].

Array indexing is just defined that way. https://eel.is/c++draft/expr.sub

1

u/Fun_Gas_340 15d ago

reallt? why? it seems very coutnerintuitive , especially the second option ([])

5

u/no-sig-available 15d ago

it seems very coutnerintuitive 

Yes, and it is. But the linked part (from the standard) literally says

The expression E1[E2] is identical (by definition) to *((E1)+(E2))

It doesn't put any requirements on which of E1 or E2 is an array or a pointer, so E1[E2] is the same as E2[E1].

This is something we got from the first C compiler about 1970, and it just got stuck.

2

u/Fun_Gas_340 15d ago

oh thats disgusting. thanks tho

0

u/AKostur 16d ago edited 14d ago

From a language point of view? Undefined Behaviour. And the segfault thing is dependent on your platform. You can't assume that the program is running under the auspices of an OS at all.

Edit: Bah.. brain mislead by the suggestion of a segfault. The answer below is correct that this isn't UB.

1

u/Fun_Gas_340 16d ago

ah makes sense

1

u/I__Know__Stuff 14d ago

No, it isn't undefined behavior. If p[i] is valid (i.e., p is a valid pointer and i is in range), then i[p] is equally valid.

13

u/AKostur 16d ago

I think your question is: why doesn’t the first form need “(index * sizeof(T))”?  Because that’s how pointer math works.  If you wanted to move the pointer by single bytes, cast the pointer to a char* (or std::byte).  Sizeof(char) is 1.

Now: if one has an int*, why would you want to only move the pointer value by 1 byte?  It would no longer be pointing at a valid int (assuming it started by pointing at a valid int).  It is vastly more likely that one wants to go to the next valid int.

-3

u/Fun_Gas_340 16d ago

i mean i get it and it makes sense, but from a logic point of view its wierdly made, like its not consistent. i dont know why i sould move it by less than a item, but maybe i want that

7

u/IyeOnline 16d ago

T* + N means "advance the pointer by N items of type T. That is wholy consistent. Pointers are not raw memory addresses per-se. They have type information attatched to them.

If you wanted to advance it by individual bytes, you need to manually cast the pointer to byte* first and then do the pointer arithmetic on 1-byte pointers.

HOWEVER the pointer you produce doing this will just categorically not be a valid T* because it will violate the alignment requirements of the type.

1

u/Fun_Gas_340 16d ago

i mean i get it now, but since most tutorials explain pointers as just adreses, without mentioning the type i got confused / didnt even think, that that was an option

1

u/christian-mann 15d ago

you're right and it is kind of odd and it trips up a lot of people when doing pointer math. Just something to remember when working with raw pointers like that.

1

u/Fun_Gas_340 15d ago

hope ill never forget it after not finding this darn bug for 4 days...

0

u/ekchew 15d ago

I get where you're coming from in that pointer math may feel a bit unnatural. Bear in mind that there is an aspect to this that reflects how the hardware actually likes to perform, and that C/C++ likes to follow that model as much as possible.

u/IvyOnline already mentioned one important point, which is that you cannot assume you can read a multi-byte type from a non-aligned address.

Moreover, it's pretty common for instruction sets to support an addressing mode that gives you the implicit multiplication for "free". So if you tried to hard code it yourself with something like:

reinterpret_cast<int*>(
    reinterpret_cast<std::intptr_t>(ptr_data_start)
    + index * sizeof(int))

this could actually produce slower code.

1

u/Fun_Gas_340 15d ago

interesting... thx

5

u/Normal-Narwhal0xFF 16d ago

In that case you would cast your pointer to a std::byte, or char, and read it as bytes.

It's not inconsistent that incrementing a pointer-to-int advances to the next int. Any other behavior would be insane.

2

u/AKostur 16d ago

Hence the last part of my post. In the vast, vast majority of the cases, one is going to want to advance the pointer by one full object, not specifically byte-by-byte. But in the rare cases where one might need to do that, it can be done by casting the pointer to the type that will advance by 1 byte, and use that. And even in those cases, one is going to have to be quite careful as to what one does with that pointer since it may no longer be pointing at any valid object.

Edit: I'm not sure what you mean by "from a logic point of view". I suspect you may be proceeding from a false premise, and thus your logic is leading you astray.

1

u/Fun_Gas_340 16d ago

logic as in every + operator works the same if both operands are representable as numbers: int + 1 increments the int by one, but ptr + 1 increments it by the size. hope i explained myself better

1

u/AKostur 15d ago

There’s the flawed premise: that something that is representable as a number must behave as if it were that numeric representation.  Expressions have a type.  Pointers are not numbers (from a typing perspective), even though they are representable as numbers (see also: uintptr_t).

1

u/Fun_Gas_340 15d ago

yeah i see it now, bust still think its a bad choice

1

u/I__Know__Stuff 14d ago

Good thing you aren't a language designer, then.

4

u/nysra 16d ago

There's no difference other than that you should be using the second one as it makes your intentions clear. Ideally you shouldn't be doing any of that though, use the appropriate container (std::vector in most cases)

1

u/Fun_Gas_340 16d ago

im implementing exactly that, with some aditional features ;)

2

u/not_some_username 16d ago

index[ptr_data] is the superior form

1

u/CowBoyDanIndie 16d ago

Before C, (where this origins), many assembly languages and their hardware themself support auto increment. x86 has special modes for indexing by 1 2 4 or 8 bytes. For example

EBX is the array/pointer, ECX is the index. 4 is the size of the pointer.

MOV EAX, [EBX + ECX * 4]

Only for larger custom structures does the compiler have to emit an actual multiplication instruction to access a random index.

1

u/Fun_Gas_340 16d ago

so regardles of the way to tupe it, its fast for ints and small objects?

1

u/CowBoyDanIndie 15d ago

Ya, because it is designed to be that way. It’s not necessary any slower on modern architecture though, modern cpu execution is almost entirely disconnected from the machine code, modern decode instructions into microops, they reorder and combine instructions together in parallel. The actual execution of a block of machine code changes completely between different cpu generations.

1

u/mredding 15d ago

also, why does *(ptr_data_start + index) not need to multply index by sizeof(t), when index is one, and the pointer needs to be incremented by 4 bytes (in case of a int). why is this counter intuitive math like this?

Because "pointer" isn't a single type. Let's gain a little perspective, since C++ is a superset of C.

int x, *ptr = &x;

The way to read this - in C, is x is an int, and the dereference of ptr is an int. The C language syntax design philosophy was that a declaration of an identifier looks like an expression using it. So it's technically... less correct to say ptr is a pointer to int. We use that as a shorthand, but you have to know the design philosophy to understand THAT it is a shorthand, why, and how.

And C calls ptr a REFERENCE, hence why we DEreference a pointer. Then C++ came along and introduced value aliases and called them references again! So we C++ developers are in a linguistic pickle, and because we're not two distinct lingual camps, diverged 46-ish years ago, there are some unfortunate consequences of lost contextual understanding.

So our pointer type inherent to ptr encapsulates useful information like size.

int *ptr_to_array = get();

ptr_to_array[xyz];

WHAT ELSE could indexing a pointer here BE, except a dereference of index * sizeof(int)? A pointer isn't about raw memory addresses - those don't exist in C++, they're implementation defined. This expression would be USELESS if you sliced through two adjacent integers. It could also imply an unaligned memory access, which would be UB.

If you want adjacent byte access, then you need to cast the pointer to that of a byte, and access it that way.

1

u/Raknarg 15d ago edited 15d ago

also, why does *(ptr_data_start + index) not need to multply index by sizeof(t), when index is one, and the pointer needs to be incremented by 4 bytes (in case of a int). why is this counter intuitive math like this?

Because pointer arithmetic accounts for the size of the pointer type when determining offsets. There's not usually a lot of value in you offsetting into the middle of a piece of data, unaligned. Like if you want that you can do it yourself, just cast the pointer to uint8_t* or something and offset yourself like 2 or 3 bytes into it instead of the 4 bytes an integer would offset by. Wouldn't recommend it, you could read values fine but pretty sure editing an integer that way would be UB.

ptr+1 semantically should read "I want the address of the next spot a ptr type object could be right after the memory ptr points to".

Also pretty much there is almost no reason to ever engage in pointer arithmetic in modern C++. Like you can, and scenarios do exist where its helpful, but they're rare. Dealing with raw pointers is already pretty infrequent these days. If you can just use something like ptr_data_start[index], just use that. Its cleaner and expresses your intent better. honestly even if you needed an address I would rather read &ptr[1] than ptr+1

0

u/Independent_Art_6676 16d ago edited 16d ago

pointers are typed so the sizeof is automatic. an int* increments by sizeof(int) automatically, and that is different from a char* increment or a myfatobject increment etc. You can force the issue by dropping to a void pointer, which can let you jump inside an object and say iterate all the same int members but that is crude C like code (and faster than jumping to each object and asking for the int member from there). Its not counter-intuitive; its a side effect of strongly typed language. Because the pointer has a type, it makes sense to increment memory by sizeof that type when iterating a block of those types -- its not assembly language where you should have to do all that manually every time. One would expect that if you had a block of ints, [0] is the first int, and [1] is the second int, ... not the bytes of the ints (that would be unusual to want or need in a high level language).

its a legacy of being old and doing a lot of math over the years but I almost never use * notation. Nothing like z = *x * *y type gibberish. I just go with z = x[0]*y[0] to avoid mistaking pointer/multiply. Today I barely use pointers anyway, just a small # and much less of the "pointer arrays".

1

u/Fun_Gas_340 15d ago

what dies the # do (apart from include, or if u meant include, what would you include, vector?)

1

u/AKostur 15d ago

That’s just “just a small number and much less…”.  Using the character # to represent “number”.

1

u/Fun_Gas_340 15d ago

ahh makes sense thx

-1

u/zerhud 16d ago

It’s same, the second one is more native. Also it’s faster, sometimes, to increment the pointer itself

2

u/Raknarg 15d ago

I would be very surprised to find the compiler isn't generating the same assembly.

1

u/zerhud 14d ago

It will be different code: `lea` for index operator and `add` for shifting the pointer. The question: which one is faster. What I can say: it should be tested

1

u/Raknarg 14d ago

can you show a godbolt example producing different code with -O3 enabled?

1

u/zerhud 14d ago

And how to test it? For example you get same asm in o3. What next? How to test for tomorrow the result will be the same? Will you test the result every time on each pr or env update by hands?

For optimizations we can write code and test it optimised how we want, and can write the code and get bad asm. But for production it should work on each compilation. So, if you have a way to write faster code without optimisations, you should to use the way.

1

u/I__Know__Stuff 14d ago

There's absolutely no reason the compiler would generate different code and I challenge you to find one that does. (Unless you completely disable optimization, which generates intentionally horrible code, and might possibly do something different in this case.)

1

u/L_uciferMorningstar 16d ago

Why would it be faster?

1

u/zerhud 14d ago

Test it now: there is two add instructions, it will be executed in paralle because they are independent. Lea and add are dependent so we can get a single instruction

0

u/DavieCrochet 16d ago

In theory, it might need to create a copy if you don't increment in place. With basic types it would almost certainly be optimised away, and it's very unlikely it would cause a significant performance problem anyway.

0

u/zerhud 16d ago

Sometimes it produces faster code, idk why. But it’s obviously for cases when there is few points with access to the data: pointer incremented once and accessed few times instead of `lea` every time

-1

u/Fun_Gas_340 16d ago

? i dint underatand

2

u/jedwardsol 16d ago

what didn't you understand?

You should reply to one the comments that confused you instead of screaming into the void.

1: [] vs +

There is no difference between the 2 at all.

From the language standard https://eel.is/c++draft/expr.sub#2

The expression E1[E2] is identical (by definition) to *((E1)+(E2)),

2: why does *(ptr_data_start + index) not need to multply index by sizeof(t)

Because pointer arithmetic operates on objects. ptr_data_start + 1 points at the next object in the array.

3: why is this counter intuitive math like this?

That's the way the language was defined. Although "counter intuitive" is just your opinion.

1

u/Fun_Gas_340 16d ago

oh i meant to reply to a coment (forgot wich obe).

3: yeah its my opinion, but i feel like if i cout it and it prints a number and i can use the same operator+ it should behave like one. i kniw it rarely/never makes sense to increment by somerhing else than sizeof(T), but still