【问题标题】:Struct offsets and pointer safety in C++C ++中的结构偏移和指针安全
【发布时间】:2018-02-17 19:35:30
【问题描述】:

这个问题是关于使用带有结构偏移的指针算术派生的指针。

考虑以下程序:

#include <cstddef>
#include <iostream>
#include <new>

struct A {
  float a;
  double b;
  int c;
};

static constexpr auto off_c = offsetof(A, c);

int main() {
  A * a = new A{0.0f, 0.0, 5};
  char * a_storage = reinterpret_cast<char *>(a);
  int * c = reinterpret_cast<int *>(a_storage + off_c));

  std::cout << *c << std::endl;

  delete a;
}

这个程序似乎可以工作,并在我测试的编译器上给出了预期的结果,使用默认设置和 C++11 标准。

(一个密切相关的程序,我们使用void *而不是char *static_cast而不是reinterpret_cast, 不被普遍接受。 gcc 5.4 发出关于 void 指针的指针运算的警告,clang 6.0 表示该指针 void * 的算术运算是错误的。)

该程序是否具有符合 C++ 标准的明确定义的行为?

答案是否取决于实现是否具有宽松的或严格的指针安全性 ([basic.stc.dynamic.safety])?

【问题讨论】:

  • 这是家庭作业吗?
  • @GarrGodfrey:我会感到惊讶...
  • 为什么不使用指向 C++ 成员功能的指针。它是offsetof 的“干净”替代品?
  • 不,我毕业了,我没有更多的家庭作业了 :) 我想我确实以家庭作业的方式提出了这个问题。有时当你问标准问题时,最好用一个特定的代码示例提出一个开放式问题,而不是开始解释你认为你知道的东西,以我有限的经验
  • 不要对 void* 进行指针运算。任何T* 的指针运算都需要sizeof(T),但void 没有大小。

标签: c++ c++11 pointers language-lawyer offsetof


【解决方案1】:

您的代码中没有基本错误。

如果A 不是普通的旧数据,则上面是UB(C++17 之前)和有条件支持(C++17 之后)。

您可能想用auto* 替换char*int*,但这是一种风格。

请注意,指向成员的指针以类型安全的方式执行完全相同的操作。大多数编译器都实现了指向成员的指针 ... 作为类型中成员的偏移量。但是,它们确实可以在任何地方工作,即使是在非 pod 结构上也是如此。

除此之外:我看不到标准中offsetofconstexpr 的保证。 ;)

无论如何,替换:

static constexpr auto off_c = offsetof(A, c);

static constexpr auto off_c = &A::c;

  auto* a_storage = static_cast<char *>(a);
  auto* c = reinterpret_cast<int *>(a_storage + off_c));

  auto* c = &(a->*off_c);

以 C++ 方式进行。

【讨论】:

  • 如果您在非 POD 类型上使用 offsetof,Clang 会对您大喊大叫,所以这是一个很好的提示,表明您所做的事情至少有点接近正确:godbolt.org/g/6jLnM6 - offset of on non -standard-layout type 'A' [-Winvalid-offsetof]
  • 这就是我关心的问题:在 3.7.4.3 [basic.stc.dynamic.safety] 中,它说只有在(条件)并且我们有严格的指针安全性时指针才是安全派生的,那么指针是无效的如果它不是来自这样的地方。在 5.7 指针算术中,它说我可以在数组中进行通常的算术运算,但我没有看到任何东西告诉我 struct offset 算术是可以的。我试图弄清楚这是否与我认为的方式无关,或者结构偏移算术在假设的“严格”实现中是否不合适,或者我读错了 5.7 (n4296)
  • 基本上我认为这个问题的答案在宽松的实现中是可以的,在严格的实现中具有未定义的行为。我认为当我向不指向数组的指针添加索引时,这在严格的 impl 中自动不安全,并且结果不是安全派生的 ptr。它还说取消引用无效的 ptr 是 ub。我认为这一定是一个起草错误。但我可能完全错了:)
  • @ChrisBeck 我相信 [basic.stc.dynamic.safety] 的目的是为允许垃圾收集器做出的假设定义规则。基本上,您不能混淆对对象的引用并期望它们不会被垃圾收集。在您的情况下,您仍然保留对对象(“a”)的引用
【解决方案2】:

在您的具体示例中是安全的,但这只是因为您的结构是标准布局,您可以使用std::is_standard_layout&lt;&gt; 进行仔细检查。

尝试将其应用于以下结构:

struct A {
  float a;
  double b;
  int c;
  std::string str;
};

将是非法的,即使字符串超出了结构的相关部分。

编辑

这是我关心的问题:在 3.7.4.3 [basic.stc.dynamic.safety] 中,它说只有在(条件)并且如果我们有严格的指针安全性并且指针不是来自时,指针是安全派生的这样的地方。在 5.7 指针算术中,它说我可以在数组中进行通常的算术运算,但我没有看到任何东西告诉我结构偏移算术是可以的。我试图弄清楚这是否与我认为的方式无关,或者结构偏移算术在假设的“严格”实现中是否不合适,或者我是否读错了 5.7 (n4296)

当你在做指针算术时,你是在一个char的数组上执行它,它的大小至少是sizeof(A),所以没关系。

然后,当您重新转换为第二个成员时,您将被 (2.4) 覆盖:

——安全派生指针值的定义明确的指针转换(4.10、5.4)的结果;

【讨论】:

  • 好的,我添加到的指针的类型为char *,但在 5.7.4 中有关数组算术的块中,它说,“如果指针指向数组对象的元素.. ." 虽然在标准意义上,这里没有数组对象。只有一个 A。这也与严格的别名问题有关:对象仅在由语言创建时才存在,而不是通过强制转换而存在。所以我不确定这算作数组中的安全指针操作。我认为从标准的角度来看,这里没有数组。 Idk 当然。
【解决方案3】:

你应该检查你的假设。

假设 #1) offsetof 给出了正确的字节偏移量。这只有在类被认为是“标准布局”时才能保证,它有许多限制,例如没有虚拟方法,避免多重继承等。在这种情况下,应该没问题,但一般你不能确定。

假设 #2) 一个字符的大小与一个字节的大小相同。在 C 中,这是根据定义,所以你是安全的。

假设 #3) offsetof 给出了从指针到类的正确偏移量,而不是从数据的开头。这与 #1 基本相同,但 vtable 肯定是个问题。同样,仅适用于标准布局。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-09
  • 1970-01-01
相关资源
最近更新 更多