Este código funciona em GCC e Clang, bug não em 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 pensa decltype(false ? f() : g())
que éconst S&&
Qual deles está certo? E porque?
Com base no meu entendimento, todos os compiladores estão errados e o programa deveria estar mal formado.
Trabalho relatado
Há uma questão do CWG enviada, mas ainda não numerada, onde @BrianBi explica esse cenário e também conclui que o texto implica má formação.
Por outro lado, @Barry compilou essas combinações de tipos no operador condicional em P3177R0 e afirma que
const S
seriam produzidas. Não está claro se isso está correto e como o autor chegou a essa conclusão.Advogado Idiomático
Para determinar o tipo correto no final, considere as regras para determinar o tipo do operador condicional em [expr.cond] . Entre outras coisas, em
... ? f() : g()
, o compilador tenta converterf()
parag()
eg()
paraf()
, conforme descrito em [expr.cond] p4 .Etapa 1: Determinando os tipos de destino
Para efeitos das conversões mencionadas acima, o compilador determina os tipos de destino para ambas as sequências de conversão implícitas.
Observe que:
f()
é do tipoconst S&&
, que se transformaria em um xvalue do tipoconst S
antes de qualquer análise ( [expr.type] p1 ), eg()
é um pré-valor do tipoS
.Conversão de
f()
para um tipo relacionado aS
Esta conversão ocorre de acordo com [expr.cond] p4.3 :
g()
é um pré-valor, então este é o caso relevante.Como
const S
eS
são do mesmo tipo de classe (ignorando a qualificação cv), masS
são menos qualificados para cv, [expr.cond] p4.3.1 não se aplica, mas sim, a conversão de lvalor em rvalor é aplicada a E2, que ég()
( [expr.cond] p4.3.3 ):Resultado: o tipo de destino é
S
Nota:
E2
ég()
e nenhuma das conversões listadas é aplicável, no entanto, tudo bem. Como obviamente você não pode aplicar todas as três conversões listadas, a intenção do texto é que qualquer uma dessas conversões seja aplicada, se possível. Em outras palavras, o tipo de destino simplesmente não temE2
nada aplicado.Conversão de
g()
para um tipo relacionado aconst S
- [expr.cond] p4.2
Todas as condições aqui são satisfeitas, visto que
f()
é um xvalue e uma referênciaconst S&&
pode ser vinculada diretamente a um pré-valor do tipoS
( [dcl.init.ref] p5.3.1 ).Resultado: o tipo de destino é "rvalue reference to
const S
"Etapa 2: conversões implícitas
Agora, o compilador verifica se as conversões implícitas podem ser executadas com os tipos de destino fornecidos.
f()
pode ser convertido paraS
usar o construtor de cópia definido implicitamente (trivial) que seria chamado como parte de uma sequência de conversão definida pelo usuário , eg()
pode ser convertidoconst S&&
simplesmente vinculando uma referência.Devemos agora encontrar [expr.cond] p4, frase 5 :
O compilador deve rejeitar o código neste ponto, porém ninguém o faz.
A perspectiva do GCC/clang
Se continuássemos como se o programa não estivesse bem formado, encontraríamos [expr.cond] p6 :
O resultado é um pré-valor e aplicamos uma rodada final de conversão de lvalor em rvalor para fazer
g()
(uma vez que foi convertido em uma referência de rvalor). Aparentemente, o GCC acha que esse valor deveria ser do tipoconst S
, mas nunca deveríamos ter chegado a esse ponto.Como o resultado é um pré-valor e
decltype
não está sendo aplicado a uma expressão de id sem parênteses,decltype
deve produzirS
orconst S
( [dcl.type.decltype] p1.6 ).A perspectiva MSVC
O MSVC provavelmente pensa que uma das duas conversões falhou; nesse caso, o resultado pode ser
const S&&
:- [expr.cond] p5
No entanto, não está claro por que o MSVC pensa que isso
f()
não pode ser convertido emS
, o que teria resultado em um cenário em que ambos os operandos são xvalores do tipoconst S&&
agora.