代码之家  ›  专栏  ›  技术社区  ›  Chen Li

从编译器的角度来看,数组的引用是如何处理的,为什么不允许传递值(而不是衰减)?

  •  4
  • Chen Li  · 技术社区  · 7 年前

    正如我们所知道的,在C++中,我们可以把数组的引用传递为一个类似的参数。 f(int (&[N]) .是的,它是由ISO标准保证的语法,但是我很好奇编译器在这里是如何工作的。我找到这个了 thread 但不幸的是,这并不能回答我的问题——编译器是如何实现这种语法的?

    然后我写了一个演示,希望能从汇编语言中看到一些东西:

    void foo_p(int*arr) {}
    void foo_r(int(&arr)[3]) {}
    template<int length>
    void foo_t(int(&arr)[length]) {}
    int main(int argc, char** argv)
    {
        int arr[] = {1, 2, 3};
        foo_p(arr);
        foo_r(arr);
        foo_t(arr);
       return 0;
    }
    

    原来,我 猜测 它仍然会衰减到指针,但会通过寄存器隐式传递长度,然后返回到函数体中的数组中。但是汇编代码告诉我这不是真的

    void foo_t<3>(int (&) [3]):
      push rbp #4.31
      mov rbp, rsp #4.31
      sub rsp, 16 #4.31
      mov QWORD PTR [-16+rbp], rdi #4.31
      leave #4.32
      ret #4.32
    
    foo_p(int*):
      push rbp #1.21
      mov rbp, rsp #1.21
      sub rsp, 16 #1.21
      mov QWORD PTR [-16+rbp], rdi #1.21
      leave #1.22
      ret #1.22
    
    foo_r(int (&) [3]):
      push rbp #2.26
      mov rbp, rsp #2.26
      sub rsp, 16 #2.26
      mov QWORD PTR [-16+rbp], rdi #2.26
      leave #2.27
      ret #2.27
    
    main:
      push rbp #6.1
      mov rbp, rsp #6.1
      sub rsp, 32 #6.1
      mov DWORD PTR [-16+rbp], edi #6.1
      mov QWORD PTR [-8+rbp], rsi #6.1
      lea rax, QWORD PTR [-32+rbp] #7.15
      mov DWORD PTR [rax], 1 #7.15
      lea rax, QWORD PTR [-32+rbp] #7.15
      add rax, 4 #7.15
      mov DWORD PTR [rax], 2 #7.15
      lea rax, QWORD PTR [-32+rbp] #7.15
      add rax, 8 #7.15
      mov DWORD PTR [rax], 3 #7.15
      lea rax, QWORD PTR [-32+rbp] #8.5
      mov rdi, rax #8.5
      call foo_p(int*) #8.5
      lea rax, QWORD PTR [-32+rbp] #9.5
      mov rdi, rax #9.5
      call foo_r(int (&) [3]) #9.5
      lea rax, QWORD PTR [-32+rbp] #10.5
      mov rdi, rax #10.5
      call void foo_t<3>(int (&) [3]) #10.5
      mov eax, 0 #11.11
      leave #11.11
      ret #11.11
    

    live demo

    我承认我不熟悉汇编语言,但很明显,这三个函数的汇编代码是相同的!所以,在汇编代码之前必须发生一些事情。无论如何,与数组不同,指针对长度一无所知,对吗?

    问题:

    1. 编译器在这里是如何工作的?
    2. 既然标准允许通过引用传递一个数组,这是否意味着实现这个数组很简单?如果是,为什么不允许按值传递?

    对于Q2,我的猜测是前C和C代码的复杂性。毕竟, int[] 等于 int* 函数参数一直是一个传统。也许一百年后,它会被否决?

    3 回复  |  直到 7 年前
        1
  •  5
  •   Peter Cordes    7 年前

    int foo(int arr[static 3]) static syntax cmov if


    What kind of C11 data type is an array according to the AMD64 ABI

    const int *arr


    gcc -O3 -fno-inline-functions -fno-inline-functions-called-once -fno-inline-small-functions -O0 inline

    -fno-inline-small-functions __attribute__((noinline))

    arr[] arr[4]

    __attribute__((noinline, noclone)) 
    void foo_p(int*arr) {(void)arr;}
    void foo_r(int(&arr)[3]) {arr[4] = 41;}
    
    template<int length>
    void foo_t(int(&arr)[length]) {arr[4] = 42;}
    
    void usearg(int*); // stop main from optimizing away arr[] if foo_... inline
    
    int main()
    {
        int arr[] = {1, 2, 3};
        foo_p(arr);
        foo_r(arr);
        foo_t(arr);
        usearg(arr);
       return 0;
    }
    

    gcc7.3 -O3 -Wall -Wextra without function inlining, on Godbolt foo_r

    <source>: In function 'int main()':
    <source>:14:10: warning: array subscript is above array bounds [-Warray-bounds]
         foo_t(arr);
         ~~~~~^~~~~
    

    void foo_t<3>(int (&) [3]) [clone .isra.0]:
        mov     DWORD PTR [rdi], 42       # *ISRA.3_4(D),
        ret
    foo_p(int*):
        rep ret
    foo_r(int (&) [3]):
        mov     DWORD PTR [rdi+16], 41    # *arr_2(D),
        ret
    
    main:
        sub     rsp, 24             # reserve space for the array and align the stack for calls
        movabs  rax, 8589934593     # this is 0x200000001: the first 2 elems
        lea     rdi, [rsp+4]
        mov     QWORD PTR [rsp+4], rax    # MEM[(int *)&arr],  first 2 elements
        mov     DWORD PTR [rsp+12], 3     # MEM[(int *)&arr + 8B],  3rd element as an imm32
        call    foo_r(int (&) [3])
        lea     rdi, [rsp+20]
        call    void foo_t<3>(int (&) [3]) [clone .isra.0]    #
        lea     rdi, [rsp+4]      # tmp97,
        call    usearg(int*)     #
        xor     eax, eax  #
        add     rsp, 24   #,
        ret
    

    foo_p() noinline noclone *arr=0; main rdi

    clone .isra.0 lea rdi, [rsp+20] [rdi] __attribute__((noclone))

    disp8 void foo_clone(int *p) { *p = 42; }


    mov rdi, rsp lea rdi, [rsp+4] &arr[-1] mov dword ptr [rdi+20], 42

    rsp

        3
  •  2
  •   Tanz87    7 年前