三元运算符与if语句相比的优势

41

我正在浏览一些代码,发现其中有几个三元运算符。这段代码是我们使用的一个库,应该非常快。

我在想除了节省空间之外,我们是否还能获得其他优势。

你有什么经验吗?


6
如果三目运算符比if语句快(或反之),编译器肯定会将其中一种转换成另一种。因此,它们不应该具有不同的性能特征(假设你使用相同的质量来编写两种语句)。 - Lasse Espeholt
2
如果有什么疑问,那就进行基准测试,这只是微小的优化。 - Gordon
可能是[使用条件?:(三元)运算符的好处]的重复问题(https://dev59.com/rXA75IYBdhLWcg3waIMJ)。 - nawfal
1
哎呀,为了推销自己而顶起老问题?朋友,你可能会被禁言的。 - hummingBird
1
nawfal:那个链接是特别针对C#的,所以在我看来不是一个好的重复。 - Tony Delroy
最好只问一个语言,否则可能无法回答。C++版本:https://dev59.com/83A65IYBdhLWcg3w6DJO - Ciro Santilli OurBigBook.com
5个回答

67

性能

三元运算符在性能上不应该与一个良好编写的等效的if/else语句有所不同......它们可能会解析为抽象语法树中的相同表示形式,经过相同的优化等。

只能使用 ? : 的事情:

如果您正在初始化常量或引用,或者在成员初始化列表中计算要使用哪个值,则不能使用if/else语句,但可以使用? :

const int x = f() ? 10 : 2;

X::X() : n_(n > 0 ? 2 * n : 0) { }

如何精简代码

使用 ? : 的主要原因包括本地化和避免在同一语句/函数调用中重复冗余的部分,例如:

if (condition)
    return x;
else
    return y;

...仅仅是更可取的选择...

return condition ? x : y;

如果面对经验不足的程序员,或某些术语太过复杂,? : 结构将会在噪音中丢失,因此出于可读性的考虑可以避免使用该结构。但对于更复杂的情况,比如:

fn(condition1 ? t1 : f1, condition2 ? t2 : f2, condition3 ? t3 : f3);

一个等效的 if/else

if (condition1)
    if (condition2)
        if (condition3)
            fn(t1, t2, t3);
        else
            fn(t1, t2, f3);
    else if (condition3)
            fn(t1, f2, t3);
        else
            fn(t1, f2, f3);
else
    if (condition2)
       ...etc...

这会增加许多额外的函数调用,而编译器可能会或可能不会对其进行优化。

另外, 允许你选择一个对象,然后使用其中的成员:

(f() ? a : b).fn(g() ? c : d).field_name);

等价的if/else语句如下:

if (f())
    if (g())
        x.fn(c.field_name);
    else
        x.fn(d.field_name);
else
    if (g())
        y.fn(c.field_name);
    else
        y.fn(d.field_name);

能否用命名临时变量改善上面的if/else代码混乱呢?

如果表达式 t1, f1, t2 等过于冗长重复,那么创建命名临时变量可能会有所帮助,但是:

  • 要想获得与 ? : 相匹配的性能,您可能需要使用 std::move,除非相同的临时变量传递给调用函数中的两个 && 参数:然后您必须避免这种方式。 那样会更加复杂和容易出错。

  • c ? x : y 会先计算 c 然后只计算 xy 中的一个,这使得我们可以安全地测试指针是否为 nullptr,同时提供一些回退值/行为。 代码仅获得实际选择的 x y 中的一个的副作用。 使用命名临时变量时,您可能需要在其初始化周围使用 if / else? : 以防止执行不必要的代码或执行比预期更频繁的代码。

功能差异:统一结果类型

考虑以下代码:

void is(int) { std::cout << "int\n"; }
void is(double) { std::cout << "double\n"; }

void f(bool expr)
{
    is(expr ? 1 : 2.0);

    if (expr)
        is(1);
    else
        is(2.0);
}
在条件运算符版本中,数字1会进行标准转换为双精度浮点数以匹配数字2.0的类型,这意味着即使在true/1的情况下也会调用is(double)重载。但if/else语句则不会触发这个转换,true/1的分支会调用is(int)。
同时在条件运算符中不能使用整体类型为void的表达式,而这种表达式在if/else语句中是有效的。
强调:先进行值选择再进行需要值的操作
if/else语句首先强调分支,其次强调要执行的动作,而三目运算符则强调要执行的动作而非选择要使用的值。
在不同的情况下,根据程序员在编写代码时考虑这些因素的顺序,任何一种方式可能更能反映程序员的"自然"视角,并使代码更易于理解、验证和维护。在你已经开始“做某事”后发现自己可能使用一些值来执行它时,使用?/:是最不干扰的表达方式,可以保持你的编程“流”。

