Условное выражение создает разные типы между MSVC и GCC/Clang

Этот код работает в 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&&

Какой из них прав? И почему?

Если функция f возвращает ссылку на неконстантное значение S&& f(), то все три компилятора соглашаются, что результат тернарного оператора имеет не-ссылочный тип S. Таким образом, GCC/Clang в вашем случае, по крайней мере, более последовательны, а MSVC неожиданно меняет свое мнение.

Fedor 20.04.2024 14:52
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
8
1
204
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Насколько я понимаю, все компиляторы неправильные, и программа должна быть некорректной.

Связанных с работой

Существует отправленный, но еще не пронумерованный выпуск CWG , где @BrianBi объясняет этот сценарий, а также приходит к выводу, что формулировка подразумевает неправильную формулировку.

С другой стороны, @Barry скомпилировал эти комбинации типов в условном операторе в P3177R0 и утверждает, что const S будет создано. Однако эти данные нельзя использовать для каких-либо выводов, поскольку они были собраны путем простого анализа результатов GCC, а не путем анализа стандартных формулировок.

Языковое адвокатство

Чтобы в итоге определить правильный тип, рассмотрим правила определения типа условного оператора в [expr.cond]. Помимо прочего, в ... ? f() : g() компилятор пытается преобразовать f() в g() и g() в f(), как описано в [expr.cond] p4.

Шаг 1. Определение типов целей

Для вышеупомянутых преобразований компилятор определяет целевые типы для обеих последовательностей неявного преобразования.

Обратите внимание, что:

  • 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», но последовательность неявного преобразования может быть сформирована только в том случае, если ссылка будет привязана напрямую.

- [expr.cond] п4.2

Все условия здесь удовлетворены, учитывая, что f() является значением x, а ссылка const S&& может напрямую связываться со значением prvalue типа S ( [dcl.init.ref] p5.3.1).

Результат: тип цели — «ссылка на rvalue на const S».

Шаг 2. Неявные преобразования

Теперь компилятор проверяет, можно ли выполнить неявные преобразования с заданными целевыми типами.

  • f() можно преобразовать в S с помощью неявно определенного (тривиального) конструктора копирования, который будет вызываться как часть определяемой пользователем последовательности преобразования , и
  • g() можно преобразовать в const S&&, просто привязав ссылку.

Теперь мы должны столкнуться с [expr.cond] p4, предложение 5:

Если обе последовательности могут быть сформированы или одна может быть сформирована, но это неоднозначная последовательность преобразования, программа является неправильной.

На этом этапе компилятор должен отклонить код, однако никто этого не делает.

Перспектива GCC/clang

Если бы мы продолжали работать так, как будто программа не была правильно сформирована, мы бы столкнулись с [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

MSVC, вероятно, думает, что одно из двух преобразований не удалось, и в этом случае результат может быть const S&&:

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

- [expr.cond] p5

Однако неясно, почему MSVC считает, что f() нельзя преобразовать в S, что привело бы к сценарию, в котором оба операнда теперь являются значениями x типа const S&&.

Ну, во всяком случае, я, должно быть, понял что-то не так; Мне интересно, почему ни один компилятор не отвергает это. Мне кажется, что ссылка привязывается напрямую, поэтому можно сформировать обе последовательности преобразования, а это недопустимо. Но если ни один компилятор не отвергает это, а так и должно быть, скорее всего, мы читаем это неправильно.

Jan Schultke 19.04.2024 17:20

@JanSchultke преобразование f() в g() невозможно сформировать. Потому что S не имеет такого же резюме, как const S.

ValueError 19.04.2024 17:28

@ValueError Это означает, что он переходит к следующему применимому случаю, wg21.link/expr.cond#4.3.3, поэтому целевой тип S

Artyer 19.04.2024 17:32

@Artyer Я обновил вопрос, и вы уже близки, но преобразование lvalue в rvalue для f() не приведет к удалению cv-квалификатора, поэтому результирующий тип будет const S, а не S.

Jan Schultke 19.04.2024 17:37

@Artyer g() — это prvalue, можно ли применить lvalue к rvalue к prvalue? После преобразования const S по-прежнему отличается от S.

ValueError 19.04.2024 17:39

@JanSchultke Это тот тип, который E2 будет иметь после этих преобразований. E2 — это значение prvalue с типом S в данном случае, а не значение const S xvalue.

Artyer 19.04.2024 17:44

@ValueError В стандарте есть множество мест, где lvalue-to-rvalue применяется к вещам, которые могут быть glvalue или prvalues. Ничего не произойдет, если это уже значение prvalue. Преобразование массива в указатель применяется и к не-массиву, с этим тоже ничего не происходит

Artyer 19.04.2024 17:46

@JanSchultke Как сказал Артьер, после преобразования E2 — это значение prvalue типа S. Таким образом, результатом должно быть значение x типа const S. Тогда decltype выдаст const S&&, значит, MSVC верен?

ValueError 19.04.2024 17:51

@Artyer, как я прочитал, существует преобразование из E1 f(), который имеет тип const S, в тип, связанный с целевым типом T2, S. Однако я не вижу причины, по которой это преобразование приведет к удалению cv-квалификатора f(), чего обычно не происходит; только для фундаментальных типов.

Jan Schultke 19.04.2024 18:00

@ValueError Вам нужно прочитать дальше, чтобы выяснить, каким будет тип условного оператора. Это будет значение x, только если оба операнда являются glvalue одного и того же типа (eel.is/c++draft/expr.cond#5). Я не думаю, что какой-либо компилятор прав в этом случае, но MSVC ошибается в другом. Он, должно быть, думает, что одно из преобразований невозможно или что-то в этом роде; возможно, он думает, что возможно только преобразование из g() в const S, и тогда вы получаете, так сказать, ссылку на rvalue с обеих сторон.

Jan Schultke 19.04.2024 18:04

@JanSchultke Преобразование применяется к E2 g() с типом S, а не к E1 f() с типом const S

ValueError 19.04.2024 18:05

@JanSchultke Преобразование из f() в тип, связанный с S, будет сформировано, потому что const S не может преобразоваться в S. Таким образом, последний второй и третий операнды — это значение x с типом const S

ValueError 19.04.2024 18:17

@ValueError, почему const S нельзя конвертировать в S? S имеет неявно определенный (тривиальный) конструктор копирования, поэтому, когда компилятор пытается сформировать ICS, это будет включать вызов этого конструктора как определяемое пользователем преобразование. Кроме того, я не уверен, что вы имеете в виду, говоря об E2.

Jan Schultke 19.04.2024 18:21

@JanSchultke Ты прав

ValueError 19.04.2024 18:40

Я посмотрел на P3177R0, и это, казалось, указывало на то, что MSVC был неправ, но я также не мог понять, почему это так. Может быть, этот результат просто иллюстрирует поведение GCC/Clang?

ValueError 19.04.2024 19:40

@ValueError, основываясь на всем, что я прочитал, и на всем, что я сейчас изучил (а это довольно много), я думаю, что предполагаемое и реализованное GCC поведение заключается в создании const S. Однако формулировка несовершенна, поэтому никто на самом деле не прав; MSVC еще дальше от желаемого результата. Это компилятор с закрытым исходным кодом, так что кто знает почему.

Jan Schultke 19.04.2024 19:44

Другие вопросы по теме