unique_ptr到底是否能按值传递?

答案是否定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

#include <memory>

#include <iostream>

static void func(std::unique_ptr<int> a)

{}

int main()

{

auto a = std::make_unique<int>(1);

func(a);

return 0;

}

这段代码是不能编译的,因为uniqe_ptr没有拷贝构造函数。

1
2
3
4
5
6
7
8
9
10
11

ben@LUbuntu ~/t/unique_ptr> g++ -o test main.cpp -std=c++14

main.cpp: In function ‘int main()’:

main.cpp:10:11: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = int; _Dp = std::default_delete<int>]’

10 | func(a);

|

那为什么问这个问题?看下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

#include <memory>

#include <iostream>

static void func(std::unique_ptr<int> a)

{}

int main()

{

func(std::make_unique<int>(1));

return 0;

}

这个是可以编译的,执行也没问题。再看make_unique的原型。

1
2
3
4
5
6
7
8
9
10
11
12
13

template< class T, class... Args >

unique_ptr<T> make_unique( Args&&... args );

template< class T >

unique_ptr<T> make_unique( std::size_t size );

template< class T, class... Args >

/* unspecified */ make_unique( Args&&... args ) = delete;

可见make_unique的返回值是unique_ptr。那么前面那段代码就给人造成了unique_ptr似乎也可以按值传递的假象。到底是什么原因呢?后面我们结合汇编一起分析一下,编译器针对这样的场景做了什么优化。

make_unique的返回值直接作为函数参数的真相

直接通过objectdum -tCS test > test.dump命令对可执行文件进行反汇编。注意编译时,要指定-g -O0。这样反汇编时,信息更多一些。

由于STL扩展代码,外加C++编译器插入的一些初始化和退出清理代码,反汇编产生的文件行数较多,我们只需关注核心的main函数以及一些相关的函数即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

int main()

{

4008a8: a9be7bfd stp x29, x30, [sp, #-32]! // 将x29,x30存储于sp-32处,且sp = sp-32

4008ac: 910003fd mov x29, sp

func(std::make_unique<int>(1));

4008b0: 52800020 mov w0, #0x1 // #1

4008b4: b9001fe0 str w0, [sp, #28]

4008b8: 910073e0 add x0, sp, #0x1c

4008bc: 910043e1 add x1, sp, #0x10

4008c0: aa0103e8 mov x8, x1

4008c4: 94000027 bl 400960 <std::_MakeUniq<int>::__single_object std::make_unique<int, int>(int&&)>

4008c8: 910043e0 add x0, sp, #0x10

4008cc: 97fffff2 bl 400894 <func(std::unique_ptr<int, std::default_delete<int> >)>

4008d0: 910043e0 add x0, sp, #0x10

4008d4: 94000035 bl 4009a8 <std::unique_ptr<int, std::default_delete<int> >::~unique_ptr()>

return 0;

4008d8: 52800000 mov w0, #0x0 // #0

4008dc: a8c27bfd ldp x29, x30, [sp], #32

4008e0: d65f03c0 ret

...

///@}

调用make_unique前的准备

main函数的第一行指令,将栈指针(sp)移动到了栈底,并保存了x29和x30寄存器。寄存器含义可以参考附录。在调用make_unique前,栈内存如下图。

ARM函数调用约定

也叫AAPCS,Procedure Call Standard for the Arm Architecture。对于aarch64,简而言之就是:

  • 小于8个参数时,使用x0-x7寄存器

  • 超过8个参数,按顺序从右往左入栈(因为栈是先入后出的)

  • 返回地址在lr寄存器,返回值的值在x0寄存器

make_unique的参数传递

按照AAPCS的理解,make_unique的实现函数实际有两个入参:一个是uniuqe_ptr的构造函数参数1,另一个就是用来存放unique_ptr对象指针的地址,即x1。

这样就很好理解了,调用完make_unique,下一步就是使用make_unique的输出调用func了。也就是add x0, sp, #0x10z这一行。将保存了unique_ptr指针的地址赋予了func函数的唯一一个参数。

所以可见,func函数并不是传递unique_ptr的值,而是通过编译器生成了一个临时变量保存了make_unique返回的unique_ptr对象指针,并传递给了func。

按引用传递会怎么样?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

static void func(std::unique_ptr<int> &a)

{}

int main()

{

auto a = std::make_unique<int>(1);

func(a);

return 0;

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

int main()

{

4008a8: a9bd7bfd stp x29, x30, [sp, #-48]!

4008ac: 910003fd mov x29, sp

4008b0: f9000bf3 str x19, [sp, #16]

auto a = std::make_unique<int>(1);

4008b4: 52800020 mov w0, #0x1 // #1

4008b8: b9002fe0 str w0, [sp, #44]

4008bc: 9100b3e0 add x0, sp, #0x2c

4008c0: 910083e1 add x1, sp, #0x20

4008c4: aa0103e8 mov x8, x1

4008c8: 94000029 bl 40096c <std::_MakeUniq<int>::__single_object std::make_unique<int, int>(int&&)>

func(a);

4008cc: 910083e0 add x0, sp, #0x20

4008d0: 97fffff1 bl 400894 <func(std::unique_ptr<int, std::default_delete<int> >&)>

...

///@}

如出一辙,对make_unique的调用是一样的,只不过这里多存了一个局部变量a,所以多花费了几个字节的栈空间。而前文的临时变量存在寄存器即可。

传右值引用呢?

1
2
3
4
5
6
7
8
9
10
11
12
13

4008b8: 910073e0 add x0, sp, #0x1c

4008bc: 910043e1 add x1, sp, #0x10

4008c0: aa0103e8 mov x8, x1

4008c4: 94000027 bl 400960 <std::_MakeUniq<int>::__single_object std::make_unique<int, int>(int&&)>

4008c8: 910043e0 add x0, sp, #0x10

4008cc: 97fffff2 bl 400894 <func(std::unique_ptr<int, std::default_delete<int> >&&)>

可见传右值引用和传值一模一样。

附录

AARCH64寄存器

参考ARMv8-aarch64 寄存器和指令集

通用寄存器

  • 参数寄存器(X0-X7): 用作临时寄存器或可以保存的调用者保存的寄存器变量函数内的中间值,调用其他函数之间的值(8 个寄存器可用于传递参数)

  • 来电保存的临时寄存器(X9-X15): 如果调用者要求在任何这些寄存器中保留值调用另一个函数,调用者必须将受影响的寄存器保存在自己的堆栈中帧。 它们可以通过被调用的子程序进行修改,而无需保存并在返回调用者之前恢复它们。

  • 被调用者保存的寄存器(X19-X29): 这些寄存器保存在被调用者帧中。 它们可以被被调用者修改子程序,只要它们在返回之前保存并恢复。

特殊用途寄存器(X8,X16-X18,X29,X30):

X8: 是间接结果寄存器,用于保存子程序返回地址,尽量不使用

X16 和 X17: 程序内调用临时寄存器

X18: 平台寄存器,保留用于平台 ABI,尽量不使用

X29: 帧指针寄存器(FP)

X30: 链接寄存器(LR)

X31: 堆栈指针寄存器 SP 或零寄存器 ZXR

参考文献