C++ 标准是否保证函数 return 值具有常量地址?

Does the C++ standard guarantee that a function return value has a constant address?

考虑这个程序:

#include <stdio.h>
struct S {
  S() { print(); }
  void print() { printf("%p\n", (void *) this); }
};
S f() { return {}; }
int main() { f().print(); }

据我所知,这里只构造了一个 S 对象。没有发生复制省略:首先没有要删除的副本,事实上,如果我明确删除副本 and/or 移动构造函数,编译器将继续接受该程序。

但是,我看到打印了两个不同的指针值。发生这种情况是因为我平台的 ABI returns 可简单复制类型,例如 CPU 中的这个类型,因此无法使用该 ABI 避免复制。即使在完全优化函数调用时,clang 也会保留此行为。如果我给 S 一个非平凡的复制构造函数,即使它不可访问,那么我确实会看到相同的值打印两次。

print() 的初始调用发生在构造期间,即在对象生命周期开始之前,但在构造函数中使用 this 通常是有效的,只要它不在一种要求构造完成的方式——例如,不强制转换为派生 class——据我所知,打印或存储其值不需要构造完成。

标准是否允许此程序打印两个不同的指针值?

注意:我知道标准允许此程序打印同一指针值的两种不同表示形式,从技术上讲,我没有排除这种可能性。我可以创建一个不同的程序来避免比较指针表示,但这样会更难理解,所以我想尽可能避免这种情况。

这不是答案,而是 关于 g++clang[=30 的不同行为的注释 =] 在这种情况下,取决于 -O 优化标志。

考虑以下代码:

#include <stdio.h>
struct S {
    int i;
    S(int _i): i(_i) { 
        int* p = print("from ctor");
        printf("about to put 5 in %p\n", (void *)&i);        
        *p = 5;
    }
    int* print(const char* s) { 
        printf("%s: %p %d %p\n", s, (void *) this, i, (void *)&i);
        return &i;
    }
};
S f() { return {3}; }
int main() { 
    f().print("from main");
}

我们可以看到 clang (3.8) 和 g++ (6.1) 的处理方式略有不同,但都得到了正确的答案。

clang(无 -O、-O1、-O2)和 g++(无 -O、-O1)

from ctor: 0x7fff9d5e86b8 3 0x7fff9d5e86b8
about to put 5 in 0x7fff9d5e86b8
from main: 0x7fff9d5e86b0 5 0x7fff9d5e86b0

g++(对于-O2)

from ctor: 0x7fff52a36010 3 0x7fff52a36010
about to put 5 in 0x7fff52a36010
from main: 0x7fff52a36010 5 0x7fff52a36010

似乎他们在这两种情况下都做对了——当他们决定跳过寄存器优化(g++ -O2)和当他们进行寄存器优化但按时将值复制到实际的 i (所有其他情况)。

T.C。在评论中指出这是标准的缺陷。是 core language issue 1590。这是一个与我的示例略有不同的问题,但根本原因相同:

Some ABIs require that an object of certain class types be passed in a register [...]. The Standard should be changed to permit this usage.

current suggested wording 将通过向标准添加新规则来解决此问题:

When an object of class type X is passed to or returned from a function, if each copy constructor, move constructor, and destructor of X is either trivial or deleted, and X has at least one non-deleted copy or move constructor, implementations are permitted to create a temporary object to hold the function parameter or result object. [...]

在大多数情况下,这将允许当前的 GCC/clang 行为。

有一个小的极端情况:目前,当一个类型只有一个删除的复制或移动构造函数时,如果默认,该构造函数将是微不足道的,根据标准的当前规则,如果删除该构造函数,该构造函数仍然是微不足道的:

12.8 Copying and moving class objects [class.copy]

12 A copy/move constructor for class X is trivial if it is not user-provided [...]

删除的复制构造函数不是用户提供的,下面的任何内容都不会使这样的复制构造函数变得不平凡。因此,按照标准的规定,这样的构造函数是微不足道的,并且 as specified by my platform's ABI,由于微不足道的构造函数,GCC 和 clang 在这种情况下也会创建一个额外的副本。我的测试程序的一行添加说明了这一点:

#include <stdio.h>
struct S {
  S() { print(); }
  S(const S &) = delete;
  void print() { printf("%p\n", (void *) this); }
};
S f() { return {}; }
int main() { f().print(); }

这会使用 GCC 和 clang 打印两个不同的地址,即使提议的解决方案也需要将相同的地址打印两次。这似乎表明,虽然我们将对标准进行更新以不再需要完全不兼容的 ABI,但我们仍需要对 ABI 进行更新以以与标准要求兼容的方式处理极端情况。