C++ g++;带有悬空引用的不完整警告行为

C++ g++;带有悬空引用的不完整警告行为,c++,compiler-construction,g++,warnings,C++,Compiler Construction,G++,Warnings,我们看到两个例子都有悬垂的参考: 示例A: int& getref() { int a; return a; } getref(): push rbp mov rbp, rsp mov eax, 0 pop rbp ret 例B: int& getref() { int a; int&b = a; re

我们看到两个例子都有悬垂的参考: 示例A:

int& getref()
{
        int a;
        return a;
}
    getref():
    push    rbp
    mov     rbp, rsp
    mov     eax, 0
    pop     rbp
    ret
    
例B:

int& getref()
{
        int a;
        int&b = a;
        return b;
}
    getref():
    push    rbp
    mov     rbp, rsp
    lea     rax, [rbp-12]
    mov     QWORD PTR [rbp-8], rax
    mov     rax, QWORD PTR [rbp-8]
    pop     rbp
    ret
我们使用相同的主要功能将它们称为:

int main()
{
        cout << getref() << '\n';
        cout << "- reached end" << std::endl;
        return 0;
}
例B:

int& getref()
{
        int a;
        int&b = a;
        return b;
}
    getref():
    push    rbp
    mov     rbp, rsp
    lea     rax, [rbp-12]
    mov     QWORD PTR [rbp-8], rax
    mov     rax, QWORD PTR [rbp-8]
    pop     rbp
    ret
现在我的程序集有点生锈了,但在示例B中,似乎涉及到更多的堆栈内存,从理论上讲,这将产生更多的内存被危险引用的可能性,因此更易于检测,因为它不太可能受到优化。令我惊讶的是,编译器在只处理寄存器时检测到了悬空引用,而在涉及实际内存时却没有检测到,就像在示例B的汇编中一样

也许这里的任何人都能理解为什么B比A更难被发现

下面是示例B的完整组件,以防感兴趣:

getref():
        push    rbp
        mov     rbp, rsp
        lea     rax, [rbp-12]
        mov     QWORD PTR [rbp-8], rax
        mov     rax, QWORD PTR [rbp-8]
        pop     rbp
        ret
.LC0:
        .string "- reached end"
main:
        push    rbp
        mov     rbp, rsp
        call    getref()
        mov     eax, DWORD PTR [rax]
        mov     esi, eax
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        mov     esi, 10
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char)
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        mov     esi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))
        mov     eax, 0
        pop     rbp
        ret
__static_initialization_and_destruction_0(int, int):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], edi
        mov     DWORD PTR [rbp-8], esi
        cmp     DWORD PTR [rbp-4], 1
        jne     .L7
        cmp     DWORD PTR [rbp-8], 65535
        jne     .L7
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        call    __cxa_atexit
.L7:
        nop
        leave
        ret
_GLOBAL__sub_I_getref():
        push    rbp
        mov     rbp, rsp
        mov     esi, 65535
        mov     edi, 1
        call    __static_initialization_and_destruction_0(int, int)
        pop     rbp
        ret
getref():
推动rbp
mov rbp,rsp
lea rax,[rbp-12]
mov QWORD PTR[rbp-8],rax
mov rax,QWORD PTR[rbp-8]
流行限制性商业惯例
ret
.LC0:
.string“-已到达末尾”
主要内容:
推动rbp
mov rbp,rsp
调用getref()
mov eax,DWORD PTR[rax]
电影esi,eax
移动edi,偏移平面:_ZSt4cout
调用std::basic_ostream::operator
B。。。返回值的正确值

由于程序的行为是未定义的,因此任何行为都不应是意外的

此外,返回的任何值都不“正确”。这简直是垃圾

我很惊讶编译器检测到了悬空引用,而只有

编译器几乎不可能通过无效引用检测所有间接寻址。因此,一定存在编译器无法检测到的复杂程度。你在这个比喻性的“点”的不同侧面找到了两个例子。不清楚为什么你会感到惊讶

也许这里的任何人都能理解为什么B比A更难被发现

它更复杂。返回的引用不是直接从本地对象初始化的,而是另一个在理论上可能引用非本地对象的引用。直到分析了中间引用的初始值设定项,我们才发现它确实引用了一个局部对象


< C++的透视图完全被“UB”所回答。也许您可能想知道为什么生成的汇编程序的行为不同

这仅仅是因为从案例A生成的程序返回一个内存值0,即null。地址0处的内存当然没有映射为进程可以访问的内容,因此当程序试图读取该内存时,操作系统会发出SEGFAULT信号

另一方面,B程序返回指向堆栈的指针。由于该地址已映射到进程,因此操作系统没有理由发出信号



值得一提的是,当启用优化时,GCC确实检测到错误,并为不同的功能生成相同的程序集。

“…读取悬挂参考时出现预期的SEGFULT…”您为什么会期望这样?这基本上是一个QOI问题。请注意,clang是这样做的。标准中到处都是“无需诊断”的实例。如果不是必需的,那么您是否能得到诊断就不可能了。@Meph说得很清楚:我不认为GCC在调试生成时没有提供与优化后相同的警告是愚蠢的。这并没有改变GCC的运作方式这一事实。据我记忆中的阅读,这可能是由于GCC的一些优化发生在语义/代码分析之前——因此我猜测在
-O2
处,引用被折叠,并且分析部分捕捉到这是一个本地引用。我对不比较未优化的程序集的评论与这一点没有直接关系。未优化的代码生成几乎总是对您编写的代码的1-1转录。因此,比较两段不同源代码的未优化汇编通常意义不大。使用函数本地引用对象的示例会生成不同的程序集,因为在未优化时,GCC会像分配指针一样在堆栈上为引用分配存储空间。这通常不是一个有用的比较,因此不值得尝试从中得出结论。这就是我所说的“不清楚为什么你会感到惊讶。”并不是说OP已经尝试过了,而是gcc在clang和msvc发出警告时没有发出警告,这至少有点令人惊讶。@cigien我想期望是非常主观的。我已经看到了足够多的例子,其中一个编译器警告,另一个编译器没有警告,它一点也不让我吃惊。也许遇到这个例子会让程序员期待它。是的,这就是我的观点。我同意有一点经验并不奇怪,但措辞表明OP应该已经知道这一点。比如“我知道这可能会令人惊讶,但过一段时间你就会习惯于UB诊断的不同QOI”怎么样?Meph接受答案并不能解决问题。如果您更喜欢稍后的答案,您可以自由更改您接受的答案。@cigien“当clang和msvc发出警告时,gcc不会发出警告,这一事实至少有点令人惊讶”它确实发出了警告,但您必须做到这一点。IIRC这与GCC在进行代码分析之前执行优化的方式有关,因此可能在
-O2
之后引用会崩溃,从而允许检测到这种情况。