В чем разница между a+i и &a[i] для арифметики указателей в C++?

Предположим, у нас есть:

char* a;
int   i;

Многие вводные сведения о C++ (например, этот) предполагает, что rvalue a+i и &a[i] взаимозаменяемы. Я наивно верил в это несколько десятилетий, пока недавно не наткнулся на следующий текст (здесь), цитируемый с [dcl.ref]:

в частности, нулевая ссылка не может существовать в четко определенной программе, потому что единственный способ создать такую ​​ссылку — это привязать ее к «объекту», полученному путем разыменования нулевого указателя, что приводит к неопределенному поведению.

Другими словами, «привязка» ссылочного объекта к нулевому разыменованию приводит к неопределенному поведению. Основываясь на контексте приведенного выше текста, можно сделать вывод, что простая оценка &a[i] (в макросе offsetof ) считается "связывающей" ссылкой. Кроме того, существует мнение, что &a[i] вызывает неопределенное поведение в случае, когда a=null и i=0. Это поведение отличается от a+i (как минимум в C++ в случае a=null, i=0).

Это приводит как минимум к 2 вопросам о различиях между a+i и &a[i]:

Во-первых, какова базовая семантическая разница между a+i и &a[i], которая вызывает эту разницу в поведении. Можно ли это объяснить с точки зрения каких-либо общих принципов, а не просто «привязка ссылки к объекту разыменования null вызывает неопределенное поведение только потому, что это очень специфический случай, который всем известен»? Может ли &a[i] генерировать доступ к памяти для a[i]? Или автора спецификации не устраивали нулевые разыменования в тот день? Или что-то другое?

Во-вторых, помимо случая, когда a=null и i=0, есть ли другие случаи, когда a+i и &a[i] ведут себя по-разному? (может быть охвачен первым вопросом, в зависимости от ответа на него.)


person personal_cloud    schedule 01.03.2019    source источник
comment
согласно ответам здесь, a+i не определено, если a=null, хотя ваша 4-я ссылка говорит об этом является определенным, если i=0, хммм   -  person kmdreko    schedule 01.03.2019
comment
@кмдреко. Неплохо подмечено. Я изменил описание различий, чтобы сосредоточиться на случаях a=null, i=0, чтобы установить, что существует a разница между a+i и &a[i]... Опять же, заставляя задуматься, есть ли какие-либо другие различия между ними.   -  person personal_cloud    schedule 01.03.2019
comment
Целью стандарта никогда не было запретить &*a, когда a является нулевым указателем. Это является предметом ошибки 232.   -  person n. 1.8e9-where's-my-share m.    schedule 01.03.2019
comment
@н.м. Очень интересно, что предлагаемое разрешение указывает только то, что происходит в случае, когда a равно null или одному после последнего элемента массива. Это почти два самых полезных случая пустого lvalue! Но почему они остановились на этом, а не сделали a+i и &a[i] полностью эквивалентными...?   -  person personal_cloud    schedule 01.03.2019
comment
Мне интересно, возможно, нет никакой разницы между a+i и &a[i], а скорее просто некоторые разногласия и/или неверные толкования спецификации, применяемые по-разному к тому или иному синтаксису, без особого намерения применять только к тому или иному синтаксису. Таким образом создается впечатление, что между a+i и &a[i] есть разница, хотя на самом деле разницы нет? Если кто-то считает, что C++ имеет ограничения на арифметику указателей, то можно ли применить те же ограничения и к a+i, и к &a[i]? (но сами ограничения омрачены спорами).   -  person personal_cloud    schedule 01.03.2019
comment
@kmdreko В одном комментарии к этому вопросу, на который вы ссылались, говорится: [expr.add]/5 определяет поведение указателя + целого числа только для указателей на массивы (или указателя на объект, который действует как массив 1). Если указатель не указывает на массив, то поведение по определению не определено. А может быть, это в равной степени применимо и к a+i, и к &a[i]?   -  person personal_cloud    schedule 01.03.2019
comment
Дело в том, что текст, который я цитировал в исходном вопросе, конкретно говорит о нулевом разыменовании, которое существует в случае &a[i], но не a+i. Однако, возможно, это не относится к C++, как указано в статье Доббса.   -  person personal_cloud    schedule 01.03.2019
comment
Я до сих пор не вижу, где макрос offsetof оценивает &a[i].   -  person cpplearner    schedule 01.03.2019
comment
Как &a[i] считается обязательным для ссылки?   -  person Language Lawyer    schedule 01.03.2019
comment
Что еще более пугающе, a[i] и i[a] на самом деле взаимозаменяемы… потому что C. (Попробуйте.)   -  person Arne Vogel    schedule 01.03.2019


Ответы (2)


В стандарте С++, раздел [expr.sub]/1, вы можете прочитать:

Выражение E1[E2] идентично (по определению) выражению *((E1)+(E2)).

Это означает, что &a[i] точно такое же, как &*(a+i). Таким образом, вы должны сначала разыменовать * указатель, а затем получить адрес &. В случае, если указатель недействителен (т.е. nullptr, но также вне диапазона), это UB.

