【问题标题】:How to access an object's storage through an aggregate如何通过聚合访问对象的存储
【发布时间】:2013-11-28 21:58:03
【问题描述】:

在“Lvalues and rvalues”, [basic.lval] (3.10) 中,C++ 标准包含一个类型列表,因此通过这种类型的左值“访问对象的存储值”是有效的 (第 10 段)。具体来说,它说:

如果程序尝试通过以下类型之一以外的左值访问对象的存储值,则行为未定义:

  • 对象的动态类型,

  • [关于简历和签名/未签名的一些不重要的细节]

  • 一种聚合或联合类型,在其元素或非静态数据成员(递归地包括子聚合或包含联合的元素或非静态数据成员)中包含上述类型之一,

  • [还有一些东西]

“聚合”规则究竟是什么意思?如何通过一些通用聚合类型的 glvalue 访问对象的存储值?!

我正在想象这样的事情:

int a = 10;                                      // my "stored value"

struct Foo { char x; float y; int z; bool w; };  // an aggregate

reinterpret_cast<Foo&>(a).y = 0;                 // ???

最终的转换是否会产生“包含a 的动态类型的聚合类型”的左值,从而使其有效?

【问题讨论】:

  • 我可能错了,但我认为上面给出的规则是避免 UB 的 必要 条件,而不是 足够 条件。您在上面所做的几乎肯定会违反其他规则,UB 也是如此。 :-)
  • @templatetypedef:可能,但我看不到其他规则尚未涵盖的任何内容...
  • 这可能是指 [class.mem]/18 吗?在 C++03 中,这仍然是根据 POD 定义的,其中 POD 是一个聚合。

标签: c++ language-lawyer lvalue


【解决方案1】:

该列表的目的不是为您提供访问对象的替代方法,而是如列表脚注所示,列出对象可能被别名的所有方式。考虑以下示例:

struct foo
{
    char x; 
    float y; 
    int z; 
    bool w;
};

void func( foo &F, int &I, double &D )
{
    //...
}

该列表的意思是访问F 也可能访问与访问I 相同的底层对象。如果您为I 传递了对F.z 的引用,则可能会发生这种情况,如下所示:

func(F, F.z, D); 

另一方面,您可以放心地假设对F 的访问不会访问与D 相同的底层对象,因为struct foo 不包含double 类型的任何成员。

即使某些小丑这样做也是如此:

union onion
{
    struct foo F;
    double D;
};

onion o; 
int i;

func( o.F, i, o.D );  // [class.union] (9.5) wants a word with you.  UB.

我不确定union 是否是您问题的核心。但是union 示例之前的部分强调了聚合规则存在的原因。

现在让我们考虑您的示例:reinterpret_cast&lt;Foo&amp;&gt;(a).y = 0; [expr.reinterpret.cast] (5.2.10),第 11 段有这样的说法:

T1 类型的左值表达式可以转换为“reference to T2”如果类型为“指向T1”的表达式可以显式 使用reinterpret_cast 转换为“指向T2 的指针”类型。那 也就是说,引用转换 reinterpret_cast&lt;T&amp;&gt;(x) 具有相同的效果 转换*reinterpret_cast&lt;T*&gt;(&amp;x) 与内置&amp; 和* 运算符(reinterpret_cast&lt;T&amp;&amp;&gt;(x) 也是如此)。结果 引用与源左值相同的对象,但具有不同的 类型。结果是左值引用类型的左值或 对函数类型的右值引用和右值的 xvalue 引用对象类型。没有暂时的 创建,不复制,构造函数 (12.1) 或转换函数 (12.3) 未被调用。71


71 这有时被称为双关语。

在您的示例的上下文中,如果将指向int 的指针转换为指向Foo 的指针是合法的,那么您的reinterpret_cast&lt;Foo&amp;)(a) 是合法的并产生一个左值。 (第 1 段告诉我们它将是一个左值。)而且,当我读到它时,根据第 7 段,指针转换本身是可以的:

