Why &x[0]+x.size() instead of &x[x.size()]?
Asked Answered
M

1

16

I'm reading A Tour of C++ (2nd edition) and I came across this code (6.2 Parameterized Types):

template<typename T>
T* end(Vector<T>& x)
{
     return x.size() ? &x[0]+x.size() : nullptr;     // pointer to one-past-last element 
}

I don't understand why we use &x[0]+x.size() instead of &x[x.size()]. Does it mean that we take the address of the first element in x and just add to that number x.size() bytes?

Mate answered 19/5, 2022 at 10:17 Comment(1)
Nope, we don't add 'x.size() bytes' but rather x.size() times the size of each item. And the size of an item follows from the type of a pointer obtained by &x[0]. Seek the notion of 'pointer arithmetic' in C/C++.Maledict
E
17

&x[x.size()] would result in (attempting to) take the address of x[x.size()]. However x[x.size()] attempts to access an out of bound element; depending on the API of Vector<T>::operator[] for the particular T, a number of bad things could happen:

|    Vector<T>::operator[] semantics      |
| ======================================= |
| return\ contr |             |           |
| type   \ -act |  unchecked  |  checked  |
| --------------------------------------- |
| reference     |    UB (1)   |    FH     |
| value         |    UB (2)   |    FH     |
| --------------------------------------- |

with

  • UB (1): undefined behavior when creating a reference to an out-of-range-element.
  • UB (2): undefined behaviour when attempting to read the value of an out-of-range element.
  • FH: some fault handling action from the API if it is checked (e.g. throwing an exception, terminating, ...).

For std::vector, as an example, you would run into UB (1) as its operator[] is unchecked and returns a reference type.

Whilst you may perform pointer arithmetics to compute a pointer to one-past-last (semantically end()) of a buffer, you may not dereference a one-past-last pointer.

Erme answered 19/5, 2022 at 10:20 Comment(10)
That, or the Vector<T>::operator[] might actually check its input and throw or do something else.Pencil
@Pencil Good point, added into the answer.Erme
@Pencil OK, got it. And what about &x[x.size() - 1]?Mate
@Mate &x[x.size() - 1] is safe if size() != 0, but if would not give you a pointer to one-past-last (i.e. end()) but to last (i.e. --end()).Erme
Taking the address of x[x.size()] does not access the element. It's that creating a reference to the element is already undefined behavior inside std::vector::operator[], which does not bounds check as per standard.Deanery
@GoswinvonBrederlow Expanded with (ref, value) x (checked, unchecked) fault table.Erme
What's FH? And I think (1) is not actually UB, since pointers (and refs?) to one-past-the-end array elements are allowed.Misbeliever
@Misbeliever Pointer yes, Reference I believe not. A reference must "point" to a valid object or you have UB. That's why a == nullptr on a reference is always false per definition. Given a pointer T *t = &x[size]; how you would return that: return *t;. It even looks like an access.Deanery
@GoswinvonBrederlow Apparently it's an open issue, it's unclear if it's UB or not.Misbeliever
I can agree too that. I still believe it's UB because it would open the door to a whole lot of problems if it is not. A reference is supposed to be safer than a pointer and that would really make reference and pointer identical in their behavior. Anyway, the code in question simply side steps the problem by converting to pointer first and then going one past the end.Deanery

© 2022 - 2024 — McMap. All rights reserved.