27

我有一个朋友将“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, not 0, 即使AB布局兼容 (7.2p8)。


所以我的进一步问题是(引用一个答案):“具有完全相同布局的两个类可能被认为是“几乎相同”,并且它们被排除在优化之外。. 有人可以为 GCC 或 Clang 提供证明吗?

4

4 回答 4

8

如果您删除B::x,则B满足 9p7 中对标准布局类的要求,并且访问变得完美定义,因为这两种类型是布局兼容的 9.2p17 并且成员都具有相同的类型。


标准布局类是这样的类:

  • 没有非标准布局类(或此类类型的数组)或引用类型的非静态数据成员,
  • 没有虚函数 (10.3) 和虚基类 (10.1),
  • 对所有非静态数据成员具有相同的访问控制(第 11 条),
  • 没有非标准布局的基类,
  • 要么在派生最多的类中没有非静态数据成员,并且最多有一个具有非静态数据成员的基类,要么没有具有非静态数据成员的基类,并且
  • 没有与第一个非静态数据成员相同类型的基类。

如果两个标准布局结构类型具有相同数量的非静态数据成员并且相应的非静态数据成员(按声明顺序)具有布局兼容类型,则它们是布局兼容的。

于 2013-06-19T14:57:02.947 回答
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() 是虚拟的。向 B 添加非静态或 const 成员将导致“10”答案,添加非平凡的构造函数或静态不会。

PS。在第二个例子中

enum A : int { X, Y };
enum B : int { Z };

这两者之间的布局兼容性由 C++14 定义,它们与底层类型不兼容(但可转换)。虽然像

 enum A a = Y;
 enum B b = (B*)a;

可能会产生未定义的行为,就像您尝试使用任意 32 位值映射浮点数一样。

于 2017-02-07T07:00:14.723 回答
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是调用 后的 -object的子对象f())并p指向类型的对象B

如果您标记a为具有static存储持续时间(就像我在上面所做的那样),我测试过的所有编译器都会很高兴地应用严格的别名并优化为 return 10

另一方面,如果您g()使用__attribute__((noinline))或添加h()返回指针的函数a

A* h();
A* h() { return &a; }

我测试过的编译器假设&a并且参数b可以别名并重新加载值。

于 2017-05-03T16:21:59.113 回答
0

我认为您的代码是 UB,因为您正在取消引用来自违反类型别名规则的强制转换的指针。

现在,如果您激活严格别名标志,您将允许编译器优化 UB 的代码。如何使用这个 UB 取决于编译器。你可以看到这个问题的答案。

关于 gcc,-fstrict-aliasing 的文档显示它可以基于以下内容进行优化:

(...) 假定一种类型的对象永远不会与不同类型的对象驻留在同一地址,除非类型几乎相同。

我一直无法找到“几乎相同”的定义,但是具有完全相同布局的两个类可能会被认为“几乎相同”并且它们被排除在优化之外。

于 2017-02-11T01:18:57.763 回答