对象如何成为子对象

How does an object become a subobject?

本文关键字:对象 何成      更新时间:2023-10-16

这个问题是关于C++标准语义的形式主义。问题不在于实现或类的内存表示。

这是一个关于指向类对象的子对象的指针的含义以及是什么使指向对象的指针成为指向子对象的指针的问题。所以在某种程度上,这是关于指针的性质。

但是指针是指定子对象的设备,因此更深入的问题在于对象如何成为子对象,或者它是否开始作为子对象存在。

在构造期间,对象的生存期尚未开始。问题是关于在生存期开始之前可用的这些指针。

在C++中,您可以操作指向当前正在构造的对象的指针,甚至可以保存它以便在对象的生存期开始时(完全构造时)使用它。

一旦您开始在 ctor-init-list 中以及稍后在构造函数主体中对子对象进行构造或初始化,this指针就可用。(在 C 和 C++ 中,指针在对象初始化开始之前就已经非常早地可用了。

这意味着这些指向对象的"早期"指针不能指向正常的构造对象,甚至不能指向已开始构造自己的成员的对象。那么他们指的是什么呢?

假设我们创建了一个类类型为C的完整对象,并且它的一个类类型的子对象S在构造过程中保存this,比如在数据成员m中。

struct S { 
S *m; 
S() : m(this) {} 
};

(另一种方法是将this保存到构造函数主体中的静态变量;这并不重要。

S可以是:

  • 基子对象
  • 成员子对象

构造C后,m是否指向C对象的子对象?

在基类类型中,m是否指向派生对象作为转换为基类的指针?换句话说:指向基类主题的指针到底是什么?

何时构造为另一个对象的一部分的对象实际上是子对象?当超对象的构造函数调用子对象的构造函数时?或者当超级物体的构造完成时?

你能有一个尚未构造的超对象的子对象吗?

示例代码

例如:

struct C1 {
S m_c1;
};
struct C2 : S {
};
C1 c1;
C2 c2;

我想c1.m_c1.m指向c1.m_c1,一个子对象。c2.m指向基类子对象SC2还是指向C2本身?

什么时候构造为另一个对象的一部分的对象真的是子对象?

一个对象是否是另一个对象的子对象是该对象的核心属性,由其创建的性质决定。它不是可以动态获取或删除的属性。对象不会"成为"子对象。每个对象要么是完整的,要么是子对象,并且没有更改此类状态的机制。

对象

可以包含其他对象,称为子对象。子对象可以是成员子对象 ([class.mem])、基类子对象 (Clause [class.derived]) 或数组元素。不是任何其他对象的子对象的对象称为完整对象。

动态创建子对象有特定的规则(如放置new),但即便如此,这些对象在创建时也是子对象:

如果在与成员子对象或数组元素 e 关联的存储中创建对象(可能在其生存期内,也可能不在其生存期内),则创建的对象是 e 包含对象的子对象,如果:<一些规则>


你能有一个尚未构造的超对象的子对象吗?

是的。它发生在任何具有子对象的对象的每个构造函数中。包含对象的构造函数尚未完成,但是一旦构造函数的主体启动,所有子对象都已初始化,因此在其生存期内。

关于成员和基如何初始化的部分不断将它们称为"子对象"。由于这个过程发生在对象的构造过程中,我认为可以肯定地说这种情况发生了。


这意味着这些指向对象的"早期"指针不能指向正常的构造对象,甚至不能指向已开始构造自己的成员的对象。那么他们指的是什么呢?

您可以考虑对象构造的状态,例如是对象的动态属性(与它是否是子对象不同)。也就是说,对象要么尚未开始其生存期,要么在其生存期内,要么在其生存期结束后。由于各种事件,对象可以在这些状态之间转换,但它仍然是同一个对象。

也就是说,如果您有一个正在构造的对象,那么在其生存期开始后,它仍然是同一对象。指针不关心它们指向的对象的生存期状态;他们仍然指出它。

我真的不能从标准中引用一些东西,因为标准中没有任何内容表明,当生命周期实际开始时,生命周期尚未开始的对象是另一个对象。因此,由于指针指向特定对象,因此没有理由期望对象的生存期状态更改本身会影响它指向的对象。

指针可以指向对象,引用可以引用对象,甚至在对象的生存期开始之前,甚至在初始化开始之前。

使用this是这类事情的一个相对温和的版本,因为对象*this的初始化至少在评估构造函数的第一个mem 初始值设定项时已经开始。

在对象初始化开始之前,对对象的指针或引用或该对象的名称的使用限制在 [basic.life]/6-7 和 [class.cdtor]/1,3 中。本质上,它只能用作内存,以形成对同一类型的其他指针和引用;您甚至还不能命名其子对象。

在执行类类型对象的构造函数(包括对mem 初始值设定项的计算)期间,对类类型对象的指针、引用或名称的使用限制在 [class.cdtor]/2,4-6 中。与您的问题相关的一个特别有趣的点是第 2 段中的规则,该规则指出,最终派生自该对象的this关键字的指针和引用可用于访问初始化子对象的值,但如果其他名称、指针或引用以相同的方式使用,则未指定行为。

假设我们创建了一个类类型为C的完整对象,并且它的类类型子对象之一S在构造期间将其保存在数据成员m中。构造C后,m是否指向C对象的子对象?

是的,因为一旦类型为S的子对象的构造函数开始,其作用域中的this值就是指向该子对象的指针。S子对象的构造函数在执行C对象的构造函数期间执行,这意味着C此时已经"拥有"其子对象,至少通过C构造函数定义中的this指针值。例如,我们可以有:

class S {
public:
S();
int number;
};
class C {
public:
C();
static S* current_s;
private:
class SetCurrS {
explicit SetCurrS(S* s_ptr) { current_s = s_ptr; }
};
SetCurrS before_s;
S s;
};
C global_c;
C::C() : before_s(&this->s), s() { current_s = nullptr; }
S::S() : number(0) {
// Will compare equal (assuming single thread):
if (this == current_s) std::cout << "equals saved pointern";
// Also will compare equal:
if (this == &global_c.s) std::cout << "equals global subobjectn";
// Definitely zero:
std::cout << "my number: " << number << 'n';
// UNSPECIFIED BEHAVIOR:
std::cout << "global's number: " << global_c.s.number << 'n';
}

在基类类型中,m 是否指向派生对象作为转换为基类的指针?换句话说:指向基类主题的指针到底是什么?

m指向基类子对象 term。就C++标准虚拟机而言,这是某个其他对象的子对象这一事实更像是该子对象的属性,而不是指向子对象的指针值的重要部分。 参见[basic.compound]/3:

指针类型的每个值都是以下值之一:

指向对象或函数的指针
  • (指针被称为指向对象或函数),或

  • 超过对象末尾的指针([expr.add]),或

  • 该类型的空指针值([conv.ptr]),或

  • 无效的指针值

当指向派生对象的指针值转换为指向基类类型的指针时,结果是指向基类子对象的指针值。此指针值与指向该子对象的任何其他指针值相同,包括在为该子对象调用的构造函数或其他非静态成员函数中有效使用时this的值。

实际上子对象是三个独立的东西C++标准共享一个属性 - 作为另一个对象的一部分:

对象

可以包含其他对象,称为子对象。子对象可以 是成员子对象、基类子对象数组元素。一 不是任何其他对象的子对象的对象称为完整的对象

何时构造为另一个对象的一部分的对象实际上是子对象?

在您的示例中,类S的唯一子对象的类型为S*。它指向的任何内容都不是子对象,因为它不是"超级对象"存储的一部分。数组也是一个对象,因此它的元素是包含类的子对象。

如果在与成员子对象关联的存储中创建对象 或数组元素e(可能在其生存期内,也可能不在其生存期内), 创建的对象是E包含对象的子对象,如果:

(2.1)包含 E的对象的生命周期已经开始且尚未结束, 和

(2.2) 新对象的存储正好覆盖存储 与E关联的位置,以及

(2.3)新对象与E的类型相同(忽略 简历资格)。

类的静态成员与类的对象无关:它们是具有静态或线程持续时间的独立变量,它们与声明它们的对象的关系具有代码组织的目的。

由于命名空间或块不是对象,因此由命名空间(包括全局范围)、函数或闭包中的声明构造的任何对象都是完整的对象。使用通过新表达式创建的存储构造的任何对象都是完整的对象。放置新重用存储,因此可以在现有对象和构造对象中创建子对象。

你能有一个尚未构造的超对象的子对象吗?

不,你不能,但是基类子对象会在完整对象的任何其他子对象之前初始化,这并不意味着首先构造基子对象,而是意味着完整对象的构造尚未完成。

有类似于初始化案例 [class.base.init] 的示例:

内存初始值设定项的表达式列表或大括号初始化列表中的名称 在构造函数的范围内进行评估,其中 指定了内存初始值设定项。

class X {
int a;
int b;
int i;
int j;
public:
const int& r;
X(int i): r(a), b(i), i(i), j(this->i) { }
};

[ 注意:因为 mem 初始值设定项是在 构造函数,此指针可以在表达式列表中使用 mem 初始值设定项,用于引用正在初始化的对象。]