C++对const引用参数的依赖不会改变

8
请考虑以下代码:
void func1(const int &i);
void func2(int i);

void f()
{
  int a=12;
  func1(a);
  func2(a);
}

使用带有-O3的g ++ 4.6编译,我可以看到编译器在函数调用之间重新读取“a”的值。将a的定义更改为“const int”,编译器不会这样做,而是直接加载立即值“12”到edi中。如果a不是常量,但我更改func1的签名为按值接受,则情况也是如此。
虽然这不是代码生成中的错误,但仍然是奇怪的行为。由于func1承诺不更改a,为什么编译器的代码会根据a是否为const而改变呢?
编辑:一些怀疑者声称我可能读错了代码。以上代码使用-S编译产生以下结果:
_Z1fv:
.LFB0:
        .cfi_startproc
        subq    $24, %rsp
        .cfi_def_cfa_offset 32
        leaq    12(%rsp), %rdi
        movl    $12, 12(%rsp)                                                          
        call    _Z5func1RKi
        movl    12(%rsp), %edi     <-- Rereading a
        call    _Z5func2i
        addq    $24, %rsp
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc                                                           

将a改成const,会产生以下结果:

_Z1fv:
.LFB0:
        .cfi_startproc
        subq    $24, %rsp
        .cfi_def_cfa_offset 32
        leaq    12(%rsp), %rdi
        movl    $12, 12(%rsp)
        call    _Z5func1RKi
        movl    $12, %edi          <-- Use immediate value
        call    _Z5func2i
        addq    $24, %rsp
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc

func1 可以使用 const_cast 去除常量性并写入 i,而且这实际上是合法的(即不是未定义行为),因为底层对象 a 没有被声明为 const - T.C.
func1如何知道a是否被声明为const? - Shachar Shemesh
它无法这样做;如果使用实际上被声明为const的内容调用它,它只会导致未定义的行为。 - T.C.
func1本身并不知道任何事情。它只是一个文本文件中的一堆字符。当调用函数时,编译器知道a是什么。 - Arne Mertz
你应该使用-S编译并发布汇编代码,而不是告诉我们你对代码的解释,让我们在黑暗中摸索。这尤其重要,因为你的描述令人怀疑...调用func1(const int&)意味着需要将2压入堆栈并传递地址,而func2(a)则意味着需要实际“读取”2并将其作为参数传递,所以当你说“重新阅读...之间”的时候,也许它的意义是第一次读取。 - Tony Delroy
感谢您抽出时间发布代码(并称呼我为怀疑论者;-P)。正如我所说,movl 12(%rsp), %edi并不是“重新读取”该值...这是客户端代码中的第一次且唯一一次读取,在计算堆栈地址(leaq)和写入(movl $12,...)之后。微不足道的区别,但我想确保您没有看到两个读取,那将会很奇怪。尽管如此,为什么只有第二个movl $12, %edi是一个好问题...(并已得到很好的回答)+1。干杯。 - Tony Delroy
2个回答

6
在C++中,const实际上只是逻辑上的常量而不是物理上的常量。func1可以进行const_cast并修改iconst就像枪的安全装置 - 你仍然可能会自己踢到自己的脚,但不会发生意外情况。
正如T.C.和juanchopanza在评论中指出的那样,去除对象的const属性并进行修改是未定义行为。引用自“Notes”这里

即使const_cast可以从任何指针或引用中去除constnessvolatility,但是使用所得到的指针或引用向被声明为const的对象写入或访问被声明为volatile的对象都会导致未定义行为。


但是如果我将"a"声明为const,编译器也会允许自己优化掉第二个内存读取的操作,这一点同样适用。 - Shachar Shemesh
@ShacharShemesh 因为在这种情况下,修改会导致未定义的行为,这意味着编译器可以自由地执行任何操作。 - T.C.
const 对象强制转换为非 const 并进行修改是不被允许的,无论是通过指针还是引用进行操作都是如此。 - juanchopanza
换句话说,你的最后一句话非常误导。 - juanchopanza
@juanchopanza 这意味着答案是不正确的。如果强制转换去除const属性是未定义的,那么当func1这样做时也是未定义的。编译器可以合法地假设func1不会改变a。 - Shachar Shemesh
@ShacharShemesh 嗯,我会说这是不正确的,但如果作者仔细阅读他们包含的链接,那么很容易就可以修复它 :-) 尽管请注意,如果函数转换了不真正“const”的某些东西的常数,则它不是UB。重要的是所引用/指向的对象的恒定性(类似于挥发性)。 - juanchopanza

3
总结答案,我认为这是最好的解释:
可以将一个常量引用作为非常量变量的引用,然后去掉const。因此,在第一种情况下,编译器不能假设func1不会改变a。
如果对一个被声明为const的变量去掉const限定符,结果未定义。在第二种情况下,编译器可能假设func1不会去掉const限定符。如果func1确实去掉了const限定符,那么func2将会接收到“错误”的值,但这只是未定义行为的一个后果。

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