在本讨论中,我假设标量对象是int、float、chars、bools和指针。非标量对象是由标量类型和递归聚合组成的聚合(structs(。
给定这个假设,C++程序访问聚合是否与它们的标量组件不同?
例如:
struct s { int a; float b; };
void assign1(s& out, s const& in) { out = in; }
void assign2(s& out, s const& in) { out.a = in.a; out.b = in.b; }
显然,assign1
和assign2
在实践中是等效的,并且都访问int s::a
和float s::b
。但是它们中的任何一个也在任何意义上访问整个聚合吗?
只有标量对象被实际访问的解释产生了有趣的结果。
例如,根据我在这里的另一个问题的解决方案,形成对对象的引用并不构成访问。给定该分辨率,我可以编写这样的函数:
void assign3(s& out, s const& in) {
int& a_out = out.a; // no access
int const& a_in = in.a; // no access
a_out = a_in; // access some ints
}
否";访问";除了在第三行发生,第三行访问一些int。out
和in
是否真的引用了s
类型的对象是无关紧要的。只有a_out
和a_in
必须实际引用int。
考虑到这一点,并且对象的地址是其第一个非静态数据成员的地址,我有权编写
int out, const in = 42;
assign3(reinterpret_cast<s&>(out), reinterpret_cast<s const&>(in));
如果所有这些假设都成立,那么C和C++在很大程度上只是可移植的汇编语言,而别名规则只是帮助编译器正确地从x87浮点协处理器中读取寄存器。
当然,这些假设并不成立。我错了。但是为什么我错了?为什么标准文档有关于有效类型或动态类型的所有这些规则?
给定struct a { int a; }; struct b { int b; };
,除了在涉及工会的某些有限情况下,通过未定义的b::b
访问a::a
有什么好处?
对编译器来说重要的一件事是从内存中重新加载寄存器。这需要时间,最好避免。所以,如果你知道在地址p
,一个包含float[2]
的Foo
结构存在,你在寄存器中有第一个浮点,然后你必须写入地址q
的浮点,你需要重新加载第一个浮点吗?如果你不能证明p!=q
,这可能是必要的。但是,如果您知道地址q
处的浮点是Bar
的一部分,因此后面跟着int
,那么这就是p!=q
的证明。因此,对q
的写入不会强制从p
重新加载。
注意,这里不读取或写入p
和q
之后的数据。只有这些类型不同的事实才允许编译器优化通过p
的冗余读取。