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

26

为什么不能重载三目运算符'?:'?

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

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

所以我的问题更多的是为什么不可能而不是如何,因为我知道它无法完成。


能否在三元运算符中重载所有可能被解释为“评估”的内容不足够吗? - WhozCraig
可能是如何重载条件运算符?的重复问题。 - Cloud
1
你想要对它进行什么样的重载?这是一个 if-else 语句,你如何以有意义的方式进行更改? - Mats Petersson
1
感谢您的评论和链接。我确实看到了作为潜在重复的链接问题,但是没有找到任何回答我的问题的答案。 - Paul Renton
6个回答

25

如果您可以覆盖三元运算符,那么您需要编写类似于以下内容:

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

为了调用您的重写函数,编译器需要计算 trueValfalseVal 的值。这不是内置三元运算符的工作方式 - 它只计算其中一个值,这就是为什么您可以编写以下内容的原因:

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

无需担心通过空指针间接访问的问题。


22
相同的推理也适用于 &&||,你是可以重载它们的。 - James Kanze
11
@JamesKanze 那真是一个多么可怕的决定啊。 - Nik Bougalis
3
当然。事后诸葛亮是一件很棒的事情。个人而言,我希望C++11默认禁用重载&&||,运算符,除非程序员包含了某些特殊头文件(或传递了特殊编译器开关)。我认为让一个语言在向后兼容的祭坛上永久地传递过去的错误是不明智的。 - Nik Bougalis
1
@NikBougalis:向后兼容性是一个巨大的负担。有些编译器会保留它们与标准不兼容的特性,只是因为它们的客户使用了这些不兼容的特性。这只是其中之一(例如禁止重载 ||,你会发现供应商将继续支持它作为扩展,因为他们的客户正在使用它——无论他们是否应该使用它,毕竟这是他们的钱...) - David Rodríguez - dribeas
2
@NikBougalis 标准的作用并不是促进良好实践(事实上,在许多情况下,它恰恰相反),而是标准化现有实践。今天没有任何东西会阻止编译器警告这种实践。(但这并不是非黑即白的。例如,标准可以将其弃用,以鼓励编译器警告。) - James Kanze
显示剩余3条评论

18

我认为当时没必要为了这个运算符发明一种新的语法,主要原因在于:没有 ?: 的标记,所以你需要为此创建若干特定的语法规则。(现行语法规则是一个操作符后面跟着另一个操作符,这是一个单独的标记。)

随着我们(通过经验)更合理地使用运算符重载,已经变得很明显,我们实际上也不应该允许对 &&|| 进行重载,原因如其他回答所指出的那样,可能也不应该支持逗号运算符(由于重载版本不会有用户预期的序列点)。因此,支持它的动机甚至比最初还要少。


“?”和“:”是分开的,但语法中的产生规则是作为一个整体进行处理的。我曾经在yacc中实现过这个运算符,使用的代码类似于: expr : expr '?' expr ':' expr { $$ = new ConditionalOp($1, $3, $5); }; - Daniel Frużyński

9
三元运算符的原则之一是,真/假表达式仅基于条件表达式的真实性或虚假性进行评估。
cond ? expr1 : expr2

在这个例子中,只有当条件为真时才会评估expr1,而只有当条件为假时才会评估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++来说也令人困惑。

4
简短而准确的答案是“因为Bjarne决定了这样做”。虽然关于哪些操作数应该被评估以及以什么顺序进行的争论提供了一个技术上准确的描述,但它们很少(实际上没有)解释为什么这个特定的运算符不能被重载。特别地,同样的基本争论也适用于其他运算符,如operator &&和operator ||。在每个内置版本的这些运算符中,左操作数被评估,然后仅当产生1(对于&&)或0(对于||)时,右操作数才被评估。同样,逗号运算符(内置版本)评估其左操作数,然后评估其右操作数。在任何这些运算符的重载版本中,两个操作数始终被评估(按未指定的顺序)。因此,在这方面,它们与重载三元运算符本质上是相同的。它们都失去了关于评估哪些操作数以及以什么顺序进行的相同保证。至于为什么Bjarne做出了这个决定:我可以看到一些可能性。其中之一是虽然它在技术上是一个运算符,但三元运算符主要用于流程控制,因此重载它更像是重载if或while,而不是大多数其他运算符的重载。另一个可能性是它会在语法上很丑陋,需要解析器处理类似于operator?:这样的东西,这要求将?:定义为一个标记等等 - 所有这些都需要对C语法进行相当严重的更改。至少在我的看法中,这个论点似乎相当薄弱,因为C++已经比C需要一个更复杂的解析器,而这个变化实际上比许多其他变化要小得多。也许最有力的论据是,它似乎不会有什么作用。由于它主要用于流程控制,改变它对某些类型的操作数所做的事情不太可能实现任何非常有用的东西。

2
当时的真正争议在于语法要求重载函数命名为“operator <op>”,其中<op>是一个单独的标记。你需要额外的语法规则来处理这个标记。 - James Kanze
1
@JamesKanze:我正在进行编辑以涵盖这一点。对于解析C++来说,复杂性在我看来使得这个论点最多只能算是很薄弱。将“?:”定义为一个令牌(即使它只在一个地方使用)并不是什么难事。正如您所知,解析C++的其他部分要困难得多. - Jerry Coffin
2
这就是原因,至少那是我从那些有发言权的人那里听到的。但我认为这是解析器问题与感觉相结合,认为它不会带来太多好处。如果你没有得到任何回报,那么增加最微小的复杂性是没有意义的。而“&&”和“||”是“免费”的,所以没有禁止它们的必要,因为当时还没有意识到它们会变得多么无用。 - James Kanze

0

之前的回答都集中在短路计算上,这是有点道理的,但我认为这并不是尝试这样做的真正问题。

最接近现有三元操作符的实现(没有短路计算)应该像这样:

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也可能是无效的。在这两种情况下都无法构建。

使用variant是必要的,因为T0和T1可能不能互相隐式转换,并且返回类型不能用于函数重载决策,这是C++17库的一个补充。但它仍然不是很实用,因为variant不能隐式转换为其任何可能的类型。


0
由于同样的原因,您确实不应该(尽管可以)重载&&||运算符-这样做会禁用这些运算符上的短路(仅评估必要部分而不是所有内容),这可能会导致严重的复杂性。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接