如果不使用三元运算符,直接调用该函数的等效方式将不得不使用中间变量和单独的条件语句。只要它们的作用域局限于函数内部,编译器应该能够优化它们。如果没有它们,你将不得不编写一页代码,这将是不可能阅读和调试的,尽管现代编译器可能会做得相当不错。 - thkala
@thkala:对于我过于简洁的“创建临时变量也很麻烦,但保证了与三元运算符类似的性能”的阐述,你说得非常好。编译器可能会消除至少一些if/else函数树,但是寄存器分配、深度限制等问题可能会变得复杂,而且很难知道它是否会一直存在。 - Tony Delroy
在这里我请求原谅。虽然你解释得没错,但条件运算符可以用在表达式中,而if()...else()可能无法在那里使用。我用这个技巧以一种不同的方式解决了Scott Meyer的More Effective C++中的第10项问题,我避免使用了auto_ptr<>。请看这里:http://siddhusingh.blogspot.com - siddhusingh
@TonyD:我认为你被代码误导了。尝试在编辑器中重新格式化代码。没有任何内存泄漏。 我所做的是:根据另一个变量的值初始化成员变量指针,如果不是则将其赋值为NULL。 如果为真,则首先将其赋值为NULL,然后使用逗号运算符调用new运算符。 当最后一个值分配给值时,如果使用逗号运算符。 在任何异常情况下,我都会使用catch(...)捕获它,清除任何已分配的内存,然后再从构造函数本身重新抛出它。 - siddhusingh
@TonyD: 请看下面的代码。我认为你对我采用的方法感到困惑。你提到的是正确的,但不是我写的上下文。下面的代码应该打印10。 #include <iostream> #include <string>using namespace std;int main(int argc, char **argv, char *arge[]) try { cout << "Hello World!" << endl; string str = "a"; int *p; int *a = (str != "") ? p = 0, new int(10) : 0; if (a == 0) { cout << " a = 0 " << endl; } else { cout << "a = " << *a << endl; } return 0; } catch(...) { cerr << "未捕获的异常..." << endl; } - siddhusingh
2
@siddhusingh:抱歉-现在注意到你的初始化列表嵌入了这些语句,因此任何指针都会被传递回数据成员。但是,如果new Image()抛出异常,则尽管未初始化,theAudioClip将被删除。您可以解决此问题:所有原始指针都应在第一个数据成员(即theName)之前进行初始化,因为构造函数可能会抛出异常,如果您在构造函数周围使用try/catch,则如下所示:: theName((theImage = theAudioClip = nullptr, name))。指针数据成员首先更好,但仍然脆弱。 - Tony Delroy

9
在我看来,三元运算符相较于普通的 if 语句仅有一个潜在优点,就是它们可以用于初始化,这对于 const 特别有用:
例如:
const int foo = (a > b ? b : a - 10);

使用if/else块实现这个功能是不可能的,除非使用函数调用。如果您恰好有很多像这样的const情况,通过正确初始化const会比使用if/else进行赋值获得小小的收益。请度量它! 尽管如此,可能甚至不可测量。我倾向于这样做的原因是,通过标记为const,编译器知道当我稍后执行某些可能会意外更改我认为已经固定的内容的操作时。
实际上,我的意思是三元运算符对于const正确性非常重要,而const正确性是一个很好的习惯:
1. 通过让编译器帮助您发现您所犯的错误,这可以节省大量时间 2. 这可能让编译器应用其他优化

9

好的...

我用GCC进行了几次测试,测试这个函数调用:

add(argc, (argc > 1)?(argv[1][0] > 5)?50:10:1, (argc > 2)?(argv[2][0] > 5)?50:10:1, (argc > 3)?(argv[3][0] > 5)?50:10:1);

使用gcc -O3编译后的汇编代码有35条指令。
使用if/else和中间变量的等效代码有36条。使用嵌套if/else,利用3>2>1的事实,我得到了44条。我甚至没有尝试将其扩展为单独的函数调用。
现在,我没有进行任何性能分析,也没有对生成的汇编代码进行质量检查,但是对于像这样没有循环等简单的东西,我认为越短越好。
看来三元运算符确实有一定的价值 :-)
当然,只有在代码速度绝对关键的情况下才是如此。嵌套的if/else语句比(c1)?(c2)?(c3)?(c4)?:1:2:3:4之类的东西更容易阅读。而且,将巨大的表达式作为函数参数是不好玩的。
还要记住,嵌套的三元表达式使重构代码或通过在条件中放置一堆方便的printf()进行调试变得更加困难。

2
顺便说一下,有趣的是,当我试图在3>2>1代码中比编译器更聪明时,它以我预料之外的方式失败了。结论:永远不要试图超越编译器! - thkala

5
如果你担心性能问题,那么我会非常惊讶两者之间是否有任何不同。从外观和感觉的角度来看,这主要取决于个人喜好。如果条件很短,而真/假部分也很短,则三元运算符就可以了,但是任何更长的内容都更适合使用if/else语句(在我看来)。

4
你认为两者之间必须有区别,实际上有很多语言放弃了“if-else”语句,转而使用“if-else”表达式(在这种情况下,它们甚至可能没有三目运算符,因为它已不再需要)。
想象一下:
x = if (t) a else b

无论如何,三元运算符是一些编程语言(如C、C#、C++、Java等)中的一个表达式,这些语言中没有“if-else”表达式,因此它在那里发挥着独特的作用。

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