我正在阅读有关右值的内容,并在编译器资源管理器中尝试代码片段时遇到了一些问题。这是一个人为的例子:

class A
{
    public:

    A&& func(A& rhs)
    {
        //return rhs;
        return std::move(rhs); //returning A&&
    }
};

对比

class A
{
    public:

    A func(A& rhs)
    {
        //return rhs;
        return std::move(rhs); //returning A&&
    }
};

我原本以为第二个代码片段会因为返回类型不匹配而编译失败。Func 的返回类型是 ,A而它实际返回的是A&&std::move()我尝试了最新版本的 gcc、clang 和 msvc。它们都具有相同的行为。有人能解释一下这里发生了什么吗?

11

  • 如果我们假设使用了移动构造函数,我们可以检查事实是否如此:


    – 

  • 3
    您混淆了值类别和类型。您没有返回A&&,而是返回了 类型的右值A


    – 

  • 1
    “由于返回类型不匹配” ——不匹配并不像您想象的那么严格。您是否尝试过类似的事情float one() { return 1; }?也就是说,一个函数返回一个float,但return语句被赋予了不同的类型(一个int值)来返回?


    – 

  • 1
    侧面评论 – 避免使用 std::move 来返回值,因为它可以防止 RVO(返回值优化) – 参见 C++ 每周 404(


    – 

  • 1
    @san216 “我甚至没有考虑应该调用哪个构造函数。我说的是编译期间应该首先进行的静态类型检查。” ——这是一个矛盾。确定应该调用哪个构造函数是静态类型检查的一部分。这是必须的,因为该构造函数是否被标记explicit决定了是否允许隐式转换。(试一试:标记移动构造函数explicit将导致你的第二个代码片段编译失败。)


    – 


最佳答案
2

在 C++ 中,当您std::move对对象使用时,您实际上并没有将其转换为右值引用 ( T&&)。相反,您将其强制转换为右值,这向编译器发出信号,它可以将该对象视为临时对象,并且如果可能的话,可以从中移动而不是复制。但是,std::move不会改变基础类型;它只是为编译器提供使用移动语义的提示。

以下是两个例子中发生的情况:

示例 1:A&& func(A& rhs)

在此版本中,函数func返回一个A&&(对 的右值引用A)。当您返回 时std::move(rhs),您直接返回一个右值引用,它与返回类型 匹配A&&。这是有道理的,并且会按预期进行编译。

示例 2:A func(A& rhs)

在这种情况下,函数的func返回类型为A,它是一个值(而不是右值引用)。但是,当您返回 时std::move(rhs),您仍然会转换rhs为右值引用(A&&)。这就是为什么它仍然编译并表现如您所观察到的那样:

  • 自动转换:当您指定 (a value) 的返回类型时A,编译器期望创建并返回一个A对象。如果您提供A&&(via ),编译器会将其解释为移动构造函数而不是复制它的std::move(rhs)指令。编译器认为这是一个右值并使用移动构造函数来构造返回值。Arhsrhs

  • 返回类型推导和临时实现:如果返回类型为按值,则编译器将在返回右值时隐式执行移动。在这种情况下,A func(A& rhs)指定返回类型为A,因此编译器将其视为右值,并使用移动构造函数std::move(rhs)创建一个临时值。A

7

  • 1
    “当您对对象使用 std::move 时,您实际上并没有将其转换为右值引用 (T&&)。“std::move实际上只是强制转换为T&&。 它以函数形式提供的唯一原因,而不是在代码中明确进行强制转换,是为了避免重复类型T


    – 

  • @jayashankar 强制转换意味着强制编译器将一种类型解释为另一种类型。除非转换非法,否则编译器将继续使用新的(转换)类型。从逻辑上讲,我认为语法检查中的静态类型检查将首先进行。编译器是否应该调用移动或复制构造函数或 RVO,应该稍后进行?


    – 

  • 1
    @ArthurTacca 答案是正确的。表达式永远没有引用类型,因此std::move只会将值类别更改为右值,而不会更改类型。


    – 

  • @HolyBlackCat 即使从语言律师的角度来看,它在技术上是正确的,但我认为它在这样的介绍/摘要中具有误导性。他们说“你实际上并没有将它转换成…… T&&”对于字面上定义为的东西static_cast<T&&>


    – 

  • 2
    更重要的是,这种区别(如果有的话)在这里并不重要。这个问题的关键在于,C++ 允许从xin隐式转换return x为实际返回的值,因此它是使用构造函数从T&&(或右值到T或任何你想叫它的名字)转换为实际T值(尽管可以省略复制/移动构造)。


    – 

我预计第二个代码片段会因为返回类型不匹配而编译失败。

这里没有有问题的不匹配。

在第二种情况下,函数返回
类型为 的右值A。此返回值将与 的移动构造函数

一起使用
,以在调用方端构造实例。
AA

下面证明了这一点:

#include <iostream>

class A {
public:
    A()                              { std::cout << "default ctor\n"; };
    ~A()                             { std::cout << "dtor\n"; };
    A([[maybe_unused]] A const & a)  { std::cout << "copy ctor\n"; };
    A([[maybe_unused]] A && a)       { std::cout << "move ctor\n"; };

    // To respect the rule of 5 we also need copy and move operator=, 
    // but it is omitted here for the example

    static A func(A& rhs) {
        //return rhs;
        return std::move(rhs); //returning A&&
    }
};

int main() {
    A a1;
    A a2 = A::func(a1);
}

输出:

default ctor
move ctor
dtor
dtor

如果您不提供移动构造函数,编译器将会生成它。