临时对象的const成员引用

20
我很乐意为您翻译。以下是需要翻译的内容:

在C ++中,const引用延长了从函数返回的临时对象的生命周期,这是一个已知特性,但使用常量引用来访问从函数返回的临时对象的成员是否可行呢?

示例:

#include <string>

std::pair<std::string, int> getPair(int n)
{
    return {std::to_string(n), n};
}

int main(int, char*[])
{
    const int x = 123456;
    const auto& str = getPair(x).first;
    printf("%d = %s\n", x, str.c_str());    
    return 0;
}

输出:

123456 = 123456

2
在我看来,像“……是一个众所周知的功能……”这样的短语并不好。它们歧视那些不了解该功能的人,并且没有任何信息。不过,这是一个有趣的问题。 - 463035818_is_not_a_number
2
我不是100%确定,但我认为根据标准它是有效的。临时变量的生命周期应该尽可能延长到其成员访问(在这种情况下为str)的生命周期。也就是说,通过复制返回值,你应该没问题。RVO将避免进行额外的复制。 - Arunmu
1
高度相关:https://dev59.com/zlsV5IYBdhLWcg3wrAVJ - NathanOliver
1
我认为相比于当前标准,那个答案已经过时了。 - Smeeheey
4个回答

13

是的,这段代码是完全可接受的。根据标准([class.temporary])规定:

  1. 有两种情况下临时对象的销毁时间不同于完整表达式结束的时间点。第一种情况是当调用默认构造函数来初始化数组的一个元素时。如果构造函数有一个或多个默认参数,则在构造下一个数组元素(如果有)之前顺序销毁在默认参数中创建的每个临时对象。

  2. 第二种情况是当引用绑定到一个临时对象时。被绑定引用的临时对象或引用所绑定的子对象的完整对象将持续存在于引用的生命周期中...

正如您所看到的,突出显示的那行清楚地表明将引用绑定到子对象是可以接受的,因为完整对象也必须扩展其生存期。

请注意,first 确实符合子对象的条件[intro.object]:

  1. 对象可以包含其他对象,称为子对象。子对象可以是成员子对象(9.2),基类子对象(Clause 10)或数组元素。不是任何其他对象的子对象的对象被称为完整对象。

1
我99%确定,突出显示的文本特指将父类引用绑定到子类临时对象。 - Mark B
3
在这里,first是子对象,pair是完整对象。规则指出,“引用所绑定的子对象(即first)的完整对象(即pair)”也必须具有寿命延长。 - Smeeheey
我不认为这是正确的:规范中没有这样说。例如,我认为 return getPair(x).first 不允许进行 RVO,因为它不是一个临时对象。 - Johannes Schaub - litb
我同意你的例子无法进行RVO(返回值优化) - 这是因为根据[class.copy/31.1]中的复制省略(copy elision)限制,只有在通过名称返回自动对象时才能执行返回值优化,因此你的例子不符合该基础。我认为这并不表明first在这种情况下不是临时对象,尽管关于临时对象确切定义的规则似乎有些模糊。 - Smeeheey
1
我并不是在说标准表明.first是一个临时对象。我是说标准对这个术语存在歧义。事实上,它反复提到它,但没有正式定义它。然而,如果临时对象的子对象本身不是临时对象,那么我不明白为什么我上面回答中强调的规则部分可能适用于什么情况,因此该规则部分的重点是什么。 - Smeeheey
显示剩余17条评论

8

定义明确。

根据标准:$12.2/6 临时对象 [class.temporary]

(强调是我的)

与引用绑定的临时变量或者是与引用绑定的子对象的完整对象将持续到引用的生命周期结束。

关于子对象,请参见$1.8/2 C++对象模型 [intro.object]

(强调是我的)

对象可以包含其他对象,称为子对象。子对象可以是一个成员子对象 ([class.mem]),一个基类子对象(第[class.derived]条),或者是一个数组元素。不是任何其他对象的子对象的对象称为完整对象

first被绑定到引用上,它是std::pair的成员子对象,因此临时对象std::pair(即完整对象)的生命周期将会延长,代码应该是正确的。

仅供参考:ClangGCC说是,VC说不是。


但是 first 是子对象的完整对象吗? - NathanOliver
1
@NathanOliver 临时变量是子对象的完整对象,而不是“first”。 - songyuanyao
2
看起来像是gcc的bug。请参见http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html. "1834. Constant initialization binding a reference to an xvalue". 我认为这里的first是一个xvalue。 - Arunmu
2
GCC目前还没有实现CWG 1834 - 如果你尝试使用非整数类型(以避免常量折叠),你会看到gcc的相同行为。 - Barry
@Barry和Arunmu,你们说得对。很高兴知道这一点。 - songyuanyao

2
如我在评论中提到的:
临时变量的生命周期应该与其成员访问(在这种情况下为str)的生命周期一样长。也就是说,通过拷贝返回值应该没问题。RVO会避免做多余的拷贝。
根据标准第12.2.5节:
第二个上下文是当引用被绑定到一个临时对象。被引用绑定的临时对象或是被引用绑定子对象的完整临时对象在引用的生命周期内保留,除非:
- 在构造函数的构造函数初始值列表(12.6.2)中绑定到引用成员的临时对象持续到构造函数退出。 - 在函数调用(5.2.2)中绑定到引用参数的临时对象在包含该调用的完整表达式完成之前保留。
为了避免任何麻烦,我宁愿这么做:
auto m_p = getPair(x);

由于每个编译器都必须对此情况进行 RVO,因此这是最有效的方法。


引用所绑定的临时对象或子对象的完整对象的生命周期与引用的生命周期相同,但是并没有提到类成员,而 first 就是一个类成员。 - NathanOliver
在这种情况下,first不是pair的子对象吗? - Arunmu
1
我非常确定它在谈论这里的 const base& foo = deriveied_temporary() - NathanOliver
@NathanOliver - 请参见[intro.object/2](根据我的回答)。在这里,“first”确实是一个子对象。 - Smeeheey
@NathanOliver,是的,我同意Smeeheey的观点。看起来是这样。你能说说你为什么持不同意见吗? - Arunmu

-1

这个问题在12.2/4-5中有所涉及:

有两种情况下,临时对象的销毁时间不同于完整表达式的结束时间。第一种情况...[处理数组的内容]

第二种情况是当引用被绑定到一个临时对象时。除了以下四种情况外,引用绑定的临时对象或作为引用绑定的子对象的完整对象将持续引用的生命周期:

这里有四个例外,涉及到引用成员的构造函数绑定、函数调用、返回引用的函数以及在new-initializers中绑定的引用。

这些情况都不适用,因此临时对象在完整语句结束时被销毁,留下对临时对象成员的悬空引用。

只需帮助编译器意识到它可以从临时对象中移动:const auto str = std::move(getPair(x).first);


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