请考虑以下事项:
struct my_type {};
my_type make_my_type() { return my_type{}; }
void func(my_type&& arg) {}
int main()
{
my_type&& ref = make_my_type();
func(ref);
}
毋庸置疑,此代码无法编译。我意识到我需要在第二个函数调用中使用std::move()
,但出于理解的目的,我想要考虑代码。
尝试编译上面的内容,Clang 3.5告诉我:
错误:没有匹配函数来调用'func'
注意:候选功能不可行:没有已知的从'my_type'到'my_type&&'的转换对于第一个参数void func(my_type&&){}
虽然g ++ 4.9说的几乎相同:
错误:无法将'my_type'左值绑定到'my_type&&'
注意:初始化'void func(my_type&&)'的参数1
这些错误消息让我感到困惑,因为虽然ref
肯定是左值,但其类型仍然是my_type&&
... isn不是吗?
我正在努力理解这里到底发生了什么,所以我想知道以下哪些(如果有的话)是真的:
由于只有rvalues可以绑定到右值引用,ref
是左值,因此不能绑定到arg
。来自Clang和g ++的错误消息在误称为ref
是(非参考)my_type
“无法转换”时会产生误导。
因为它是左值,所以ref
作为非参考my_type
被视为重载解析的目的,尽管其实际类型为my_type&&
。来自Clang和g ++的错误消息具有误导性,因为它们显示内部用于函数匹配的类型,而不是ref
的实际类型。
在main()
的正文中,ref
的类型是普通my_type
,尽管事实上我明确写了my_type&&
。因此来自编译器的错误消息是准确的,并且我的期望是错误的。然而,这似乎并非如此,因为
static_assert(std::is_same<decltype(ref), my_type&&>::value, "");
通过。
还有其他一些我没有考虑的魔法。
重复一下,我知道解决方法是使用std::move()
将rref转换回rvalue;我正在寻找对“幕后”发生的事情的解释。
答案 0 :(得分:0)
考虑这三个任务:
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
并且它被初始化为函数调用(rvalue)的结果,因此可以使用移动构造函数/赋值或构造x 可以完全省略(跳过并x
在func_returning_my_type_byvalue
生成结果时就地构建。)
请注意,x
是一个左值 - 您可以获取其地址,因此它也是一种引用本身。从技术上讲,所有非引用的变量都是对它们自身的引用。在这方面,左值是一个绑定站点,用于分配和读取已知存储持续时间内存。
第二个不会编译 - 你不能分配对结果的引用(这种方式),你必须使用引用赋值语法来 alias 一个现有的左值。然而,这样做完全没问题:
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
,则{{1}会调用该值的析构函数 - 实际上是捕获它。情况并非如此,save_arg
将首先消失,因此将左值转移到其中是没有意义的,我们可以在save_arg
之后仍然引用潜在的!
考虑即使你 使用func
强制进行编译。析构函数仍然不会在std:move
内调用,因为您还没有创建新对象,只是新的引用,然后在原始对象本身出现之前销毁此引用范围。
对于所有意图和目的,func
的行为就像它arg
一样,与任何右值引用一样。技巧是存储持续时间和通过引用传递的生命周期扩展的语义。它是引擎盖下的所有常规引用,没有&#39; rvalue类型&#39;。
如果有帮助,请调用增量/减量运算符。存在两个重载,而不是两个运算符。 my_type&
(前)和operator++(void)
(后)。从来没有传递过实际的operator++(int)
,只是因此编译器对于价值处理的不同情况/上下文/协议有不同的签名。这与参考文献的处理方式相同。
总之:对象的生命周期。
必须始终将左值参考指定为使用更长存储持续时间的东西,这已经构建了。这样就不需要为左值引用变量的范围调用构造函数或析构函数,因为根据定义,我们会得到一个就绪对象,并在它被销毁之前忘记它。
它也是相关的,对象是按照它们定义的相反顺序被隐式销毁的:
int
如果我们分配了一个左值引用,那么它是一个左值引用,同样的规则也适用 - 根据定义,左值必须有更长的存储空间,因此我们不需要为{{1}调用构造函数或析构函数}甚至是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
欺骗编译器,因为它知道要移动的对象的范围 - d
明确的持续时间短于引用它&#如果给出了,我们只是强迫编译器使用右值类型检查/上下文以及我们已经实现的所有内容。
不同之处在于非左值引用 - 像表达式这样的东西可以引用它们,但这些引用肯定是短暂的,可能比局部变量的持续时间短。提示提示。
当我们将函数调用或表达式的结果赋给rvalue引用时,我们正在创建对无法引用的临时对象的引用。因此,我们实际上强制从表达式的结果中就地构造变量。这是复制/移动省略的变体,编译器别无选择,只能忽略临时到就地构造:
std::move
d
归结为左值赋值 - 作为函数参数的引用引用可能存在的对象比函数调用存在的时间更长,因此即使在时也是lvalues 参数类型是右值参考。
这两个案例是:
int a = 2, b = 3; // lvalues
int && temp = a + b; // temp is constructed in-place using the result of operator+(int,int)
func
不允许猜测我们将提前使用哪种情况(无优化)。如果我们不允许案例1,那么将合法的理由认为右值引用参数的存储持续时间少于函数调用,但这也没有意义,因为 该对象始终在func( std::move( variable ) ); // case 1
func( my_type() + my_type() ); // case 2
内或在其外部进行清理,并且有&#34;未知&#34;编译时的存储时间不尽如人意。
编译器别无选择,只能假设最坏的情况,1 可能最终发生,在这种情况下,我们必须保证{{1}的存储持续时间因为比一般情况下对func
的调用要长。因此,func
被认为存在的时间比arg
某些当时的调用时间长,而func
&#39;生成的代码必须在两种情况下均可使用 - arg
的允许使用量和假定的存储时间符合func
而非func
的要求。< / p>