指向对象的指针可以显式转换为指向对象的指针 不同的对象类型。当类型为“指向T1”的prvalue v 是 转换为“指向 cv T2 的指针”类型,如果 T1 和 T2 都是标准布局类型 (3.9) 和对齐方式,则结果为 static_cast<cv T2*>(static_cast<cv void*>(v)) T2 的要求并不比T1 的要求严格。将类型为“pointer to T1”的纯右值转换为 键入“指向T2 的指针”(其中T1 和T2 是对象类型,T2 的对齐要求不是 比T1) 更严格,并返回其原始类型会产生原始指针值。任何结果 其他此类指针转换未指定。

您拥有具有兼容对齐约束的标准布局类型。因此,您所拥有的是一种产生左值的双关语类型。您列出的规则本身并没有使其未定义。

那么什么可能使它未定义?好吧,首先,[class.mem] (9.2) 第 21 段提醒我们,指向标准布局结构对象的指针指向其初始成员,反之亦然。因此,在输入双关语之后,您将得到对Foo 的引用,这样Foo 的x 与a 位于同一位置。

而且...这就是我的语言律师逐渐消失的地方。我凭直觉知道,通过该 franken-reference 访问 Foo 是在 best 实现定义或未指定。我找不到它被明确地放逐到 undefined 行为领域的地方。

但是,我想我回答了您最初的问题:为什么存在聚合规则?它为您提供了一种非常基本的方法来判断潜在的别名,而无需进一步的指针分析。

【讨论】:

  • 非常感谢您的详细处理!但是有一个问题:如果我在第一个 foo 中添加了一个虚函数(因此使它不是一个聚合),那将如何改变参数?
  • @KerrekSB:嗯,在这一点上,它不再是一个标准的布局类。 5.2.10 第 7 段的最后一句话似乎适用:“任何其他此类指针转换的结果都是未指定的。”此外,您在 9.2 中失去了指向类的指针也是指向其第一个成员的指针的保证。 (很可能,vtbl 指针位于顶部,对您隐藏。)您仍然可以保证指向 Foo 的指针指向它的第一个字节。
  • 是的,我明白,但这是否意味着func(F, F.z, D); 不再访问F?
  • @KerrekSB:好的,仔细分析那段,我倾向于“Foo的引用/指针”永远不会被解释为潜在的别名“@987654373的引用/指针” @“ 在这种情况下。我猜聚合规则是用来桥回 C 的。也就是说,在研究这个问题时,我发现很多地方(包括 IBM 的 XL 编译器文档)将所有类、结构和联合集中在“聚合”下横幅。
  • @KerrekSB:从字里行间解读——open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#584——他们似乎非常谨慎地选择将非聚合类排除在您在 3.10 中突出显示的语句之外。请注意他们如何有目的地保留非聚合工会,特别是。或者也许我对那个裁决读得太多了?
【解决方案2】:

子句的 item 只是指对任何聚合(struct、class 或数组)或union 的成员的正常访问:您需要能够访问对象的存储值而不会导致未定义的行为。该子句仅说明必要条件:至少其中一项必须为真。它没有说明充分条件,即除了这些条件之外,其他条件也可能需要成立。

【讨论】:

  • 但是如果我只有Foo b,那么b.y 将只是一个普通的int 类型的左值并且被第一条规则覆盖,那么为什么我们需要这个额外的规则呢?
  • 如果你的glvalue是Foo类型,第一项涵盖了对Foo类型实体的访问(“对象的动态类型”和“对象”指的是glvalue),即,整个对象。额外的子句声明您还可以访问子对象。
  • 那为什么说“聚合或联合”?您如何访问非聚合成员?
  • 应该使用什么?例如,“成员”不起作用,因为它不适用于数组(它们没有成员,只有元素)。 ...并且union 不是聚合,但您仍然应该能够访问当前存储在union 中的对象。
  • 我的意思是,非聚合类的成员呢?
猜你喜欢
  • 1970-01-01
  • 2018-10-27
  • 2016-04-29
  • 2021-08-05
  • 1970-01-01
  • 2019-10-31
  • 2017-06-08
  • 2020-01-14
  • 1970-01-01
相关资源
最近更新 更多