Estou vendo algum comportamento inesperado ao usar o std::variant::operator<
. Na situação em que o tipo possui um operador de conversão bool implícito e seu operador less não é uma função membro (em C++20 com compilador mscv 19.38).
#include <variant>
struct Foo {
int x;
int y;
#ifndef DROP_CAST_OP
constexpr operator bool() const { return x || y; }
#endif
#ifdef USE_SPACESHIP
constexpr auto operator<=>(const Foo&) const noexcept = default;
#else
friend constexpr bool operator<(const Foo& a, const Foo& b) noexcept
{
return a.x < b.x || (a.x == b.x && a.y < b.y);
}
#endif
};
using TestVariant = std::variant<Foo, int>;
constexpr Foo fooA { 0, 1 };
constexpr Foo fooB { 1, 0 };
constexpr std::variant<Foo, int> varA = fooA;
constexpr std::variant<Foo, int> varB = fooB;
static_assert(fooA < fooB);
static_assert(varA < varB);
https://godbolt.org/z/1zfq5dq1r
Observe que a asserção começa a passar quando uma das seguintes condições é atendida:
- use C++17 em vez de C++20
- use o operador de comparação de três vias em vez do operador de função livre menos
- não definindo conversão implícita para operador bool
- marcando o operador bool de conversão como explícito
Todos os compiladores têm o mesmo comportamento.
Heh, eu sabia exatamente qual seria esse código quando li o título. Não consigo encontrar um grande alvo ingênuo, então tentarei fazer desta a resposta canônica.
C++17
Em C++ 17,
std::variant
(como vários outros modelos de classe na biblioteca padrão,std::pair
,std::tuple
, estd::optional
entre eles) define<
em termos de adiamento aos tipos subjacentes'<
. A única operação invocada no tipo subjacente foiT
.Especificamente, o que
operator<
faria em dois objetos do tipovariant<T, U>
(assumindo<
que foi definido para ambosT
eU
) é primeiro comparar os índices e, se forem iguais, comparar os valores. Algo assim:C++20
O C++ 20 foi introduzido
<=>
, que geralmente é uma maneira muito melhor de lidar com pedidos e traz muitas conveniências para facilitar a escrita de comparações (igualdade e ordem). Mas também surgiu o problema de nenhum código anterior ao C++ 20 estar<=>
disponível. Portanto, não podemos vender apenasstd::variant
a comparação de use<=>
porque nenhum código existente usa<=>
.Em vez disso, a biblioteca usa preferencialmente,
<=>
mas recorre<
se<=>
não estiver disponível. Isso é feito com um objeto somente de especificação chamadosynth-three-way
, especificado em [expos.only.entity] :É bastante simples: se
<=>
estiver disponível, realmente queremos usar o<=>
. Mas se<=>
não estiver disponível, voltamos ao que tínhamos que fazer em C++ 17 e usamos<
.E isso tem o comportamento que você deseja.
Exceto quando... isso não acontece.
Vejamos seu tipo:
Podemos passar por vários comportamentos. Presumo aqui que sempre fornecemos exatamente um de
<
ou<=>
:<
<
<
<
<
<
<
bool
(veja abaixo)<
<
<=>
<=>
<=>
<=>
<=>
<=>
Tenha em mente que a regra é: se
<=>
funciona , use<=>
, caso contrário, volte para<
. Porém, não temos um mecanismo na linguagem para verificar como<=>
funciona.Quando você fornece um
<=>
para comparar osFoo
s, então<=>
existe e é viável e é a melhor opção, por isso não é surpresa que seja usado.Quando você fornece um
<
para comparar osFoo
s, isso por si só não significa necessariamente que<=>
não seja viável. Quando você fornece uma conversão implícitabool
para , entãof1 <=> f2
ainda é viável - ela é avaliada como(bool)f1 <=> (bool)f2
porque os candidatos internos estão disponíveis. Isso não é específico parabool
- qualquer tipo interno (comoint
ouchar const*
) ou outro tipo para o qual o ADL possa encontrar um candidato levaria ao mesmo comportamento. Portanto, de acordo com a linguagem, comparar doisFoo
s com<=>
funciona perfeitamente - esse é o mecanismo que preferimos na biblioteca. Só que neste caso específico dá um comportamento surpreendente, já que você provavelmente preferiu o explícito<
ao implícito<=>
por meio dabool
conversão implícita.É por isso que marcar o operador de conversão explícito resolve o problema - o builtin
operator<=>(bool, bool)
não é mais um candidato viável, portanto não há maneira viável de invocar<=>
doisFoo
s. Portanto, a biblioteca volta a usar<
.Observe que este não é um problema novo. Se
Foo
tivesse fornecido uma conversão implícita parabool
, mas nem anoperator<
nem aoperator<=>
, mesmo em C++ 17 avariant
comparação ainda funcionaria: por meio da conversão implícita parabool
. Porque avaliart < u
seria uma expressão válida por meio dessa conversão. A única novidade aqui é que, devido à priorização de<=>
, mesmo fornecer an<
não garante que a biblioteca use o operador de comparação que você escreveu.Esse é um problema que sempre surge, porque as pessoas escrevem tipos que possuem operadores de comparação explícitos (via
<
), mas também fornecem uma função de conversão implícita para um tipo que possui um builtin<=>
. Qualquer mecanismo de biblioteca que detecte a presença de<=>
fornecerá um falso positivo aqui, e a única solução é fornecer<=>
você mesmo um explícito ou tornar a função de conversãoexplicit
em vez de implícita.Se tivéssemos um mecanismo de linguagem para descobrir o que especificamente
t <=> u
invocado (e há um proposto em P2825 ), então poderíamos adicionar validação adicional de que apenas selecionamos<=>
set <=> u
et < u
são viáveis e invocamos o mesmo tipo de coisa (ou seja, que ambos invocam o mesmooperator<=>
ou se este invocar uma função chamadaoperator<
que ambas as funções recebam os mesmos tipos de parâmetros). Mas até que isso aconteça, tome cuidado com funções de conversão implícitas na presença de<=>
.