我正在制作自己的C编译器,以尽可能了解C的详细信息。现在我正在尝试准确理解volatile
对象的工作原理。
令人困惑的是,代码中的每个读取访问都必须严格执行(C11,6.7.3p7):
具有易失性限定类型的对象可能以实现未知的方式进行修改或具有其他未知的副作用。因此,任何引用这种对象的表达式都应严格按照抽象机器的规则进行评估,如5.1.2.3所述。此外,在每个序列点上,对象中最后存储的值应与抽象机器所规定的值相同,除非被先前提到的未知因素修改。134)构成具有易失性限定类型的对象的访问是由实现定义的。
例如:在a = volatile_var - volatile_var;
中,易失变量必须被读取两次,因此编译器无法优化为a = 0;
同时,序列点之间的评估顺序是不确定的(C11,6.5p3):
运算符和操作数的分组由语法指示。除非另有规定,否则子表达式的副作用和值计算是无序的。例如:在b = (c + d) - (e + f)中,加法的计算顺序是未指定的,因为它们是无序的。
但是,在评估创建副作用的无序对象时(例如使用
volatile
),行为是未定义的(C11,6.5p2):如果标量对象上的副作用与同一标量对象上的不同副作用或使用相同标量对象的值计算无序,则行为是未定义的。如果表达式的多个可允许的子表达式排序,则如果任何这样的无序副作用发生,则行为是未定义的。
这是否意味着像
x = volatile_var - (volatile_var + volatile_var)
这样的表达式是未定义的?我的编译器是否应该抛出警告?
我尝试了解CLANG和GCC的行为。两者都没有抛出错误或警告。输出的汇编代码显示变量在执行顺序上并未被按顺序读取,而是从左到右,如下所示的risc-v汇编代码:
const int volatile thingy = 0;
int main()
{
int new_thing = thingy - (thingy + thingy);
return new_thing;
}
main:
lui a4,%hi(thingy)
lw a0,%lo(thingy)(a4)
lw a5,%lo(thingy)(a4)
lw a4,%lo(thingy)(a4)
add a5,a5,a4
sub a0,a0,a5
ret
编辑:我不是在问“编译器为什么会接受它”,我在问“如果我们严格遵循C11标准,这是否属于未定义行为”。标准似乎表明这是未定义行为,但我需要更多关于它的精确信息才能正确解释。
int x = thingy + (thingy=42);
可能会导致未定义行为,而int x=thingy - (thingy + thingy)
则不会。 - tstanisl