Гарантируется ли завершение следующей программы на языке C с помощью 0
или компилятору разрешено идентифицировать объекты s
и t
друг с другом, как это разрешено в C++, как так называемая форма оптимизации возвращаемого значения (NRVO) при копировании?
typedef struct {
int i, j;
double a, b;
} S;
int result;
S test(S *q) {
S s = {0, 0, 0, 0};
result = &s == q;
return s;
}
int main(void)
{
S t = test(&t);
return result;
}
Кланг выходит с 1
вопреки моим ожиданиям, см. https://godbolt.org/z/ME8sPGn3n.
(В C++ оба 0
и 1
являются допустимыми результатами из-за явного разрешения на выполнение NRVO.)
@0___________ Не могли бы вы указать мне, какая часть UB? Предполагая, что я не упустил из виду что-то очевидное по глупости, я, по крайней мере, почти уверен, что в C++ нет UB, и я не вижу, чем он будет отличаться в C.
В С++ тоже. godbolt.org/z/nEos6ojch
Он также возвращает 1 в C++.
@0___________ Да, в C++ разрешены оба кода выхода 0
и 1
, и неизвестно, какой из них будет выбран. Хотя это не УБ. Вопрос в том, отличается ли это в C.
@BradLanam См. комментарий выше.
Учитывая, что s
является локальным для test()
, а q = &s;
, доступ к значению q
после возврата test()
осуществляется через UB.
@AndrewHenle Хорошо, это определяется реализацией в C++. Но я снова удалил этот вариант. Наверное, это просто отвлекло от моей мысли.
@user17732522 user17732522 Мне не известен какой-либо эквивалент NRVO на языке C, поэтому я бы сказал, что возвращаемое значение C должно быть равно 0, поскольку s
является локальным для test()
, а t
является локальным для main()
. Это два разных объекта, адреса каждого из которых взяты, поэтому у них должны быть разные адреса. Кажется, вы нашли ошибку компилятора.
@user17732522 user17732522 Я подозреваю, что если бы вы не взяли адреса объектов, то по правилу «как если бы» [N]RVO было бы вполне приемлемо в C, поскольку не было бы ни одного из возможных побочных эффектов. в C++ при копировании объектов. Конечно, без получения адреса(ов) вы никогда не сможете наблюдать такое поведение изнутри своего кода.
@AndrewHenle Конечно. Независимо от того, оптимизирует ли компилятор копию, само по себе обычно невозможно наблюдать, и тогда это можно сделать как на C, так и на C++ в соответствии с общими правилами «как если бы», применимыми ко всем оптимизациям. Правило NRVO в C++ существует потому, что оно явно разрешает такую оптимизацию, даже если она имеет наблюдаемые побочные эффекты. Гораздо проще генерировать наблюдаемые побочные эффекты в C++, но, как показывает этот пример, это также возможно и в C при получении адресов.
@user17732522 user17732522 Я добавил это: NRVO используется в режиме C
@TedLyngmo Вы использовали мой вариант, который я тем временем удалил, который, как мне было указано в предыдущем комментарии, имеет UB на C. Вместо этого вам следовало опубликовать исходный вариант, который все еще находится в вопросе.
@TedLyngmo В C используется указатель, значение которого указывает на объект за пределами его жизни, равно UB, как это происходит с ptr
при сравнении в main
. (В C++ такое использование значения указателя имеет поведение, определяемое реализацией, при этом обычно значение указателя по-прежнему ведет себя так, как если бы оно представляло исходный адрес объекта для цели ==
.)
@user17732522 user17732522 Я обновил билет, указав ваш текущий код.
Are there two or three S objects?
Два. Объект должен быть «определен». Возвращаемое значение не является объектом. Это ценность.
@KamilCuk Я заметил, что это в любом случае не имеет значения, потому что время жизни s
в C существует уже тогда, когда его блок введен, поэтому в любом случае он должен будет иметь уникальный адрес во время всей соответствующей обработки. Но мне интересно, действительно ли временных объектов нет. Черновик N3220 по крайней мере описывает объекты с временным временем жизни для выражений без lvalue типа структуры/объединения с элементами массива в §6.2.4. Я полагаю, что это необходимо, потому что на элемент массива можно сформировать указатель.
@KamilCuk Кроме того, что интересно, в §6.2.4 также говорится об объектах с временным временем жизни: «Такой объект не может иметь уникальный адрес». Фактически это означает, что в C разрешен эквивалент исключения копирования в форме оптимизации возвращаемого значения (RVO вместо NRVO). Адрес временного объекта будет единственным наблюдаемым отличием при применении этой оптимизации в C.
Как правило, если есть оптимизация, которая явно разрешена в C++ и явно не разрешена или явно не разрешена в C, и вы обнаружите, что clang выполняет эту оптимизацию для C, вы должны предположить, что clang содержит ошибки, особенно если другой компилятор не делает то же самое.
@ user17732522, ты читаешь об этом больше, чем есть на самом деле. Единственная цель временного существования — гармонизировать семантику доступа к элементам массива, содержащегося в качестве подобъекта выражения, отличного от lvalue. Даже если бы ваш тип S
предоставлял объекты с временным сроком жизни - это не так - это условие не позволило бы (наблюдаемому) использовать NVRO. Временный объект предположительно можно идентифицировать с одним из именованных объектов, участвующих в возврате структуры по значению, но это не идентифицирует источник и место назначения друг с другом.
@JohnBollinger Да, именно поэтому я указал, что это эквивалентно RVO, а не NRVO, в C++ (до C++17, который исключил временные объекты как объекты результатов функции). Я не говорил о примере в вопросе. RVO будет идентификацией временного объекта результата вызова функции с переменной, которая инициализируется из него.
Хорошо, @user17732522, но для этого вам не нужна временная жизнь. В C нет способа различить, выполняется ли (анонимный) RVO или нет, поэтому это разрешено в соответствии с правилом «как если бы», и так было всегда. Насколько я понимаю, многие компиляторы делают это.
@JohnBollinger Хм, для типов без членов массива это явно ненаблюдаемо, но я подумал, возможно, неявное преобразование массива в указатель позволит наблюдать адрес временного объекта. Но единственный способ, которым мне это удастся сделать, приведет к тому, что временный объект больше не будет инициализировать переменную.
Гарантированно завершается с 0. t
и s
— разные объекты, указатели на них не могут сравниваться равными.
О коде, который вы удалили:
А как насчет следующего варианта, в котором соответствующие соображения по поводу срока службы могут быть другими?
Из https://port70.net/~nsz/c/c11/n1570.html#6.2.4p2:
Значение указателя становится неопределенным, когда объект, на который он указывает (или только что прошедший), достигает конца своего существования.
Он может вернуть 1 или 0 или выполнить ловушку.
Теперь также подтверждено, что это ошибка в clang: github.com/llvm/llvm-project/issues/…
Стоит отметить, что оптимизация была бы разрешена, если бы программа не пыталась ее соблюдать!
Исключение копирования/перемещения — это концепция C++, определенная в разделе 15.8.3 C++17. В некоторых случаях это позволяет оптимизировать вызовы конструктора и деструктора, и в этом случае объекты источника и назначения являются одним и тем же:
Когда определенные критерии соблюдены, реализация может быть опущена. конструкция копирования/перемещения объекта класса, даже если конструктор выбранный для операции копирования/перемещения и/или деструктор для объект имеет побочные эффекты. В таких случаях реализация обрабатывает источник и цель пропущенной операции копирования/перемещения как просто два разные способы обращения к одному и тому же объекту.
В C нет такого положения, хотя, начиная с C17, он позволяет использовать объект с временным сроком жизни, например. массив, содержащийся в структуре в выражении, отличном от lvalue, чтобы не иметь уникального адреса. Однако это неприменимо к данному случаю.
Тот факт, что эта оптимизация была выполнена для программы на языке C, меняет наблюдаемое поведение программы, что нарушает стандарт C. В частности, t
и s
— это разные объекты, время жизни которых перекрывается, и поэтому они должны иметь разные адреса.
Итак, это ошибка в clang, заключающаяся в том, что исключение копирования применяется к программе на C.
Что касается «C не имеет такого положения»: за исключением того, как заметил user17732522, возвращаемый объект с временным сроком жизни может иметь тот же адрес, что и другой объект.
@EricPostpischil Интересно, кажется, в C17 была добавлена часть об отсутствии уникального адреса.
Существует множество причин, по которым программы выполняют сравнения указателей на равенство, и во многих случаях такие сравнения можно наиболее эффективно рассматривать как получение либо 0, либо 1 неопределенным способом без побочных эффектов. В этом примере последний раз обращение к s
будет предшествовать первому обращению к t
, и, таким образом, использование одного и того же хранилища для обоих позволит сделать код более эффективным. Несмотря на то, что объекты технически имеют перекрывающееся время жизни, код, который сравнивает их адреса, редко заботится о результате сравнения, тем более, что большинство вещей, которые код обычно делает на основе результата сравнения, делают это необходимым. присваивать объектам отдельные адреса.
Хотя такое поведение в режиме, который должен соответствовать требованиям, можно считать «ошибкой», режим компиляции, который позволяет объектам использовать общие адреса в некоторых случаях, не разрешенных Стандартом, может для некоторых целей быть более полезным, чем тот, который точно соответствует Стандарту. Например, хотя в стандарте указано, что все статические константные структуры имеют уникальные адреса, компиляторам часто бывает полезнее консолидировать их адреса. К сожалению, документация компилятора часто не в состоянии адекватно документировать крайние случаи, когда поведение может отклоняться от языка Денниса Ритчи или Стандарта.
Ваша программа вызывает неопределенное поведение, поэтому вам не следует ничего ожидать.