C++ 为什么f(i=-1,i=-1)是未定义的行为?

C++ 为什么f(i=-1,i=-1)是未定义的行为?,c++,language-lawyer,undefined-behavior,C++,Language Lawyer,Undefined Behavior,我在读一本书,他们举了一个让我困惑的例子 1) 如果标量对象上的一个副作用相对于同一标量对象上的另一个副作用未排序,则该行为未定义 // snip f(i = -1, i = -1); // undefined behavior 在这个上下文中,i是一个标量对象,这显然意味着 算术类型(3.9.1)、枚举类型、指针类型、指向成员类型的指针(3.9.2)、std::nullptr__t以及这些类型的cv限定版本(3.9.3)统称为标量类型 在那种情况下,我看不出这句话有多么含糊不清。在我看来,无

我在读一本书,他们举了一个让我困惑的例子

1) 如果标量对象上的一个副作用相对于同一标量对象上的另一个副作用未排序,则该行为未定义

// snip
f(i = -1, i = -1); // undefined behavior
在这个上下文中,
i
是一个标量对象,这显然意味着

算术类型(3.9.1)、枚举类型、指针类型、指向成员类型的指针(3.9.2)、std::nullptr__t以及这些类型的cv限定版本(3.9.3)统称为标量类型

在那种情况下,我看不出这句话有多么含糊不清。在我看来,无论第一个或第二个参数是先求值的,
i
最终都是
-1
,而且这两个参数也是
-1

有人能澄清一下吗


更新 我真的很感谢大家的讨论。到目前为止,我非常喜欢它,因为它暴露了定义这个语句的陷阱和复杂性,尽管乍一看它是多么直截了当。指出使用参考文献时出现的一些问题,但我认为这与这个问题的未排序副作用方面是正交的


总结 由于这个问题引起了大量关注,我将总结主要观点/答案。首先,请允许我离题一点,指出“为什么”可能有密切相关但微妙不同的含义,即“为了什么原因”、“为了什么原因”和“为了什么目的”。我将根据“为什么”的含义对答案进行分组

什么原因 这里的主要答案来自于,提供了一个类似但不那么广泛的答案。保罗·德雷珀的答案归结为

它是未定义的行为,因为它没有定义行为是什么

在解释C++标准所说的内容方面,答案是非常好的。本文还讨论了UB的一些相关情况,如
f(++i,+++i)
f(i=1,i=-1)。在第一个相关案例中,不清楚第一个参数是否应该是
i+1
,第二个参数是否应该是
i+2
,反之亦然;在第二种情况下,不清楚函数调用后,
i
应该是1还是-1。这两种情况都属于UB,因为它们属于以下规则:

如果标量对象上的副作用相对于同一标量对象上的另一副作用未排序,则该行为未定义

因此,
f(i=-1,i=-1)
也是UB,因为它属于相同的规则,尽管程序员的意图(IMHO)是显而易见的和明确的

保罗·德雷珀在他的结论中也明确指出

是否可以定义为行为?对它被定义了吗?没有

这就引出了这样一个问题:“为什么
f(i=-1,i=-1)
被保留为未定义的行为?”

出于什么原因/目的 尽管在C++标准中有一些疏忽(粗心),但许多遗漏都是合理的,并且有特定的用途。虽然我知道其目的通常是“让编译器编写者的工作更轻松”或“更快的代码”,但我主要想知道是否有充分的理由将f(I=-1,I=-1)
保留为UB。

并提供主要答案,为UB提供理由。Harmic指出,优化编译器可能会将表面上的原子赋值操作分解为多条机器指令,并可能进一步交错这些指令以获得最佳速度。这可能会导致一些非常令人惊讶的结果:
i
在他的场景中以-2结束!因此,harmic演示了如果操作未排序,将同一个值多次赋给变量可能会产生不良影响

supercat提供了一个相关的解释,说明了试图让
f(i=-1,i=-1)
做它看起来应该做的事情的陷阱。他指出,在某些体系结构上,对同一内存地址的多个同时写入存在硬限制。如果我们处理的不是像
f(i=-1,i=-1)
那样琐碎的东西,编译器可能很难捕捉到这一点

还提供了一个与harmic非常相似的交错指令示例

尽管harmic、supercat和davidf的每一个例子都有些做作,但综合起来,它们仍然提供了一个明确的理由,为什么
f(i=-1,i=-1)
应该是未定义的行为

我接受了哈米奇的答案,因为它在解释为什么的所有含义方面做得最好,尽管保罗·德雷珀的答案更好地解释了“原因是什么”部分

其他答案

指出,如果我们考虑重载赋值运算符(而不是纯标量),那么我们也会遇到麻烦。

首先,“标量对象”是指一种类型,如“代码> int <代码>,<代码>浮点< /COD>,或指针(参见)。


第二,似乎更明显的是

f(++i, ++i);
会有未定义的行为。但是

f(i = -1, i = -1);
不太明显

一个稍微不同的例子:

int i;
f(i = 1, i = -1);
std::cout << i << "\n";
inti;
f(i=1,i=-1);

std::cout在这种情况下,大多数实现的结果都是相同的,这是偶然的;评估的顺序尚未确定。考虑<代码> f(i=1,i=2)< /代码>:这里,命令很重要。在您的示例中,这不重要的唯一原因是两个值都是
-1


如果表达式被指定为具有未定义行为的表达式,那么恶意兼容的编译器在计算
f(i=-1,i=-1)
并中止执行时可能会显示不适当的映像,并且仍然被认为是完全正确的。幸运的是,据我所知,没有编译器会这样做。

在我看来,关于函数参数表达式排序的唯一规则如下:

3) 调用函数时(无论函数是否为内联函数,以及是否为显式函数
void f(int l, int r) {
    return l < -1;
}
auto b = f(i = -1, i = -2);
if (b) {
    formatDisk();
}
f(i=-1, i=-1)
clear i
clear i
decr i
decr i
// config.h
#define VALUEA  1

// defaults.h
#define VALUEB  1

// prog.cpp
f(i = VALUEA, i = VALUEB);
 #define VALUEB 2
struct A {
    bool first;
    A () : first (false) {
    }
    const A & operator = (int i) {
        first = !first;
        return * this;
    }
};

void f (A a1, A a2) {
    // ...
}


// ...
A i;
f (i = -1, i = -1);   // the argument evaluated first has ax.first == true
uint8_t v;  // Global

void hey(uint8_t *p)
{
  moo(v=5, (*p)=6);
  zoo(v);
  zoo(v);
}
reg = 0xFF; // first instruction
reg |= 0xFF00; // second
reg |= 0xFF0000; // third
reg |= 0xFF000000; // fourth
i = reg; // last
reg = 0xFF;
reg |= 0xFF00;
reg |= 0xFF0000;
reg = 0xFF;
reg |= 0xFF000000;
i = reg; // writes 0xFF0000FF == -16776961
reg |= 0xFF00;
reg |= 0xFF0000;
reg |= 0xFF000000;
i = reg; // writes 0xFFFFFFFF == -1
void g(int a, int b, int c, int n) {
    int i;
    // hey, compiler has to prove Fermat's theorem now!
    f(i = 1, i = (ipow(a, n) + ipow(b, n) == ipow(c, n)));
}
f(i = -1, i = -1); // value of i is -1
f(i = -1, i = -2); // value of i is either -1 or -2, but not specified which one