表达式 `new T` 的值是 rvalue 还是 lvalue?

7

我正在阅读关于 rvalue 引用的教程/解释:

http://thbecker.net/articles/rvalue_references/section_07.html

在倒数第二段中,作者提到“factory 函数体内 T 的复制构造函数的参数是左值”。他所指的代码如下:

template<typename T, typename Arg> 
shared_ptr<T> factory(Arg const & arg)
{ 
  return shared_ptr<T>(new T(arg));
}

我知道new T(arg)会在堆上构造一个T对象,但是返回值不是一个临时指针值吗?如果不使用它(导致内存泄漏),那么它将丢失,因此是右值吗?
编辑:为了澄清,我理解在这个例子中不会出现内存泄漏。我只是想说,如果指针值不被使用,我们就无法访问构造的T对象,因此会出现内存泄漏。

std::shared_ptr<T> 实际上为您提供了一个 _lvalue_。 - πάντα ῥεῖ
如果一个 shared_ptr 不再被引用,它将被清理。例如,如果你从一个方法中返回一个 shared_ptr,但在调用代码中没有使用该 shared_ptr,则该 shared_ptr 将自动被清理。 - cageman
我认为new返回一个rvalue,该值被复制到std::shared_ptr的内部lvalue中。 - Galik
2
这篇文章讨论的是 new T(arg) 中的 arg,而不是整个表达式。arg 是一个左值。new T(arg) 是一个右值。 - Raymond Chen
1
正确。引用“T的复制构造函数的参数”是指lvalue arg。这与newshared_ptr无关。 - Oktalist
显示剩余2条评论
4个回答

6
简短回答,我相信有人会写更长的答案,但是: new 给你一个指针 rvaluenew int = nullptr; 会编译失败并提示需要一个 lvalue
解引用该指针会给你一个 lvalue*(new int) = 5; 可以编译(当然,这个简单的语句也会泄露内存,因为指针丢失了)。
复制构造函数需要一个引用,所以如果你有对象的指针,你需要对它进行解引用。
如果你丢失了指针,那么你就不能删除它,所以它不会被销毁,堆内存也不会被释放(直到程序退出并将内存返回给操作系统)。
如果你把指针放到其他可以拥有它的对象中,比如一个 shared_ptr,那么你就不会失去指针。其他对象将根据其语义删除它,在其他对象(或最后一个对象,在共享所有权的情况下,比如 shared_ptr)本身被销毁时最迟删除它。

3
许多贡献者似乎对值类别有所困惑,甚至有人认为shared_ptr“存储一个左值”,这根本没有任何意义。左值和右值与某些东西的“返回”或内存中对象的状态无关,而是与代码中表达式的状态有关。表达式是左值还是右值(或其他之一)取决于各种语言规则和结构。简而言之,左值表达式是“名称”,其他所有内容都不是左值表达式。这个规则有一个显著的例外是指针类型的*ptr,因为出于历史原因,*ptr被定义为会产生一个左值(除非涉及运算符重载)。现在,在标准中并没有明确建议new“返回”(求值为)rvalue,因为这个陈述毫无意义。按照语言规则,new求值得到指向新内存块的指针,而表达式new T是右值,因为这就是右值的含义。如果不使用new T(arg)返回的临时指针值将会丢失,这完全与动态分配内存的事实无关。

已更改标题以反映您刚才说的内容。希望现在更有意义。谢谢。 - elatalhm
@elatalhm: 这样更好! - Lightness Races in Orbit

2
很不幸,运算符及其操作数的值类别 未指定,这使我们陷入了不幸的境地,不得不推断出该值类别。许多人认为,除非明确指定结果是 prvalue。
我们可以通过尝试在赋值的左侧使用 new 并发现它无法正常工作来作出一个相当有根据的猜测,并得出它不产生 lvalue 的结论,但我们仍然处于未指定的领域。
我们可以像这个 Luc Danton 回答中的代码 一样通过实证来确定它。
template<typename T>
struct value_category {
    // Or can be an integral or enum value
    static constexpr auto value = "prvalue";
};

template<typename T>
struct value_category<T&> {
    static constexpr auto value = "lvalue";
};

template<typename T>
struct value_category<T&&> {
    static constexpr auto value = "xvalue";
};

// Double parens for ensuring we inspect an expression,
// not an entity
#define VALUE_CATEGORY(expr) value_category<decltype((expr))>::value

and the result of:

std::cout << VALUE_CATEGORY( new int) << std::endl ;

是:

prvalue

,这证实了我们通过推断得出的结论。

除此之外,它只检查了特定的C++实现。 - Yakk - Adam Nevraumont
@Yakk 我曾经在问 这个问题 时花了一段时间研究,看起来虽然它在官方上是未确定的,正如 Joseph Mansfield的评论 和其他来源所述,但编译器开发人员已经同意了这些事情,因此我认为它不太可能会有所不同,尽管从技术上讲他们可能会。 - Shafik Yaghmour
你还可以从这样的缺陷报告中发现,委员会对这些表达式的值类别有明确的看法,尽管这些看法在标准中没有正式说明。我希望约瑟夫的缺陷报告最终能够导致这一点在标准中更明确地被编码,就像C++14中使用不确定值的方式一样。 - Shafik Yaghmour

1

new 的返回值是一个 prvalue 而不是 lvalue,因此您无法写成:

 new T(arg) =  ....;   // not legal, so no lvalue

标准定义(§3.10):左值(历史上称为左值,因为左值可以出现在赋值表达式的左侧)指定一个函数或对象。new的返回值指定对象的地址,而不是对象本身。
如果您将此值分配给指针a并对该指针进行解引用,甚至如果您直接对新的返回值进行解引用,则会使其成为左值:
*new T(arg) = ....;    //  valid, althoug you'd loose the address ! 
*a = ....;     // normal poitner dereferencing 

这在标准中有解释(§5.3.1/1):一元*运算符执行间接引用:应用该运算符的表达式必须是对象类型的指针或函数类型的指针,结果是一个左值,引用该表达式指向的对象或函数。 重要提示:您的编辑和shared_ptr 您的示例没有泄漏:shared_ptr<T>(new T(arg))创建了一个共享指针来管理您新创建的对象,并且如果不再使用,则会删除它。
两种可能性:
  • 返回的 shared pointer 在表达式中使用(例如作为临时 shared pointer 或分配给另一个 shared_ptr 对象):对象将被复制,并更新其使用计数以反映对其的活动引用次数。没有泄漏!

  • 忽略返回的 shared pointer :返回的对象将被销毁,其使用计数将递减,并且由于它在其他地方没有被使用,因此您的对象将被删除。


1
那个赋值的例子证明了什么?声明 const int i = 5。你不能写 i = ...。然而,i 是一个左值。声明 void foo()。你不能写 foo = ...。然而,foo 是一个左值。 - AnT stands with Russia
是的,我同意经验法则并不完全准确,因为它没有考虑到不可修改的对象(const和函数)。 - Christophe
1
函数根本不是一个对象。"lvalue"的概念早已从轶闻中的“可以用于赋值操作的左侧”测试中分离出来。从lvalue的定义中得出的更精确的测试是通过内置的一元&运算符进行的。但即使是这个测试也不是100%准确的。 - AnT stands with Russia

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接