27

这是一个简单的 C++ 片段:

int x1 = 10, x2 = 20, y1 = 132, y2 = 12, minx, miny, maxx, maxy;
x1 <= x2 ? minx = x1, maxx = x2 : minx = x2, maxx = x1;
y1 <= y2 ? miny = y1, maxy = y2 : miny = y2, maxy = y1;
cout << "minx=" << minx << "\n";
cout << "maxx=" << maxx << "\n";
cout << "miny=" << miny << "\n";
cout <<" maxy=" << maxy << "\n";

我认为结果应该是:

minx=10
maxx=20
miny=12
maxy=132

但实际上结果是:

minx=10
maxx=10
miny=12
maxy=132

有人可以解释为什么maxx不是20吗?谢谢。

4

6 回答 6

40

由于运算符优先级,表达式解析如下:

(x1<=x2 ? minx=x1,maxx=x2 : minx=x2), maxx=x1;

你可以解决这个问题:

(x1<=x2) ? (minx=x1,maxx=x2) : (minx=x2, maxx=x1);

实际上你不需要前两对括号。还要检查这个问题

于 2013-05-18T21:38:02.803 回答
25

条件运算符的优先级大于逗号运算符,所以

x1<=x2 ? minx=x1,maxx=x2 : minx=x2,maxx=x1;

括号内为

(x1<=x2 ? minx=x1,maxx=x2 : minx=x2),maxx=x1;

因此,无论条件如何,都会完成最后一次分配。

要修复它,您可以

  • 使用括号:

    x1 <= x2 ? (minx = x1, maxx = x2) : (minx = x2, maxx = x1);
    

    (你不需要分支中的括号true,但 IMO 最好也有它们)。

  • 使用两个条件:

    minx = x1 <= x2 ? x1 : x2;
    maxx = x1 <= x2 ? x2 : x1;
    
  • 使用if

    if (x1 <= x2) {
        minx = x1;
        maxx = x2;
    } else {
        minx = x2;
        maxx = x1;
    }
    

无论是否经过优化编译,if版本和带逗号的括号中的单个条件都会在 gcc (4.7.2) 和 clang (3.2) 下生成相同的程序集,因此也可以合理地期望其他编译器也能做到这一点。具有两个条件的版本产生不同的程序集,但经过优化,这两个编译器cmp也只发出一条指令。

在我看来,if版本是最容易验证正确性的,所以更可取。

于 2013-05-18T21:38:31.390 回答
5

虽然其他人已经解释了问题的原因是什么,但我认为“更好”的解决方案应该是用 if 编写条件:

int x1 = 10, x2=20, y1=132, y2=12, minx, miny, maxx, maxy;
if (x1<=x2) 
{ 
   minx=x1;
   maxx=x2;
}
else
{
   minx=x2; 
   maxx=x1;
}
if (y1<=y2)
{
    miny=y1;
    maxy=y2;
} 
else 
{
    miny=y2;
    maxy=y1;
}

是的,它长了几行,但它也更容易阅读和清楚到底发生了什么(如果你需要在调试器中单步执行它,你可以很容易地看到它的走向)。

任何现代编译器都应该能够将其中任何一个转换为相当有效的条件赋值,可以很好地避免分支(因此是“错误的分支预测”)。

我准备了一个小测试,我用它编译

g++ -O2 -fno-inline -S -Wall ifs.cpp

这是源代码(我必须将其设为参数以确保编译器不只是直接计算正确的值而只是做mov $12,%rdx,而是实际上做了比较并决定更大):

void mine(int x1, int x2, int y1, int y2)
{
    int minx, miny, maxx, maxy;
    if (x1<=x2) 
    { 
    minx=x1;
    maxx=x2;
    }
    else
    {
    minx=x2; 
    maxx=x1;
    }
    if (y1<=y2)
    {
    miny=y1;
    maxy=y2;
    } 
    else 
    {
    miny=y2;
    maxy=y1;
    }

    cout<<"minx="<<minx<<"\n";
    cout<<"maxx="<<maxx<<"\n";
    cout<<"miny="<<miny<<"\n";
    cout<<"maxy="<<maxy<<"\n";
}

