4

考虑以下:

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”

注意:候选函数不可行:对于第一个参数 void func(my_type&&) {},没有已知的从 'my_type' 到 'my_type &&' 的转换

虽然 g++ 4.9 说了几乎相同的内容:

错误:无法将“my_type”左值绑定到“my_type&&”

注意:初始化 'void func(my_type&&)' 的参数 1

这些错误消息让我很困惑,因为虽然ref肯定是一个lvalue,但它的类型仍然是my_type&&......不是吗?

我试图准确地了解这里发生了什么,所以我想知道以下哪些(如果有的话)是正确的:

  • 由于只有 rvalues 可以绑定到 rvalue 引用,并且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 转换回右值;我正在寻找“幕后”发生的事情的解释。

4

1 回答 1

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,它被初始化为函数调用的结果(右值),因此可以使用移动构造函数/赋值,或者可以完全省略 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`

如果我们分配给一个右值引用,一个左值引用,同样的规则也适用——左值根据定义必须有更长的存储空间,所以我们不需要为cor调用构造函数或析构函数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&&

于 2016-06-27T01:27:00.123 回答