6

Известно, что если не иметь оператора sizeof то кол-во элементов массива возможно посчитать, например, так:

int arr[10];
size_t size = *(&arr + 1) - arr;

где

arr есть указатель на первый элемент

&arr есть указатель на весь массив

&arr + 1 есть указатель на следующий кусок памяти после нашего массива

*(&arr + 1) есть адрес элемента который идет после последнего элемента массива.

и соответсвенно разность указателей даёт кол-во элементов между ними.

Вопросы:

  1. Не приведёт ли это *(&arr + 1) к UB ?

  2. Каким образом &arr есть указатель на весь массив ? Это определено стандартом ?

ampawd
  • 3,701
  • "Не приведет ли к UB" Вот чтобы не думать об этом, лучше так не писать. :) Есть же std::extent_v для этих целей. – HolyBlackCat Dec 23 '18 at 21:08
  • 1
    @HolyBlackCat ну конечно же так писать не стоит, вопрос больше теоретический – ampawd Dec 23 '18 at 21:10
  • 1
    Про (1) не знаю, а вот про (2) могу сказать. & - одна из немногих вещей (как и sizeof), которая не превращает массив в указатель на его первый элемент. Так что он с массивами работает так же, как и с остальными типами. "Это определено стандартом?" Конечно. – HolyBlackCat Dec 23 '18 at 21:14
  • 3
    arr это массив, а не указатель на первый элемент (хотя массив может неявно преобразовываться в указатель на первый элемент) – user7860670 Dec 23 '18 at 21:14
  • @VTT тогда почему *arr дает значение первого элемента ? – ampawd Dec 23 '18 at 21:17
  • 1
    *arr эквивалентно *static_cast<int *>(arr) Если бы arr был указателем на первый элемент, то &arr дал бы вам указатель на указатель на первый элемент, а не указатель на массив. И да, разыменовывание *(&arr + 1) будет неопределенным поведением, так как указатель &arr + 1 не указывает на валидный объект. – user7860670 Dec 23 '18 at 21:17
  • Потому что почти при любом использовании массив неявно преобразуется в указатель на свой первый элемент. Это называется (array-to-pointer) decay. Этого не происходит при применении к массиву & и sizeof и еще в кое-каких редких случаях. – HolyBlackCat Dec 23 '18 at 21:18
  • @AR Hovsepyan а вы попробывали скомпилировать ? – ampawd Dec 23 '18 at 21:56
  • @VTT, std::end(arr) тоже не указывает на валидный обьект, однако используется для проверки достижения конца. И тут тоже &arr + 1 не разыменовывается, поэтому без разницы какой там обьект или мусор – AR Hovsepyan Dec 23 '18 at 22:17
  • Sorry, я вот даже не могу представить тот воспалённый разум, который привёл бы к такой архитектуре... – PinkTux Dec 23 '18 at 22:24
  • 3
    @AR Hovsepyan: Речь идет именно о применении (явном или неявном) оператора * к невалидному указателю. В std::end(arr) нет такого применения. В рассматриваемом выражении - есть. Об этом и речь. – AnT stands with Russia Dec 23 '18 at 22:47

2 Answers2

8

*(&arr + 1) может быть записано как (&arr)[1] или как 1[&arr]. Именно в таком "более интересном" виде этот вопрос периодически всплывает в обсуждениях.

В С++ формального ответа на этот вопрос не существует. Тема когда-то активно обсуждалась, но так и застряла в состоянии "drafting":

http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#232

Никаких поползновений дать окончательный ответ на этот вопрос пока не видно. То есть до сих пор в языке С++ нет ответа на давний вопрос о том, можно ли делать так

int arr[10];

for (int *p = &arr[0]; p != &arr[10]; ++p) // `&arr[10]` - UB или нет?
  ...;

for (int *p = arr; p != 1[&arr]; ++p) // `1[&arr]` - UB или нет?
  ...;

Вопрос о формальной легальности по крайней мере первого варианта известен еще со времен Царя Гороха, но вменяемого ответа на него до сих пор не предоставили.

В языке С предприняли попытки разрешить часть таких ситуаций, объявив соседние операторы & и * "аннигилирующими" друг друга еще до начала вычисления выражения. Это легализовало вариант &arr[10]

&arr[10]  <=>  &*(arr + 10)   <=>  arr + 10 - нет UB

Но это формально не легализовало вариант 1[&arr] (ваш вариант). В этом варианте мы имеем

 (int *) *(&arr + 1)
   ^     ^^^^^^^^^^^ 
   |     выражение, результат которого имеет тип `int [10]`
   |
   стандартное неявное преобразование массива к указателю 

Точно так же, как язык С объявил соседние & и * "аннигилирующими" друг друга, надо было бы соседнее "неявное преобразование массива к указателю" и оператор * объявить "коллапсирующим" до просто преобразования указателя, т.е. считать это выражение эквивалентным

 (int *) (&arr + 1)

Однако этого пока сделано не было. То есть в языке С ваш вариант формально порождает неопределенное поведение.

В языке С++ все пока (и уже давно) подвешено в воздухе.

  • Значит за массивом может быть не достаточно доступной памяти для такого же массива, чтобы взять его адрес, или как?... – AR Hovsepyan Dec 23 '18 at 22:54
0

По стандарту оператор & берет адрес операнда и фактически превращает в массив из одного элемента.

For purposes of pointer arithmetic ([expr.add]) and comparison ([expr.rel], [expr.eq]), an object that is not an array element whose address is taken in this way is considered to belong to an array with one element of type T.

Далее простая адресная арифметика. В данном случае элементом массива является массив из десяти элементов целого числа. Поэтому инкрементирование указателя приведет к переходу на следующий элемент массива. После чего остается только взять указатель на первый элемент следующего элемента (извините за тавтологию) и получить их разницу. Разименование *(&arr + 1) даст указатель на первый элемент следующего массива int arr[10];, а по сути указатель останется тот же, только с типом int*.

Указатель и то, на что он указывает это разные вещи. Место в памяти не имеет никакого значения, указатель можно инкрементировать сколько угодно. В данном случае работа идет с обычным числом, которое является указателем:

int arr[10];
auto sp = &arr;
sp++;
sp++;
sp++;
sp++;
//size_t size = *(&arr + 1) - arr;
size_t size = (int*)sp - arr; // По сути тоже самое

Судя по стандарту все нормально. Ни к какому UB это не приведет. И да, в стандарте это все определено.

Чтобы лучше продемонстрировать можно взять более простой тип:

int a;
int *pa = &a;
int *pb = pa + 1;
size_t s = pb - pa;

if P and Q point to, respectively, elements x[i] and x[j] of the same array object x, the expression P - Q has the value i − j.

В данном случае получим размерность массива, которая будет равна единице. Если нужно получить размерность типа тогда можно сделать следующее:

size_t s = (char*)pb - (char*)pa;

Но лучше не изобретать велосипед и использовать sizeof.

Andrey Sv
  • 1,031