a+i основан на арифметике указателей. На первый взгляд это выглядит менее опасным, так как нет разыменования, которое точно было бы UB. Однако это также может быть UB (см. [expr.add]/4:

Когда выражение, имеющее целочисленный тип, добавляется к указателю или вычитается из него, результат имеет тип операнда указателя. Если выражение P указывает на элемент x[i] объекта массива x с n элементами, выражения P + J и J + P (где J имеет значение j) указывают на (возможно, гипотетический) элемент x[i + j], если 0 ≤ i + j ≤ n; в противном случае поведение не определено. Точно так же выражение P - J указывает на (возможно, гипотетический) элемент x[i - j], если 0 ≤ i - j ≤ n; в противном случае поведение не определено.

Итак, хотя семантика этих двух выражений немного отличается, я бы сказал, что в конце концов результат одинаков.

person Christophe    schedule 01.03.2019
comment
Но см. [expr.add]/7 в C+ +17 DIS или [expr.add]/(4.1) из действующий проект стандарта. - person cpplearner; 01.03.2019
comment
Спасибо, что разбили это на &*(a+i); это очень полезно. Просто для уточнения: я думаю, вы говорите, что если мы верим в [expr.add]/4, то &* не вводит никаких случаев UB, которые еще не были созданы (a+i)? (и если мы не верим [expr.add]/4, то &* может предположительно создать случаи UB, которых не было в (a+i))? Думаю, я могу принять это как полный ответ. Спасибо. - person personal_cloud; 01.03.2019
comment
@personal_cloud Да! Арифметика указателя и индексация действительно определены очень последовательно, так что они приводят к одному и тому же результату (часть исключения, которое вы уже упомянули в своем вопросе). - person Christophe; 01.03.2019

TL; DR: a+i и &a[i] оба правильно сформированы и создают нулевой указатель, когда a является нулевым указателем, а i равен 0, в соответствии (намерение) стандарта, и все компиляторы согласны.


a+i, очевидно, правильно сформирован согласно [expr.add]/4 из последний проект стандарта:

Когда выражение J, имеющее целочисленный тип, добавляется или вычитается из выражения P типа указателя, результат имеет тип P.

  • Если P оценивается как значение нулевого указателя, а J оценивается как 0, результатом является значение нулевого указателя.
  • [...]

&a[i] сложно. Согласно [expr.sub]/1, a[i] эквивалентно *(a+i), таким образом, &a[i] эквивалентно &*(a+i). Теперь в стандарте не совсем ясно, правильно ли сформировано &*(a+i), когда a+i является нулевым указателем. Но как @n.m. указывает в комментарий, намерение, записанное в cwg 232 должен разрешить этот случай.


Поскольку базовый язык UB должен быть пойман в константном выражении ([expr.const] /(4.6)), мы можем проверить, считают ли компиляторы эти два выражения UB.

Вот демо, если компиляторы думают, что константное выражение в static_assert является UB, или если они думают, что результат не true, то они должны выдать диагностику (ошибку или предупреждение) в соответствии со стандартом:

(обратите внимание, что здесь используются однопараметрические static_assert и constexpr lambda, которые являются функциями C++17, и лямбда-аргумент по умолчанию, который также довольно новый)

static_assert(nullptr == [](char* a=nullptr, int i=0) {
    return a+i;
}());

static_assert(nullptr == [](char* a=nullptr, int i=0) {
    return &a[i];
}());

Судя по https://godbolt.org/z/hhsV4I, в этом случае все компиляторы ведут себя одинаково. , вообще не производя диагностики (что меня немного удивляет).


Однако это отличается от случая offset. Реализация, опубликованная в этого вопроса, явно создает ссылка (что необходимо для обхода определяемого пользователем operator&) и, таким образом, подлежит требованиям к ссылкам.

person cpplearner    schedule 01.03.2019
comment
Поскольку базовый язык UB должен быть перехвачен в постоянном выражении, [expr.const] говорит, что ... будет иметь неопределенное поведение, как указано в ..., что я всегда понимал так как UB должен быть явно указан. И нет формулировки, явно говорящей, что *p является UB, когда p == nullptr. - person Language Lawyer; 01.03.2019
comment
@LanguageLawyer Ну, если вы имеете в виду, что это правильно сформировано, то я согласен. Если вы имеете в виду, что есть что-то, что явно не указано как UB, но все же является UB, то, я думаю, вам нужно доказать существование такой вещи. - person cpplearner; 01.03.2019
comment
Доказать существование UB, который явно не помечен стандартом как UB? Из определения UB следует: поведение, для которого этот международный стандарт не предъявляет никаких требований. Если стандарт к чему-то не предъявляет требований, это UB, даже если в стандарте об этом прямо не сказано. В примечании после определения говорится следующее: Неопределенное поведение можно ожидать, если в настоящем стандарте отсутствует какое-либо явное определение поведения. - person Language Lawyer; 01.03.2019
comment
@Language Lawyer Но expr.unary.op/1 кажется определите базовое требование: если у вас есть указатель p, то *p обозначает его расположение в памяти. Не сказано, что это должно быть действительное местоположение. Итак, по вашей логике, *p не UB... Я думаю, что логика @cpplearner здесь верна; это другие разделы спецификации, такие как [dcl.ref], которые специально создают здесь возможность UB. - person personal_cloud; 01.03.2019
comment
@personal_cloud Я ничего не нашел о расположении памяти в определении оператора косвенности. В нем говорится, что результирующее lvalue ссылается на объект или функцию, на которую указывает выражение указателя. А поскольку p == nullptr не указывает ни на какой объект или функцию, и этот случай явно не обрабатывается стандартом, такое поведение считается неопределенным. AFAIU, поскольку этот UB является неявным, компиляторам не требуется диагностировать его в константных выражениях. Но тут я не уверен на 100%. - person Language Lawyer; 01.03.2019