void original(int x1, int x2, int y1, int y2)
{
    int minx, miny, maxx, maxy;
    x1<=x2 ? (minx=x1,maxx=x2) : (minx=x2,maxx=x1);
    y1<=y2 ? (miny=y1,maxy=y2) : (miny=y2,maxy=y1);
    cout<<"minx="<<minx<<"\n";
    cout<<"maxx="<<maxx<<"\n";
    cout<<"miny="<<miny<<"\n";
    cout<<"maxy="<<maxy<<"\n";
}

void romano(int x1, int x2, int y1, int y2)
{
    int  minx, miny, maxx, maxy;

    minx = ((x1 <= x2) ? x1 : x2);
    maxx = ((x1 <= x2) ? x2 : x1);
    miny = ((y1 <= y2) ? y1 : y2);
    maxy = ((y1 <= y2) ? y2 : y1);
    cout<<"minx="<<minx<<"\n";
    cout<<"maxx="<<maxx<<"\n";
    cout<<"miny="<<miny<<"\n";
    cout<<"maxy="<<maxy<<"\n";
}

int main()
{
    int x1=10, x2=20, y1=132, y2=12;
    mine(x1, x2, y1, y2);
    original(x1, x2, y1, y2);
    romano(x1, x2, y1, y2);
    return 0;
}

生成的代码如下所示:

_Z4mineiiii:
.LFB966:
    .cfi_startproc
    movq    %rbx, -32(%rsp)
    movq    %rbp, -24(%rsp)
    movl    %ecx, %ebx
    movq    %r12, -16(%rsp)
    movq    %r13, -8(%rsp)
    movl    %esi, %r12d
    subq    $40, %rsp
    movl    %edi, %r13d
    cmpl    %esi, %edi
    movl    %edx, %ebp
    cmovg   %edi, %r12d
    cmovg   %esi, %r13d
    movl    $_ZSt4cout, %edi
    cmpl    %ecx, %edx
    movl    $.LC0, %esi
    cmovg   %edx, %ebx
    cmovg   %ecx, %ebp
        .... removed actual printout code that is quite long and unwieldy... 
_Z8originaliiii:
    movq    %rbx, -32(%rsp)
    movq    %rbp, -24(%rsp)
    movl    %ecx, %ebx
    movq    %r12, -16(%rsp)
    movq    %r13, -8(%rsp)
    movl    %esi, %r12d
    subq    $40, %rsp
    movl    %edi, %r13d
    cmpl    %esi, %edi
    movl    %edx, %ebp
    cmovg   %edi, %r12d
    cmovg   %esi, %r13d
movl    $_ZSt4cout, %edi
cmpl    %ecx, %edx
movl    $.LC0, %esi
cmovg   %edx, %ebx
cmovg   %ecx, %ebp
        ... print code goes here ... 
_Z6romanoiiii:
    movq    %rbx, -32(%rsp)
    movq    %rbp, -24(%rsp)
    movl    %edx, %ebx
    movq    %r12, -16(%rsp)
    movq    %r13, -8(%rsp)
    movl    %edi, %r12d
    subq    $40, %rsp
    movl    %esi, %r13d
    cmpl    %esi, %edi
    movl    %ecx, %ebp
    cmovle  %edi, %r13d
    cmovle  %esi, %r12d
movl    $_ZSt4cout, %edi
cmpl    %ecx, %edx
movl    $.LC0, %esi
cmovle  %edx, %ebp
cmovle  %ecx, %ebx
        ... printout code here.... 

如您所见,mineandoriginal是相同的,并且romano使用稍微不同的寄存器和不同形式的cmov,但除此之外它们在相同数量的指令中执行相同的操作。

