是不是运行完return语句就发生了赋值或拷贝的行为?将x*2的结果
赋值给一个无名的临时对象(假如叫temp)?
如果你不想了解编译器的具体细节,这样理解是没问题的。具体到编译器-汇编语言层面上,不同的编译器和开发环境里,底层的行为并不是完全一样的。
比如,这个temp是谁准备的?这个temp具体放在那里?如果temp是一个比较大的数据结构,而不是一个常见的int/long这种类型的数据,那么temp如何保存?
这个东西叫做ABI(Application binary interface),不同的编译器,不同的操作系统,32、64位不同环境下,不同的硬件环境(ARM/X86/PPC),ABI的行为不完全相同。
通常情况下,在Windows、Linux环境里,是用EAX/RAX寄存器来保存返回值,因为高级语言里并不能直接看到寄存器(内联汇编除外),所以这个寄存器可以理解为你所说的temp,但如果返回值比较大,寄存器里放不下这么大的数据,比如返回一个结构体,或者返回浮点类型的数据,那么具体行为就要看编译器实现了。
C++ 中从函数返回(不考虑异常、 longjmp
等非正常退出方式)的过程是个稍微有点长的话题…初学阶段可能不必纠结。这段代码不需要无名的临时对象,也没有赋值。
正常来说你只需要普通地写函数,返回一个同返回类型的局部变量时不用 move
,尽可能避免悬垂指针/引用就行了。多数时候不用意识到后述的细节。
如果要返回指针/引用,就尽可能让它指代函数外部就创建好,并且不随函数退出而失效的对象,或函数体内的 static
对象;也可以返回 nullptr
以表示某种失败。
比较特殊的情况有针对调用约定优化返回类型的设计、为了优化抑制 NRVO [1]等等。这些算是相当进阶,甚至在业务中罕见的内容。
首先初始化返回值,这里做的事和返回类型有关:
void
。则这步不做任何事return
的操作数复制初始化产生一个纯右值,语义上通常没有临时对象。但如果返回类型是一个比较“平凡”(见后述)的类类型,则语义上可以有临时对象[2]。return
的操作数是一个忽略顶层 const
后与函数返回类型相同的,非 static
或 thread_local
,且不是形参的局部变量[3],则标准允许编译器令这个局部变量与函数返回的结果最终形成的对象成为同一对象[4][5]。这里最终形成的对象可能是用函数返回值初始化的同类型变量,或者函数返回值形成的临时对象等等。return
(也包括 C++20 协程的 co_return
)的操作数是一个对象类型或右值引用类型的变量,包括形参,但不是 static
或 thread_local
(这里也包括上一种情况中没有消除复制/移动时做的选择)[6],则首先把该操作数当作右值(即优先进行移动操作);如果不能选择正确的函数,再当作左值。初始化完返回值后,操作依次是:
return
语句中产生的临时对象;static
或 thread_local
)的局部变量。(这里“析构”的说法不太严谨,对于非类类型可以认为就是使对象不再存在了。)然后在标准语义中,控制已经离开了该函数,而函数调用表达式的类型和值类别按以下方式确定:
void
的函数,则表达式为 void
类型纯右值。不考虑构造函数,因为不存在直接调用构造函数的函数调用表达式[7]。const int
,函数调用表达式的类型也还是 int
)。函数返回类型如果是满足下列条件的类类型[9],则即使从 C++17 起,返回时语义上也可以有临时对象:
按照标准,如果形参如果是具有非平凡析构函数的类类型对象(注意不是引用),则由实现选择二种策略之一,具体如何选择是实现定义的:
这两种策略有一些细微的区别,前者会在更多情况下悬垂引用或悬垂指针。不过无论如何,都尽可能不要既令函数形参为对象类型,又令返回的引用或指针指代形参对象。
虽然标准中形参对象的析构是在离开函数之后,但选择前者的实现(例如 MSVC )可能会在汇编/机器码层次上离开被调用函数之前就进行析构。
实现上,调用一个未被内联的函数时,往往需要在栈上记录控制流在调用函数后待返回的位置。而函数正常返回时,控制流会取得栈上记录的地址,并跳转到该地址以进行后续操作。
除了这一通用部分,从函数返回的实现会依赖调用约定。各种平台的调用约定非常复杂[13],不过我们可以把返回的行为归纳为两个部分:返回方式与栈清理。
多数调用约定会允许使用一个或多个通用寄存器返回,而引用、指针不长于通用寄存器的整数会通过寄存器返回。浮点类型可能会通过专门的浮点寄存器返回。
在有些调用约定上,对于一个就传递/返回目的而言平凡[14]的类类型,若它大小和组成[15]满足一定要求,则它可以通过寄存器返回。
对于大小比较大,或返回会涉及非平凡特殊成员函数的类类型(以及一些编译器提供的较大的非标准类型),调用约定会选择通过内存返回(有些调用约定上任何类类型都要通过内存返回),具体做法是:
实际上,通过内存返回的机制构成了允许函数返回时不产生临时对象,以及允许编译器令局部变量和函数返回值最终形成对象等同的基础——前者需要继续向内传递最终形成的对象的地址,而后者需要在返回所用的地址上创建局部变量。
除了控制流待返回的位置,调用约定还还需要通过寄存器和栈传递参数 。有些调用约定只会通过栈传递,而有些是在传参用的寄存器不够时才通过栈传递。无论是哪种调用约定,都需要在离开函数后使栈记录回到原来的位置,通常通过将栈指针恢复为原先的值。
不同的调用约定在栈清理的表现上不同:
默认的调用约定(如 __cdecl
以及 x64 调用约定)上往往由调用方清理栈。这种设计对于 C 的变长实参函数(例如 printf
,注意它与 C++ 的形参包无关)更合理,同时也允许了 C++ 中在全表达式末尾析构形参对象。
另外的一些调用约定(如 32 位 Windows 的 __stdcall
) 上由被调用方清理栈。这种设计较不容易兼容变长实参函数(例如 __stdcall
就不支持),也使得这种调用约定不能允许在全表达式末尾析构形参对象。由 callee 清理栈目的可能是减少机器码大小:清理栈的机器码只需在被调用的函数内存在一次,而无需在每个 caller 处都重复。