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 进行更新以以与标准要求兼容的方式处理极端情况。
考虑这个程序:
#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 ofX
is either trivial or deleted, andX
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 进行更新以以与标准要求兼容的方式处理极端情况。