Этот код работает в GCC и Clang, ошибка отсутствует в MSVC:
#include <concepts>
#include <utility>
struct S {};
const S&& f();
S g();
static_assert(std::same_as<decltype(false ? f() : g()), const S>);
https://godbolt.org/z/99rMPzecM
MSVC считает, что decltype(false ? f() : g()) — это const S&&
Какой из них прав? И почему?





Насколько я понимаю, все компиляторы неправильные, и программа должна быть некорректной.
Существует отправленный, но еще не пронумерованный выпуск CWG , где @BrianBi объясняет этот сценарий, а также приходит к выводу, что формулировка подразумевает неправильную формулировку.
С другой стороны, @Barry скомпилировал эти комбинации типов в условном операторе в P3177R0 и утверждает, что const S будет создано.
Однако эти данные нельзя использовать для каких-либо выводов, поскольку они были собраны путем простого анализа результатов GCC, а не путем анализа стандартных формулировок.
Чтобы в итоге определить правильный тип, рассмотрим правила определения типа условного оператора в [expr.cond].
Помимо прочего, в ... ? f() : g() компилятор пытается преобразовать f() в g() и g() в f(), как описано в [expr.cond] p4.
Для вышеупомянутых преобразований компилятор определяет целевые типы для обеих последовательностей неявного преобразования.
Обратите внимание, что:
f() имеет тип const S&&, который перед любым анализом превратится в значение x типа const S ([expr.type] p1), иg() — это значение типа S.f() в тип, связанный с SЭто преобразование происходит в соответствии с [expr.cond] p4.3:
Если E2 является значением prvalue или если ни одна из приведенных выше последовательностей преобразования не может быть сформирована и хотя бы один из операндов имеет тип класса (возможно, с указанием cv): [...]
g() — это ценное значение, так что это соответствующий случай.
Поскольку const S и S являются одним и тем же типом класса (игнорируя cv-квалификацию), но S менее подходит для cv, [expr.cond] p4.3.1 не применяется, а применяется преобразование lvalue в rvalue до E2, то есть g() ( [expr.cond] p4.3.3):
в противном случае целевой тип — это тип, который будет иметь E2 после применения стандартных преобразований lvalue-to-rvalue, массива-указателя и функции-указателя.
Результат: тип цели: S.
Примечание. E2 — это g(), и ни одно из перечисленных преобразований не применимо, однако это нормально. Поскольку вы, очевидно, не можете применить все три перечисленных преобразования, цель формулировки состоит в том, чтобы применить любое из этих преобразований, если это возможно. Другими словами, тип цели — это просто E2, к которому ничего не применяется.
g() в тип, связанный с const SЕсли E2 является значением x, целевым типом является «ссылка rvalue на T2», но последовательность неявного преобразования может быть сформирована только в том случае, если ссылка будет привязана напрямую.
Все условия здесь удовлетворены, учитывая, что f() является значением x, а ссылка const S&& может напрямую связываться со значением prvalue типа S ( [dcl.init.ref] p5.3.1).
Результат: тип цели — «ссылка на rvalue на const S».
Теперь компилятор проверяет, можно ли выполнить неявные преобразования с заданными целевыми типами.
f() можно преобразовать в S с помощью неявно определенного (тривиального) конструктора копирования, который будет вызываться как часть определяемой пользователем последовательности преобразования , иg() можно преобразовать в const S&&, просто привязав ссылку.Теперь мы должны столкнуться с [expr.cond] p4, предложение 5:
Если обе последовательности могут быть сформированы или одна может быть сформирована, но это неоднозначная последовательность преобразования, программа является неправильной.
На этом этапе компилятор должен отклонить код, однако никто этого не делает.
Если бы мы продолжали работать так, как будто программа не была правильно сформирована, мы бы столкнулись с [expr.cond] p6:
В противном случае результатом будет prvalue. [...] В противном случае применяются определенные таким образом преобразования, и преобразованные операнды используются вместо исходных операндов в оставшейся части этого подраздела.
Результатом является prvalue, и мы применяем последний этап преобразования lvalue в rvalue, чтобы получить g() (поскольку оно было преобразовано в ссылку rvalue).
Судя по всему, GCC считает, что это значение prvalue должно быть типа const S, но нам не следовало доходить до этого момента.
Поскольку результатом является prvalue и decltype не применяется к непарному выражению идентификатора, decltype должно выдавать S или const S ([dcl.type.decltype] p1.6).
MSVC, вероятно, думает, что одно из двух преобразований не удалось, и в этом случае результат может быть const S&&:
Если второй и третий операнды являются glvalues одной и той же категории значений и имеют один и тот же тип, результат имеет этот тип и категорию значения и является битовым полем, если второй или третий операнд является битовым полем или если оба являются битовыми полями.
Однако неясно, почему MSVC считает, что f() нельзя преобразовать в S, что привело бы к сценарию, в котором оба операнда теперь являются значениями x типа const S&&.
Ну, во всяком случае, я, должно быть, понял что-то не так; Мне интересно, почему ни один компилятор не отвергает это. Мне кажется, что ссылка привязывается напрямую, поэтому можно сформировать обе последовательности преобразования, а это недопустимо. Но если ни один компилятор не отвергает это, а так и должно быть, скорее всего, мы читаем это неправильно.
@JanSchultke преобразование f() в g() невозможно сформировать. Потому что S не имеет такого же резюме, как const S.
@ValueError Это означает, что он переходит к следующему применимому случаю, wg21.link/expr.cond#4.3.3, поэтому целевой тип S
@Artyer Я обновил вопрос, и вы уже близки, но преобразование lvalue в rvalue для f() не приведет к удалению cv-квалификатора, поэтому результирующий тип будет const S, а не S.
@Artyer g() — это prvalue, можно ли применить lvalue к rvalue к prvalue? После преобразования const S по-прежнему отличается от S.
@JanSchultke Это тот тип, который E2 будет иметь после этих преобразований. E2 — это значение prvalue с типом S в данном случае, а не значение const S xvalue.
@ValueError В стандарте есть множество мест, где lvalue-to-rvalue применяется к вещам, которые могут быть glvalue или prvalues. Ничего не произойдет, если это уже значение prvalue. Преобразование массива в указатель применяется и к не-массиву, с этим тоже ничего не происходит
@JanSchultke Как сказал Артьер, после преобразования E2 — это значение prvalue типа S. Таким образом, результатом должно быть значение x типа const S. Тогда decltype выдаст const S&&, значит, MSVC верен?
@Artyer, как я прочитал, существует преобразование из E1 f(), который имеет тип const S, в тип, связанный с целевым типом T2, S. Однако я не вижу причины, по которой это преобразование приведет к удалению cv-квалификатора f(), чего обычно не происходит; только для фундаментальных типов.
@ValueError Вам нужно прочитать дальше, чтобы выяснить, каким будет тип условного оператора. Это будет значение x, только если оба операнда являются glvalue одного и того же типа (eel.is/c++draft/expr.cond#5). Я не думаю, что какой-либо компилятор прав в этом случае, но MSVC ошибается в другом. Он, должно быть, думает, что одно из преобразований невозможно или что-то в этом роде; возможно, он думает, что возможно только преобразование из g() в const S, и тогда вы получаете, так сказать, ссылку на rvalue с обеих сторон.
@JanSchultke Преобразование применяется к E2 g() с типом S, а не к E1 f() с типом const S
@JanSchultke Преобразование из f() в тип, связанный с S, будет сформировано, потому что const S не может преобразоваться в S. Таким образом, последний второй и третий операнды — это значение x с типом const S
@ValueError, почему const S нельзя конвертировать в S? S имеет неявно определенный (тривиальный) конструктор копирования, поэтому, когда компилятор пытается сформировать ICS, это будет включать вызов этого конструктора как определяемое пользователем преобразование. Кроме того, я не уверен, что вы имеете в виду, говоря об E2.
@JanSchultke Ты прав
Я посмотрел на P3177R0, и это, казалось, указывало на то, что MSVC был неправ, но я также не мог понять, почему это так. Может быть, этот результат просто иллюстрирует поведение GCC/Clang?
@ValueError, основываясь на всем, что я прочитал, и на всем, что я сейчас изучил (а это довольно много), я думаю, что предполагаемое и реализованное GCC поведение заключается в создании const S. Однако формулировка несовершенна, поэтому никто на самом деле не прав; MSVC еще дальше от желаемого результата. Это компилятор с закрытым исходным кодом, так что кто знает почему.
Если функция
fвозвращает ссылку на неконстантное значениеS&& f(), то все три компилятора соглашаются, что результат тернарного оператора имеет не-ссылочный типS. Таким образом, GCC/Clang в вашем случае, по крайней мере, более последовательны, а MSVC неожиданно меняет свое мнение.