这个黑科技能够消除别名警告,是否会产生未定义行为?

10

我们刚刚将编译器升级到gcc 4.6,现在我们遇到了一些警告。目前我们的代码库还没有达到可以用c++0x编译的状态,而且无论如何,我们也不想在生产环境中运行它(至少还没有),因此我需要一个解决方法来消除这个警告。

这些警告通常是因为类似于以下情况而出现的:

struct SomeDataPage
{
  // members
  char vData[SOME_SIZE];
};

后来,它被以下方式使用

SomeDataPage page;
new(page.vData) SomeType(); // non-trivial constructor

为了读取、更新和返回以下转换,以前使用过以下方式

reinterpret_cast<SomeType*>(page.vData)->some_member();

在4.4版本中这是可以的,在4.6版本中上述代码会生成以下警告:

warning: type punned pointer will break strict-aliasing rules

现在,消除此错误的一种干净方法是使用union。然而,正如我所说,我们不能使用c++0x(因此无限制联合),因此我采用了下面这个可怕的Hack - 现在警告已经消失了,但我有可能会调用鼻子守护程序吗?

static_cast<SomeType*>(reinterpret_cast<void*>(page.vData))->some_member();

这似乎可以正常工作(请参见此处的简单示例:http://www.ideone.com/9p3MS),并且不会产生警告,那么在C++0x之前使用这种方法是否可以(并非从风格角度考虑)?

注意:通常我不想使用-fno-strict-aliasing...

编辑:看来我错了,4.4版也有同样的警告,我猜我们最近才发现了这个问题的变化(这始终不太可能是编译器的问题),但问题仍然存在。

编辑:进一步的调查得出了一些有趣的信息,似乎将转换和调用成员函数放在同一行中导致了警告,如果将代码拆分为以下两行,则没有警告:

SomeType* ptr = reinterpret_cast<SomeType*>(page.vData);
ptr->some_method();

实际上,这并不会产生警告。因此,在ideone上的我的简单示例有缺陷,更重要的是我上面的hack无法修复这个警告,唯一的解决方法是将函数调用与强制转换分开 - 然后可以将强制转换保留为reinterpret_cast


1
我认为在你执行了new(page.vData) SomeType()之后,位于与page.vData相同位置的对象的实际类型是SomeType,因为这是存储在那里的最后一件事情,因此类型游戏是合法的。尽管转换触发的警告可能不确定您是否已经完成了放置新操作。如果我的判断没有错的话,那么假设GCC中没有错误,那么可怕的黑客应该也没问题(因为当然GCC对reinterpret_cast的定义使得结果指针相同)。不过我不确定。 - Steve Jessop
“这实际上不会生成警告。因此,应该选择哪些选项?” - curiousguy
@Nim 你可能需要尝试一些优化后再次尝试。 - curiousguy
@curiousguy,"-O3",我没有看到警告... - Nim
@Nim 这并不是绝对的,但通常来说,更多的优化意味着更多的代码分析,也意味着更多的警告。 - curiousguy
显示剩余2条评论
4个回答

2
SomeDataPage page;
new(page.vData) SomeType(); // non-trivial constructor
reinterpret_cast<SomeType*>(page.vData)->some_member();

这在4.4版本是可以的;但在4.6版本中,上述内容会引发以下结果:

warning: type punned pointer will break strict-aliasing rules

你可以尝试以下方法:
SomeDataPage page;
SomeType *data = new(page.vData) SomeType(); // non-trivial constructor
data->some_member();

谢谢你的回答,我想我已经更新了问题,我能避免警告的唯一方法是拆分转换(不幸的是new操作发生在初始化时,而对some_member()的调用发生在正常运行期间)-所以现在有一个reinterpret_cast,就像我在编辑中添加的那样。这是我能够消除警告的唯一方法。我本应该把这个作为答案添加上去,但一直没有时间... - Nim
1
@Nim 这里的问题在于,通过分解操作,你并没有解决潜在的问题(如果真的存在潜在的问题),你只是让编译器更难发出警告。 - curiousguy
据我所知,没有潜在的问题,我正在使用放置 new 来构造对象;在我看来,这是一个虚假的警告,在这种特定情况下需要被消除,而不需要通常关闭此类警告的开关。 - Nim
@Nim 我的理解也是这是一个虚假警告;我对严格别名规则的问题在于规则不够清晰,特别是对于编译器的作者。 - curiousguy

1
为什么不使用:
SomeType *item = new (page.vData) SomeType();

然后:

item->some_member ();

我认为联合体不是最好的选择,它可能会带来问题。从gcc文档中可以看到:

`-fstrict-aliasing'
 Allows the compiler to assume the strictest aliasing rules
 applicable to the language being compiled.  For C (and C++), this
 activates optimizations based on the type of expressions.  In
 particular, an object of one type is assumed never to reside at
 the same address as an object of a different type, unless the
 types are almost the same.  For example, an `unsigned int' can
 alias an `int', but not a `void*' or a `double'.  A character type
 may alias any other type.

 Pay special attention to code like this:
      union a_union {
        int i;
        double d;
      };

      int f() {
        a_union t;
        t.d = 3.0;
        return t.i;
      }
 The practice of reading from a different union member than the one
 most recently written to (called "type-punning") is common.  Even
 with `-fstrict-aliasing', type-punning is allowed, provided the
 memory is accessed through the union type.  So, the code above
 will work as expected.  However, this code might not:
      int f() {
        a_union t;
        int* ip;
        t.d = 3.0;
        ip = &t.i;
        return *ip;
      }

如何与您的问题相关可能很难确定。我猜编译器没有将SomeType中的数据视为与vData中的数据相同。


@Nim:有什么理由不这样做吗?也许可以添加一个虚拟构造函数,它不会覆盖“vData”,当需要使用它时,只需在原地新建即可。 - Skizz
有一些我没有提到的复杂情况,SomeType 实际上是一个变体,并且我想利用分配给变体持有的相同类型比构造默认值(然后重新构造新类型)更便宜的事实。 - Nim

0
struct SomeDataPage
{
  // members
  char vData[SOME_SIZE];
};

这对于别名/对齐原因是有问题的。首先,这个结构体的对齐方式不一定与您试图放置在其中的类型相同。您可以尝试使用GCC属性来强制执行特定的对齐方式:
struct SomeDataPage { char vData[SOME_SIZE] __attribute__((aligned(16))); };

对于我遇到的任何情况,16字节对齐应该足够了。然而,编译器仍然不会喜欢你的代码,但如果对齐良好,它就不会崩溃。或者,您可以使用新的C++0x alignof/alignas。

template <class T>
struct DataPage {
   alignof(T) char vData[sizeof(T)];
};

很遗憾,我无法在这里使用模板,因为我们使用一种机制来扩展“SomeType”,以便与旧版本保持向后兼容性 - 因此,所有版本都必须具有足够的存储空间来应对“SomeType”的潜在更改... - Nim

0

说实话,我更担心SOME_SIZE不够大。但是,使用char*别名任何类型是合法的。所以,只需执行reinterpret_cast<T*>(&page.vData[0])就可以了。

此外,我会质疑这种设计。除非你正在实现boost::variant或类似的东西,否则没有太多理由使用它。


2
使用char*作为任何类型的别名是合法的,但是将char*与任何类型进行别名可能不合法。在对象的实际类型和用于访问它的类型之间,类型转换规则并不对称。 - Steve Jessop
尽管类型别名规则通常不对称,但它们在与char相关的情况下是对称的 - 你可以用char给任何类型起别名,也可以用任何类型给char起别名。由于这段代码是与char进行别名处理的,编译器的警告是错误的。 - Chris Dodd
@Chris:请检查3.10/15。它并没有说一个动态类型为char的对象可以通过任何类型的lvalue访问。一些SomeType的情况是可以的,但是唯一符合条件的UDT是具有charsigned charunsigned char成员或这些成员的cv限定版本的聚合体或联合体。SomeType很可能不是这样的聚合体或联合体。没有对称性。因此,由于放置new,只有对象的动态类型是SomeType,所以这个别名才是合法的。如果标准中还有其他允许别名的地方,我可能错过了。 - Steve Jessop
@SteveJessop:我认为这是标准中的一个漏洞。必须有一种方法来拥有一个特定字节数的未类型化内存块。如果没有,那么就需要创建一种批准的方法来实现这个。如果没有其他办法,这将使在C++中实现分配器变得几乎不可能。 - Omnifarious
@Omnifarous:这里有一个尴尬之处-放置new 让你有一种方法可以将原始内存(char)转换为你的 SomeType 类型的对象,而无需将其作为其他左值进行引用,因为实现会处理它。显然,这不是 C 解决问题的方式,POD 类型具有一些关于生命周期的规则以及您可以通过“重用”它们的内存来结束它的事实。“重用”可能以某种方式与别名规则互动,我记不清了。如果其中任何一点意味着3.10/15中未说明的对称性,则请见谅。 - Steve Jessop
@SteveJessop 这是一个混乱、悲伤、令人恐惧的局面:有一些不同的别名规则解释,不仅仅是一些随意的C++程序员,还有编译器实现者(至少GNU人员)。而其中一种解释,也是最严格的一种,意味着你不能在C/C++中使用自定义分配器,就此结束。第二种不那么严格的解释意味着**你不能在C中使用自定义分配器,但可以使用placement-new。请注意,ISO C标准似乎具有非常不同的别名语义(在我看来,C委员会在别名方面变得疯狂了)。 - curiousguy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接