【问题标题】:Confusing error messages with named rvalue references将错误消息与命名右值引用混淆
【发布时间】:2023-03-07 15:57:02
【问题描述】:

考虑以下几点:

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 肯定是一个左值,但它的type 仍然是my_type&&...不是吗?

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

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

【问题讨论】:

  • while ref is certainly an lvalue, its type is still my_type&amp;&amp;... isn't it? 不是真的。对于大多数意图和目的,引用类型的表达式不存在。 "5/5 如果表达式最初具有类型“对 T 的引用”(8.3.2、8.5.3),则在进一步分析之前将类型调整为 T。表达式指定对象或由引用表示的函数,并且表达式是左值或 xvalue,具体取决于表达式。"
  • 认为 Nic's answer 对一个有点相关的问题可能会提供一两个提示。
  • 嗯,作为参数传递给函数的是一个表达式,如f(2+2)。表达式的一种可能形式是 id-expression (5.1.1) - 只是一个名称(变量或命名常量)。在func(ref) 中,ref 是一个 id-expression,其 unqualified-id 命名一个变量。变量类型为my_type&amp;&amp;;该表达式是my_type 类型的左值,这要归功于每个5/5 的调整。
  • 您的第一个要点是正确的。 表达式 ref 的结果是一个左值。编译器语法也是正确的,不仅是因为上述原因,还因为没有“my_type&amp;&amp; lvalue”之类的东西。
  • 使用expression_name&lt;decltype((ref))&gt;() (stackoverflow.com/a/20721887/576911) 输出表达式 ref的类型。使用type_name&lt;decltype(ref)&gt;()stackoverflow.com/questions/81870/print-variable-type-in-c/…)输出ref声明类型

标签: c++ c++11 language-lawyer rvalue-reference pass-by-rvalue-reference


【解决方案1】:

考虑这三个任务:

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&amp;,任何右值引用也是如此。诀窍是存储持续时间和通过引用传递延长生命周期的语义。引擎盖下都是常规引用,没有“右值类型”。

如果有帮助,请回忆一下递增/递减运算符。存在两个重载,而不是两个运算符。 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甚至@调用构造函数或析构函数987654345@。您不能在此使用 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 不允许提前猜测我们将在哪种情况下使用它(没有优化)。如果我们不允许第 1 种情况,那么将有正当理由认为右值引用参数的存储持续时间比函数调用短,但这也没有任何意义,因为 either 对象总是在func 内部或总是在它外部清理,并且在编译时具有“未知”的存储持续时间并不令人满意。

编译器别无选择,只能假设最坏的情况,即第一种情况可能发生最终,在这种情况下我们必须保证arg的存储时长比一般情况下对func 的调用要长。因此,arg 将被认为存在的时间比对func 的调用时间长一些,并且func 生成的代码必须 em> 在这两种情况下都有效 - arg 的允许使用量和假定的存储持续时间满足 my_type&amp; 而不是 my_type&amp;&amp; 的要求。

【讨论】:

    猜你喜欢
    • 2020-04-14
    • 2020-08-24
    • 1970-01-01
    • 2014-04-14
    • 1970-01-01
    • 2013-06-19
    • 1970-01-01
    • 1970-01-01
    • 2017-01-24
    相关资源
    最近更新 更多