谁复制了函数的返回值?

时间:2013-07-02 08:53:11

标签: c++ c++11 copying scopeguard

是调用者还是被调用者复制或移动函数的返回值?例如,如果我想实现队列的pop()函数,就像这样

template <typename T> 
class queue
{
    std::deque<T> d;
public:
    // ... //
    T pop()
    {
        // Creates a variable whose destructor removes the first
        // element of the queue if no exception is thrown. 
        auto guard = ScopeSuccessGuard( [=]{ d.pop_front(); } );
        return d.front();
    }
}

是复制前元素后调用范围内容的析构函数吗?

编辑:后续问题:该行

auto item = q.pop();

现在是非常安全的吗?

2 个答案:

答案 0 :(得分:9)

在局部变量超出范围之前复制出返回值。复制/移动可能是临时位置(堆栈或寄存器)或直接到调用者自己的缓冲区或首选寄存器 - 这是优化/内联问题。

如果涉及的临时位置编译器必须在调用者和被调用者之间安排一些分工,并且有许多OS和二进制对象/可执行格式特定的返回值约定(当然还有函数参数) ,使用一个编译器编译的库/对象通常仍然可以与另一个编译器/对象一起使用。

  

该行......

auto item = q.pop();
  

......强烈的异常安全吗?

假设pop_front()不能throw,有趣的情况是返回一个临时位置,在返回函数后,该值再次被复制到调用者缓冲区。在我看来,你没有充分保护这一点。 Elision(被调用者直接在调用者的结果缓冲区/寄存器中构造返回值)是允许的,但不是必需的。

为了探索这个,我编写了以下代码:

#include <iostream>

struct X
{
    X() { std::cout << "X::X(this " << (void*)this << ")\n"; }
    X(const X& rhs) { std::cout << "X::X(const X&, " << (void*)&rhs
                                << ", this " << (void*)this << ")\n"; }
    ~X() { std::cout << "X::~X(this " << (void*)this << ")\n"; }

    X& operator=(const X& rhs)
    { std::cout << "X::operator=(const X& " << (void*)&rhs
                << ", this " << (void*)this << ")\n"; return *this; }
};

struct Y
{
    Y() { std::cout << "Y::Y(this " << (void*)this << ")\n"; }
    ~Y() { std::cout << "Y::~Y(this " << (void*)this << ")\n"; }
};

X f()
{
   Y y;
   std::cout << "f() creating an X...\n";
   X x;
   std::cout << "f() return x...\n";
   return x;
};

int main()
{
    std::cout << "creating X in main...\n";
    X x;
    std::cout << "x = f(); main...\n";
    x = f();
}

使用g++ -fno-elide-constructors进行编译,我的输出(带有额外注释)是:

creating X in main...
X::X(this 0x22cd50)
x = f(); main...
Y::Y(this 0x22cc90)
f() creating an X...
X::X(this 0x22cc80)
f() return x...
X::X(const X&, 0x22cc80, this 0x22cd40)   // copy-construct temporary
X::~X(this 0x22cc80)   // f-local x leaves scope
Y::~Y(this 0x22cc90)
X::operator=(const X& 0x22cd40, this 0x22cd50)  // from temporary to main's x
X::~X(this 0x22cd40)
X::~X(this 0x22cd50)

显然,作业发生在f()左侧范围之后:任何例外都是在你的范围保护(此处由Y表示)被销毁之后。

如果main包含X x = f();X x(f());,则会发生同样的事情,除非它是在销毁f() - 局部变量后调用的复制构造函数。

(我很欣赏一个编译器的行为有时是推断标准是否需要某些东西工作的不良基础,但它反过来更可靠:当它不能正常工作时编译器坏了 - 哪个是相对罕见的 - 或者标准不要求它。这里,编译器行为只是用来增加我对标准要求的印象的重要性。)

好奇的细节:并非通常只有一种方式调用代码才有用,但可能安全的东西是const X& x = f();const引用扩展了临时的生命周期,但是我不能说服自己标准要求让临时生命的扩展是临时的,复制的函数不包含任何额外的副本;它的价值很小 - 它在我的程序中“起作用”,有趣的是临时占用了相同的堆栈位置,如果忽略了返回值,这表明f()代码被有效地编译,具有消除能力和{{1选项不是禁用优化,而是为了添加悲观化:在调用函数之前为临时函数添加额外的堆栈空间,然后添加额外的代码以从中复制并破坏临时函数然后重新调整堆栈指针。 ...

答案 1 :(得分:6)

返回值的副本由被调用者完成,并且必须在调用析构函数之前完成,否则您将无法返回本地构造变量的值/内容。

以下是标准中的相关部分:第12.4节,第11点(析构函数)

  

隐式调用析构函数

     
      
  • 对于具有自动存储持续时间(3.7.3)的构造对象,当创建对象的块退出时(6.7)
  •   

我试图找到一个地方,它说“回归发生在破坏之前”,但它并没有像我想的那样清楚[除非我遗漏了什么]。