【问题标题】:C++ rvalue parameterC++ 右值参数
【发布时间】:2021-04-02 04:21:09
【问题描述】:

我写了这段代码:

#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.

【问题讨论】:

  • AFAIK 这些地址是虚拟的,用户程序无法控制它......你控制你拥有的东西,新编译的目标文件可能/可能不会改变地址
  • 看起来问题在于,有一个析构函数使类 non-trivial for the purposes of calls 符合 ABI 的定义,因此它必须在内存中传递,并且始终使用相同的副本。没有析构函数,因为它只有 4 个字节,所以可以在寄存器中传递,ff() 必须将其溢出到堆栈中(必须在与前一个副本不同的地址)。

标签: c++ rvalue


【解决方案1】:

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

但不保证NRVO

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

【讨论】:

    【解决方案2】:

    正如@NateEldredge 在 cmets 中指出的那样,这个问题与应用的 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 规定它在寄存器中的传递(而不是在内存中)。传入寄存器时,函数内外的对象存储在不同的堆栈帧中,因此具有不同的地址。相反,当传入内存时,函数内部和外部都使用单个存储(分配在调用者的堆栈帧中)。

    【讨论】:

      猜你喜欢
      • 2015-08-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多