为什么不可能重载三元运算符

Why is it not possible to overload the ternary operator?

本文关键字:三元 运算符 不可能 重载 为什么      更新时间:2023-10-16

为什么不能重载三元运算符"?:"?

我经常使用三元运算符来合并if语句,我很好奇为什么语言设计者选择禁止这个运算符重载。我在C++运算符重载中寻找原因的解释,但没有找到描述为什么这不可能的解释。脚注提供的唯一信息是不能重载。

我最初的猜测是,重载运算符几乎总是会违反上面链接中给出的第一或第二条原则。重载的含义很少是明显或清晰的,或者它会偏离其最初已知的语义。

因此,我的问题更多的是为什么这不可能,而不是如何做到,因为我知道这是不可能的。

如果你可以覆盖三元运算符,你必须写这样的东西:

xxx operator ?: ( bool condition, xxx trueVal, xxx falseVal );

要调用覆盖,编译器必须计算trueValfalseVal的值。内置的三元运算符不是这样工作的——它只计算其中一个值,这就是为什么你可以写这样的东西:

return p == NULL ? 23 : p->value;

而不用担心通过NULL指针进行间接操作。

我认为当时的主要原因是它似乎不值得为该运算符发明新语法的努力。没有令牌?:,因此您必须创建一个专门为它制定的语法规则operator后面跟一个运算符,该运算符是单个代币。)

正如我们(从经验中)学到的那样,使用运算符重载更合理地说,我们真的不应该允许&&||过载其他回复指出的原因,可能不是运算符逗号(因为重载版本没有用户期望的序列点)。所以动机对它的支持甚至比最初更少。

三元运算符的原理之一是,仅根据条件表达式的真或假来评估真/假表达式。

cond ? expr1 : expr2

在该示例中,仅当cond为真时才评估expr1,而当cond为假时才评估expr2。记住这一点,让我们看看三元重载的签名会是什么样子(为了简单起见,这里使用固定类型而不是模板)

Result operator?(const Result& left, const Result& right) { 
  ...
}

这个签名根本不合法,因为它违反了我描述的确切语义。为了调用此方法,该语言必须同时评估expr1expr2,因此不再有条件地评估它们。为了支持三元,操作员要么需要

  1. 为每个值取一个lambda,这样它就可以根据需要生成它们。这必然会使调用代码复杂化,因为它必须考虑逻辑上不存在lambda的lambda调用语义
  2. 三元运算符需要返回一个值来表示编译器应该使用expr1还是expr2

编辑

有些人可能会争辩说,在这种情况下,没有短路是可以的。原因是C++已经允许您使用||&& 在操作员过载时违反短路

Result operator&&(const Result& left, const Result& right) { 
  ...
}

尽管我仍然觉得这种行为让C++感到困惑。

简短而准确的答案只是"因为这就是Bjarne的决定。"

尽管关于应该对哪些操作数进行求值以及按何种顺序求值的自变量在技术上准确地描述了发生的事情,但它们几乎没有(实际上)解释为什么这个特定的运算符不能重载。

特别是,相同的基本自变量同样适用于其他运算符,如operator &&operator||。在这些运算符中的每一个的内置版本中,计算左操作数,然后,如果且仅当它为&&生成1或为||生成0时,计算右操作数。同样,(内置)逗号运算符计算其左操作数,然后计算其右操作数。

在这些运算符中任何一个的重载版本中,两个操作数都将始终求值(按未指定的顺序)。因此,在这方面,它们本质上与重载的三元运算符相同。它们都失去了关于计算什么操作数以及按什么顺序计算的相同保证。

至于Bjarne做出这个决定的原因:我看到了一些可能性。一个是,尽管从技术上讲它是一个运算符,但三元运算符主要用于流量控制,因此重载它更像是重载ifwhile,而不是重载大多数其他运算符。

另一种可能性是,它在语法上会很难看,需要解析器处理类似operator?:的东西,这需要将?:定义为令牌等等——所有这些都需要对C语法进行相当严重的更改。至少在我看来,这个论点似乎很弱,因为C++已经需要一个比C复杂得多的解析器,而这个更改实际上比许多其他更改都要小得多。

也许最有力的论点只是,它似乎不会取得多大成就。由于它主要用于流控制,因此更改它对某些类型的操作数所做的操作不太可能实现任何非常有用的事情。

出于同样的原因,为什么你真的不应该(尽管你可以)过载&&||操作员-这样做会禁用这些操作员的短路(只评估必要的部分,而不是所有的),这可能会导致严重的并发症。

以前的答案集中在短路上,这在一定程度上是有效的,但甚至不是尝试这样做的真正问题

现有三元运算符(无短路)的最接近实现必须如下所示:

template<typename T0, typename T1>
std::variant<T0, T1>&& operator?:(bool predicate, T0&& arg0, T1&& arg1)
{
    if(predicate)
        return { std::forward<T0&&>(arg0) };
    return { std::forward<T1&&>(arg1); }
}

然而,T0可能是无效的。T1可能是空的。这两种情况都不会发生。

该变体是必要的,因为T0和T1可能不能隐式地相互转换,并且返回类型不能用于函数重载解析,这是添加了C++17库。但它仍然不能真正起作用,因为变体不能隐式转换为任何可能的类型。