考虑以下代码,它以“嵌套”方式将临时对象绑定到const
引用:
#include <iostream>
std::string foo()
{
return "abc";
}
std::string goo()
{
const std::string & a = foo();
return a;
}
int main()
{
// Is a temporary allocated on the heap to support this, even for a moment?
const std::string & b = goo();
}
我一直试图理解编译器在内存存储方面必须做些什么才能支持这种“嵌套”构造。
我怀疑对于foo()
的调用,内存分配很简单:std::string
的存储将在函数foo()
退出时在堆栈上分配。
但是,编译器必须做些什么来支持b
引用的对象的存储?函数goo
的堆栈必须展开并“替换为”b
引用的堆栈上的对象,但是为了展开goo
的堆栈,编译器将是需要暂时在堆上创建对象的副本(在将其复制回不同位置的堆栈之前)?
或者编译器是否可以在堆上没有分配任何存储的情况下完成此构造的要求,即使是暂时的?
或者甚至可以让编译器对b
引用的对象的相同的存储位置使用a
引用的对象,而不进行操作堆栈上还是堆上的任何额外分配?
答案 0 :(得分:7)
我认为你没有考虑过中间步骤,即你没有b
与a
的约束,而是a
的副本。这不是因为任何奇特的记忆恶作剧!
goo
按值返回,因此,根据所有常用机制,该值在{em> full-expression 范围内main
范围内可用。它要么在main
的堆栈框架中,要么在其他地方,或者(在这个设计的情况下)可能完全优化。
这里唯一的魔力就是main
范围内保留,直到b
超出范围,因为b
是参考 - const
(而不是近乎立即销毁)。
那么,堆会以任何方式进入它吗?好吧,如果你有一堆,没有。如果你的意思是免费商店,那么,仍然没有。
答案 1 :(得分:4)
从理论上讲,由于goo
(以及foo
)按值返回,因此将返回a
引用的变量的副本(并放置在堆栈中)。所述副本的生命周期将延长b
,直到b
的范围结束。
我认为您遗漏的要点是您按价值返回。这意味着在foo
或goo
返回之后,它确实对它们内部的任何内容没有任何区别 - 您将留下一个临时字符串,您将其绑定到const
引用。
在实践中,一切都很可能会被优化。
答案 2 :(得分:4)
以下是C ++标准允许编译器重建代码的示例。我正在使用完整的NRVO。请注意使用展示位置new
,这是一个中等模糊的C ++功能。你传递new
指针,然后在那里构造结果,而不是在免费商店中。
#include <iostream>
void __foo(void* __construct_std_string_at)
{
new(__construct_std_string_at)std::string("abc");
}
void __goo(void* __construct_std_string_at)
{
__foo(__construct_std_string_at);
}
int main()
{
unsigned char __buff[sizeof(std::string)];
// Is a temporary allocated on the heap to support this, even for a moment?
__goo(&__buff[0]);
const std::string & b = *reinterpret_cast<std::string*>(&__buff[0]);
// ... more code here using b I assume
// end of scope destructor:
reinterpret_cast<std::string*>(&__buff[0])->~std::string();
}
如果我们在goo
中屏蔽了NRVO,那么它看起来就像
#include <iostream>
void __foo(void* __construct_std_string_at)
{
new(__construct_std_string_at)std::string("abc");
}
void __goo(void* __construct_std_string_at)
{
unsigned char __buff[sizeof(std::string)];
__foo(&__buff[0]);
std::string & a = *reinterpret_cast<std::string*>(&__buff[0]);
new(__construct_std_string_at)std::string(a);
// end of scope destructor:
reinterpret_cast<std::string*>(&__buff[0])->~std::string();
}
int main()
{
unsigned char __buff[sizeof(std::string)];
// Is a temporary allocated on the heap to support this, even for a moment?
__goo(&__buff[0]);
const std::string & b = *reinterpret_cast<std::string*>(&__buff[0]);
// ... more code here using b I assume
// end of scope destructor:
reinterpret_cast<std::string*>(&__buff[0])->~std::string();
}
基本上,编译器知道引用的生命周期。因此,它可以创建存储变量实际实例的“匿名变量”,然后创建对它的引用。
我还注意到,当你调用一个函数时,你有效地(隐式地)将一个指向缓冲区的指针传递给返回值所在的位置。因此被调用的函数在调用者的范围内“就地”构造了对象。
使用NRVO,被调用函数作用域中的命名变量实际在调用函数“返回值所在的位置”构造,这使得返回变得容易。没有它,你必须在本地做所有事情,然后在return语句中将你的返回值复制到你的返回值的隐式指针,通过相当于placement new。
在堆(也就是免费商店)上不需要做任何事情,因为生命周期都很容易证明和堆栈排序。
具有预期签名的原始foo
和goo
必须仍然存在,因为它们具有外部链接,直到发现没有人使用它们时可能会丢弃。
所有以__
开头的变量和函数仅用于展示。编译器/执行环境不再需要具有命名变量,而不需要具有红血球名称。 (理论上,因为__
是保留的,在编译之前进行这样的转换传递的编译器可能是合法的,如果你实际使用了那些变量名并且编译失败,那将是你的错,而不是编译器的错,但是...这将是一个非常hackey编译器。;))
答案 3 :(得分:3)
不,生命周期延长不会有任何动态分配。常见的实现等同于以下代码转换:
std::string goo()
{
std::string __compiler_generated_tmp = foo();
const std::string & a = __compiler_generated_tmp;
return a;
}
不需要动态分配,因为只要引用处于活动状态,生命周期才会被扩展,并且在当前作用域结束时将通过C ++生命周期规则进行扩展。通过在范围中放置一个未命名的(__compiler_generated_tmp
在上面的代码中)变量,通常的生命周期规则将适用并执行您期望的操作。
答案 4 :(得分:1)
在std::string goo()
中,std :: string按值返回。
当编译器看到你在main()中调用这个函数时,它注意到返回值是一个std :: string,并在main的堆栈上为std :: string分配空间。
当goo()返回时,goo()内的引用a
不再有效,但是std :: string a
引用被复制到main()中堆栈上保留的空间中
在这种情况下,可能会进行一些优化,你可以阅读一个编译器可以做什么here