考虑这三个任务:
my_type x = func_returning_my_type_byvalue();
my_type & y = func_returning_my_type_byvalue();
my_type && z = func_returning_my_type_byvalue();
第一个 - 你有一个局部变量x
,它被初始化为函数调用的结果(右值),因此可以使用移动构造函数/赋值,或者可以完全省略 x 的构造(跳过并x
就地构造func_returning_my_type_byvalue
当它产生结果时)。
请注意,这x
是一个左值 - 您可以获取它的地址,因此它本身也是一种引用类型。从技术上讲,所有不是引用的变量都是对自身的引用。在这方面,左值是对已知存储持续时间内存进行分配和读取的绑定站点。
第二个不会编译 - 您不能将引用分配给结果(这种方式),您必须使用引用分配语法来为现有左值设置别名。但是,这样做非常好:
my_type & y = func_returning_my_type_byreference();
// `y` will never use constructors or destructors
这就是第三个存在的原因,当我们需要对某些东西的引用时,我们无法使用常规语法创建对引用的引用。在类似于func
原始问题的内容中,生命周期arg
并不是立即显而易见的。例如,如果没有明确的移动,我们就无法做到这一点:
void func( my_type && arg ) {
my_type && save_arg = arg;
}
不允许这样做的原因是因为arg
首先是对值的引用。如果arg
' 值的存储(它所指的)比' 的存储更短save_arg
,那么save_arg
将调用该值的析构函数 - 实际上捕获它。这不是这里的情况,save_arg
它会首先消失,所以将一个左值传递给它是没有意义的,我们可以在 之后仍然潜在地func
引用!
考虑一下,即使您要使用它来std:move
强制编译。析构函数仍然不会被调用,func
因为你还没有创建一个新对象,只是一个新的引用,然后这个引用在原始对象本身超出范围之前被销毁。
出于所有意图和目的arg
,它的行为就像它my_type&
一样,任何右值引用也是如此。诀窍是存储持续时间和通过引用传递延长生命周期的语义。这都是引擎盖下的常规引用,没有“右值类型”。
如果有帮助,请回忆递增/递减运算符。存在两个重载,而不是两个运算符。operator++(void)
(前)和operator++(int)
(后)。从来没有真正int
的被传递,只是编译器对于不同的情况/上下文/关于价值处理的协议有不同的签名。这与引用的处理方式相同。
如果右值和左值引用总是像左值一样被引用,有什么区别?
一句话:对象生命周期。
必须始终将左值引用分配给使用具有较长存储持续时间的东西,即已经构造的东西。这样就不需要为左值引用变量的范围调用构造函数或析构函数,因为根据定义,我们得到了一个准备好的对象,并且在它被销毁之前忘记了它。
同样相关的是对象以它们定义的相反顺序被隐式销毁:
int a; // created first, destroyed last
int b; // created second, destroyed 2nd-last
int & c = b; // fine, `c` goes out of scope before `b` per above
int && d = std::move(a); // fine, `a` outlives `d`, same situation as `c`
如果我们分配给一个右值引用,一个左值引用,同样的规则也适用——左值根据定义必须有更长的存储空间,所以我们不需要为c
or调用构造函数或析构函数d
。你不能用std::move
这个来欺骗编译器,因为它知道被移动对象的范围 -d
明显比它给出的引用更短,我们只是强制编译器使用右值类型检查/上下文,那就是我们所取得的一切。
不同之处在于非左值引用——比如可以引用它们的表达式,但这些引用肯定是短暂的,可能比局部变量的持续时间更短。提示提示。
当我们将函数调用或表达式的结果分配给右值引用时,我们正在创建对临时对象的引用,否则该对象无法被引用。因此,我们实际上是在从表达式的结果中强制就地构造变量。这是复制/移动省略的一种变体,其中编译器别无选择,只能省略临时到就地构造:
int a = 2, b = 3; // lvalues
int && temp = a + b; // temp is constructed in-place using the result of operator+(int,int)
案例与func
它归结为左值赋值 - 作为函数参数的引用指的是可能存在比函数调用更长的对象,因此即使参数类型是右值引用也是左值。
这两种情况是:
func( std::move( variable ) ); // case 1
func( my_type() + my_type() ); // case 2
func
不允许提前猜测我们将在哪种情况下使用它(无优化)。如果我们不允许第一种情况,那么就有正当理由认为右值引用参数的存储时间比函数调用短,但这也没有任何意义,因为对象要么总是在内部清理,func
要么总是在它之外,并且在编译时具有“未知”的存储持续时间并不令人满意。
编译器别无选择,只能假设最坏的情况,即情况 1可能最终发生,在这种情况下,我们必须保证存储持续arg
时间比func
一般情况下的调用更长。由于这一点 - 这arg
将被认为存在比调用更长func
的时间,并且func
生成的代码必须在这两种情况下都可以工作 -arg
允许的使用和假定的存储持续时间满足和不满足的my_type&
要求my_type&&
。