使用时我看到了一些意外行为std::variant::operator<
。在类型具有隐式 bool 转换运算符并且其 less 运算符不是成员函数的情况下(在带有 mscv 19.38 编译器的 C++20 中)。
#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
请注意,当满足以下条件之一时,断言开始通过:
- 使用 C++17 代替 C++20
- 使用三向比较运算符代替自由函数运算符
- 未定义隐式转换为 bool 运算符
- 将转换 bool 运算符标记为显式
所有编译器都有相同的行为。
嘿,当我读到标题时,我确切地知道这个代码是什么。我找不到一个很好的重复目标,所以我会尝试将其作为规范答案。
C++17
在 C++17 中,
std::variant
(与标准库中的许多其他类模板一样,,,std::pair
以及std::tuple
其中std::optional
)<
根据推迟到底层类型的定义<
。对底层类型调用的唯一T
操作是。具体来说,
operator<
对两个类型为 的对象variant<T, U>
(假设和<
都定义了)的操作是首先比较索引,如果索引相同,则比较值。如下所示:T
U
C++20
C++20 引入了
<=>
,这通常是处理排序的更好方法,并且带来了许多便利,使编写比较(相等和排序)变得更容易。但它也带来了一个问题,即 C++20 之前没有<=>
可用的代码。因此,我们不能将std::variant
的比较全部更改为使用,<=>
因为没有现有代码使用<=>
。相反,该库会优先使用,但如果不可用,则
<=>
返回。它使用名为 的仅规范对象来实现这一点,该对象在[expos.only.entity]中指定:<
<=>
synth-three-way
这很简单:如果
<=>
可用,我们确实想使用<=>
。但如果<=>
不可用,我们就会回到我们在 C++17 中必须做的事情并使用<
。这就是您想要的行为。
除非……它没有。
让我们回顾一下你的类型:
我们可以看一下各种行为。我在这里假设我们总是提供以下
<
之一<=>
:<
<
<
<
<
<
<
bool
(见下文)进行比较<
<
<=>
<=>
<=>
<=>
<=>
<=>
请记住,规则是:如果
<=>
有效,则使用<=>
,否则返回到<
。但是,我们在语言中没有检查其工作原理的<=>
机制。当你提供一个来与s
<=>
进行比较时Foo
,那么<=>
它就存在并且是可行的,并且是最佳选择,因此它被使用也就不足为奇了。当您提供一个
<
来比较Foo
s 时,这本身并不一定意味着<=>
不可行。当您提供对 的隐式转换时bool
,则f1 <=> f2
仍然可行 - 它评估为(bool)f1 <=> (bool)f2
因为内置候选者可用。这并不特定于bool
- 任何内置类型(如int
或char const*
)或 ADL 可以找到候选者的其他类型都会导致相同的行为。所以根据语言,比较两个Foo
s 和<=>
工作得很好 - 所以这是我们在库中喜欢的机制。只是在这种特定情况下,它给出了令人惊讶的行为,因为您可能更喜欢通过隐式转换显式<
而不是隐式。<=>
bool
这就是为什么将转换运算符标记为显式可以解决问题的原因 - 内置函数
operator<=>(bool, bool)
不再是可行的候选者,因此没有可行的方法<=>
在两个Foo
s 上调用。因此,该库回退到使用<
。请注意,这甚至不是一个新问题。如果提供了对的
Foo
隐式转换bool
,但没有或,即使在 C++17 中,比较仍然有效:通过对 的隐式转换。因为通过该转换,求值将是一个有效的表达式。这里唯一的新奇之处在于,由于 的优先级,即使提供一个 也不能确保库使用您编写的比较运算符。operator<
operator<=>
variant
bool
t < u
<=>
<
这个问题一直存在,因为人们编写的类型有显式比较运算符(通过
<
),但也提供了隐式转换函数,转换为具有内置的<=>
。任何检测到 的存在的库机制<=>
都会在这里给出误报,唯一的解决方案是自己提供显式转换<=>
或使用转换函数explicit
而不是隐式转换。如果我们有一种语言机制来确定具体调用了什么( P2825
t <=> u
中提出了一种机制),那么我们可以添加额外的验证,即我们只选择当和都是可行的并且调用相同类型的东西时(即它们都调用相同的函数,或者后者调用一个名为的函数,这两个函数都采用相同的参数类型)。但在那之前,在存在的情况下要小心使用隐式转换函数。<=>
t <=> u
t < u
operator<=>
operator<
<=>