C++ 右值参数

问题描述

我写了这段代码

#include <iostream>
using namespace std;
class Foo
{

public:
    int a = 0;
    Foo()
    {
        cout << "ctor: " << this << endl;

    }
    ~Foo() {
       cout << "dtor: " << this << endl;
    }
};

Foo f()
{
    Foo foo;
    cout << "f " << &foo << endl;
    return foo;
}
void ff(Foo &&ffoo)
{
    cout << "ff " << &ffoo << endl;

}

int main()
{
    ff(f());
    std::cout << "Hello World!\n";
}

输出看起来不错:

ctor: 0x7ffeda89bd7c
f 0x7ffeda89bd7c
ff 0x7ffeda89bd7c
dtor: 0x7ffeda89bd7c

但是当我像这样删除 ~Foo() 时:

#include <iostream>
using namespace std;
class Foo
{

public:
    int a = 0;
    Foo()
    {
        cout << "ctor: " << this << endl;
    }
    // ~Foo() {
    //    cout << "dtor: " << this << endl;
    // }
};

Foo f()
{
    Foo foo;
    cout << "f " << &foo << endl;
    return foo;
}
void ff(Foo &&ffoo)
{
    cout << "ff " << &ffoo << endl;

}
int main()
{
    ff(f());
    std::cout << "Hello World!\n";
}

我得到了这个输出

ctor: 0x7fffd5c8bf4c
f 0x7fffd5c8bf4c
ff 0x7fffd5c8bf6c
Hello World!

为什么 ffoo 的地址与 f 不同?不应该是一样的吗?

编译cmd为:

g++ tmp.cpp 
g++ --version
g++ (Debian 8.3.0-6) 8.3.0
copyright (C) 2018 Free Software Foundation,Inc.

解决方法

只有在申请 NRVO 时,您才会拥有相同的地址。

但不能保证 NRVO

碰巧的是,随着您的更改,NRVO 在一种情况下发生,而在另一种情况下不会发生,但在两个版本中,NRVO 都可能发生。

,

正如@NateEldredge 在评论中指出的,这个问题与应用的 ABI 相关。我想,在你的情况下,它是 System V ABI

您的类 Foo 的两个版本之间的区别在于它们的琐碎。在第一种情况下,Foo 有一个用户提供的析构函数,因此该类不是微不足道的。在第二种情况下,情况正好相反。

现在,根据 ABI 有什么区别?基本上,它表示在寄存器中的函数之间通过值传递/返回不超过其二进制表示的某种大小的普通类型的对象。 C++ 标准明确允许在 [class.temporary/3]:

当类类型为 X 的对象被传递给函数或从函数返回时,如果 X 至少有一个符合条件的复制或移动构造函数([特殊]),每个这样的构造函数是微不足道的,而 X 的析构函数是微不足道的或被删除的,允许实现创建一个临时对象来保存函数参数或结果对象临时对象分别由函数参数或返回值构造而成,并且函数的参数或返回对象被初始化,就像使用符合条件的平凡构造函数来复制临时对象(即使该构造函数不可访问或不会被重载决议选择来执行对象的复制或移动)。

[注意 4:授予此权限允许将类类型的对象传递给寄存器中的函数或从函数返回。 — 尾注]

这允许对象在其生命周期内仅存储在寄存器中,这样它们就不需要存储在(慢速)内存中。但是,就您而言,由于您正在评估它们的地址,因此它们最终必须存储在内存中。这发生在函数 f 的内部和外部。由于在寄存器中传递返回值,除了将两个对象显式存储在堆栈上之外别无选择,一次在 f 的堆栈帧中,一次在调用函数的堆栈帧中。这意味着两个对象具有不同的地址。

现在,当您提供自定义析构函数时,类型不再是微不足道的。根据 ABI,它现在需要在内存中传递。对于按值返回,它的工作原理是调用者在其堆栈上为返回值分配存储,并将其地址作为隐藏参数传递;引用 ABI 文档:

如果类型具有 MEMORY 类,则调用者为返回值提供空间并将此存储的地址传递到 %rdi 中,就好像它是函数的第一个参数一样。实际上,这个地址变成了“隐藏的”第一个参数。

这意味着该函数在内部可以访问此存储。现在,它有两个选择。在没有优化的情况下,该函数可以首先在其堆栈帧中创建一个单独的对象,最后将其复制/移动构造到 rdi 指向的存储中。第二种选择是直接使用这个存储,这样可以省略复制/移动,这种优化被称为 (N)RVO。

请注意,一旦您将类设置得足够大,使其二进制表示不再适合寄存器,那么使用类的普通版本也将获得相同的结果。

TL;DR 两个版本的类之间的区别在于其简单性。当类很琐碎(并且足够小)时,ABI 规定它在寄存器中(而不是在内存中)传递。传入寄存器时,函数内部和外部的对象存储在不同的堆栈帧中,因此具有不同的地址。相反,当传入内存时,函数内部和外部都使用单个存储(分配在调用者的堆栈帧中)。