I have a case朋友将类型为“Base”的非基类对象强制转换为类型对象“Derived”,其中“Derived”是“Base”的派生类,只添加函数,但是没有数据。在下面的代码中,我 将数据成员x
添加到派生类
struct A {
int a;
};
struct B : A {
// int x;
int x;
};
A a;
int g(B *b) {
a.a = 10;
b->a++;
return a.a;
}
通过严格的别名分析,GCC(也是Clang)总是返回10
而不是11
,因为b
永远不会指向明确定义的a
码。但是,如果我删除B::x
(就像我朋友的代码中的情况一样),GCC的输出汇编代码不优化a.a
的返回访问权限并重新加载来自记忆的价值。所以我的朋友的代码调用g
“在GCC上工作”(正如他的意图),即使我认为它仍然有未定义的行为
g((B*)&a);
所以在基本相同的两种情况下,GCC优化了一种情况并且没有优化其他情况。是因为b
可以合法地点到a
?或者是因为GCC只是想破坏现实世界的代码?
我测试了陈述
的答案如果删除B :: x,那么B符合9p7中对标准布局类的要求,并且访问变得非常明确,因为这两种类型是布局兼容的,9.2p17。
使用两个布局兼容的枚举
enum A : int { X, Y };
enum B : int { Z };
A a;
int g(B *b) {
a = Y;
*b = Z;
return a;
}
g
的汇编程序输出返回1
,而不是0
,即使A
和B
布局兼容(7.2p8)。
所以我的进一步问题是(引用答案):“两个具有完全相同布局的类可能被认为”几乎相同“并且它们被排除在优化之外。” 。 有人可以为GCC或Clang提供此证明吗?
答案 0 :(得分:8)
如果删除B::x
,则B
符合9p7中标准布局类的要求,并且访问权限变得非常明确,因为这两种类型< em> layout-compatible ,9.2p17和成员都有相同的类型。
标准布局类是一个类:
- 没有非标准布局类(或此类类型的数组)或引用类型的非静态数据成员,
- 没有虚函数(10.3),没有虚基类(10.1),
- 对所有非静态数据成员具有相同的访问控制(第11条),
- 没有非标准布局基类
- 在大多数派生类中没有非静态数据成员,并且最多只有一个具有非静态数据成员的基类,或者没有包含非静态数据成员的基类,并且< / LI>
- 没有与第一个非静态数据成员相同类型的基类。
如果两个标准布局结构类型具有相同数量的非静态数据成员,并且相应的非静态数据成员(按声明顺序)具有布局兼容类型,则它们是布局兼容的。
答案 1 :(得分:6)
未定义的行为包括 工作的情况,即使它不应该。
根据此联合的标准用法,允许访问标头或数据成员的类型和大小字段:
union Packet {
struct Header {
short type;
short size;
} header;
struct Data {
short type;
short size;
unsigned char data[MAX_DATA_SIZE];
} data;
}
这严格限于工会,但许多编译器支持这种扩展,只要“不完整”类型将以未定义大小的数组结束。如果从子类中删除额外的静态非成员,它确实变得微不足道并且布局兼容,这允许别名?
struct A {
int a;
};
struct B {
int a;
//int x;
};
A a;
int g(B *b) {
a.a = 10;
b->a++;
return a.a;
}
仍然执行别名优化。在具有相同数量的非静态成员的情况下,假定最派生的类与基类相同。让我们颠倒顺序:
#include <vector>
#include <iostream>
struct A {
int a;
};
struct B : A {
int x;
};
B a;
int g(A *b) {
a.a = 10;
b->a++;
return a.a;
}
int main()
{
std::cout << g((A*)&a);
}
这会按预期返回11,因为B显然也是A,与原始尝试不同。让我们进一步发挥
struct A {
int a;
};
struct B : A {
int foo() { return a;}
};
除非foo()是虚拟的,否则不会导致别名优化。将非静态或const成员添加到B将导致“10”答案,添加非平凡构造函数或静态不会。
PS。 在第二个例子中
enum A : int { X, Y };
enum B : int { Z };
这两者之间的布局兼容性由C ++ 14定义,并且它们与底层类型(但可转换)不兼容。虽然像
之类的东西 enum A a = Y;
enum B b = (B*)a;
可能会产生未定义的行为,就像您尝试使用任意32位值映射float一样。
答案 2 :(得分:0)
我认为您的代码是UB,因为您要取消引用来自违反type aliasing rules的转换的指针。
现在,如果激活严格别名标志,则允许编译器优化UB的代码。如何使用此UB取决于编译器。您可以看到this question的答案。
关于gcc,documentation for -fstrict-aliasing显示它可以根据以下内容进行优化:
(...)假设一种类型的对象永远不会驻留在同一个对象上 地址作为不同类型的对象,除非类型几乎 同样的。
我无法找到&#34;几乎相同&#34;的定义,但可以考虑具有完全相同布局的两个类&#34;几乎相同&#34;并且它们被排除在优化之外。
答案 3 :(得分:0)
我相信以下是合法的C ++(不调用UB):
#include <new>
struct A {
int a;
};
struct B : A {
// int x;
};
static A a;
int g(B *b);
int g(B *b) {
a.a = 10;
b->a++;
return a.a;
}
int f();
int f() {
auto p = new (&a) B{};
return g(p);
}
因为(全局)a
总是引用A
类型的对象(即使它是B
- 对象的子对象,在调用{{1}之后}}和f()
指向p
类型的对象。
如果您将B
标记为a
存储持续时间(就像我上面所做的那样),我测试的所有编译器都会很乐意应用严格的别名并优化以返回static
。
另一方面,如果您使用10
标记g()
或添加返回指向__attribute__((noinline))
h()
a
我测试的编译器假设A* h();
A* h() { return &a; }
和参数&a
可以别名并重新加载值。