于 2013-05-18T21:51:16.750 回答
4

关于操作优先级和代码生成的有趣问题。

好的,,操作的优先级很低(最低,参见参考表)。由于这个事实,您的代码与以下几行相同:

((x1<=x2) ? minx=x1,maxx=x2 : minx=x2),maxx=x1;
((y1<=y2) ? miny=y1,maxy=y2 : miny=y2),maxy=y1;

实际上只有 C/C++ 语法首先防止,相同的行为。

C/C++ 操作优先级中另一个真正危险的地方是按位操作和比较。考虑以下片段:

int a = 2;
int b = (a == 2|1); // Looks like check for expression result? Nope, results 1!

展望未来,我建议以这种方式重写您的片段,以保持效率和可读性之间的平衡:

minx = ((x1 <= x2) ? x1 : x2);
maxx = ((x1 <= x2) ? x2 : x1);
miny = ((y1 <= y2) ? y1 : y2);
maxy = ((y1 <= y2) ? y2 : y1);

关于这段代码最有趣的事实是,由于 CPU 指令集中的条件位标志,这种风格可能会为 ARM 等某些体系结构生成最有效的代码(条件复制不会花费更多,将编译器指向正确的代码片段)。

于 2013-05-18T21:45:54.633 回答
1

由于运算符优先级:

(x1<=x2 ? minx=x1,maxx=x2 : minx=x2),maxx=x1

您可以使用以下方法修复它:

int x1=10, x2=20, y1=132, y2=12, minx, miny, maxx, maxy;
x1<=x2 ? (minx=x1,maxx=x2) : (minx=x2,maxx=x1);
y1<=y2 ? (miny=y1,maxy=y2) : (miny=y2,maxy=y1);
cout<<"minx="<<minx<<"\n";
cout<<"maxx="<<maxx<<"\n";
cout<<"miny="<<miny<<"\n";
cout<<"maxy="<<maxy<<"\n";
于 2013-05-18T21:42:10.807 回答
1

在 C++11 中,您可以使用std::tiestd::make_pair使其一目了然(TM)

#include <tuple>
#include <utility>
#include <iostream>

using namespace std;

int main()
{
    int x1 = 10, x2=20, y1=132, y2=12, minx, miny, maxx, maxy;

    tie(minx, maxx) = (x1 <= x2)? make_pair(x1, x2) : make_pair(x2, x1);
    tie(miny, maxy) = (y1 <= y2)? make_pair(y1, y2) : make_pair(y2, y1);

    cout<<"minx="<<minx<<"\n";
    cout<<"maxx="<<maxx<<"\n";
    cout<<"miny="<<miny<<"\n";
    cout<<"maxy="<<maxy<<"\n";
}

在线输出

这在语义上等同于发布的所有其他解决方案,并且任何体面的优化编译器都没有任何开销。它在语法上更好,因为它有

  • 最少的代码重复,
  • 4 个赋值给变量都在赋值的左侧,并且
  • 4 个赋值变量都在右边。

作为一种可以概括为查找指向序列的最小和最大元素的指针的细微变化,您可以使用std::minmax_element原始数组具有非成员函数begin()end()(两个 C++11 功能)的事实

#include <algorithm>
#include <tuple>
#include <iostream>

using namespace std;

int main()
{
    int x[] = { 10, 20 }, y[] = { 132, 12 }, *minx, *miny, *maxx, *maxy;

    tie(minx, maxx) = minmax_element(begin(x), end(x));
    tie(miny, maxy) = minmax_element(begin(y), end(y));

    cout<<"minx="<<*minx<<"\n";
    cout<<"maxx="<<*maxx<<"\n";
    cout<<"miny="<<*miny<<"\n";
    cout<<"maxy="<<*maxy<<"\n";
}

在线输出

于 2013-05-19T09:39:32.923 回答