【问题标题】:Is it really possible to separate storage allocation from object initialization?真的可以将存储分配与对象初始化分开吗?
【发布时间】:2021-11-10 18:56:42
【问题描述】:

来自[basic.life/1]

对象或引用的生命周期是对象或引用的运行时属性。如果一个变量是默认初始化的,并且如果它是类类型或其(可能是多维的)数组,那么该类类型具有一个普通的默认构造函数,则称该变量具有空初始化T 类型对象的生命周期开始于:

  • 获得了具有适合T类型的正确对齐和大小的存储,并且
  • 其初始化(如果有)已完成(包括空初始化)([dcl.init]),

除非对象是联合成员或其子对象,否则它的生命周期仅在联合成员是联合中的初始化成员([dcl.init.aggr]、[class.base.init])时才开始,或如 [class.union] 和 [class.copy.ctor] 中所述,但 [allocator.members] 中所述除外。

来自[dcl.init.general/1]

如果没有为对象指定初始化器,则该对象是默认初始化的。

来自[basic.indet/1]

当一个对象获得自动或动态存储时长的存储时,该对象有一个不确定的值,如果没有对该对象执行初始化,该对象将保留一个不确定的值,直到该值被替换([expr.ass])。 [注意 1:具有静态或线程存储持续时间的对象是零初始化的,请参阅 [basic.start.static]。 — 尾注]

考虑这个 C++ 程序:

int main() {
    int i;
    i = 3;
    return 0;
}

函数main的第一条语句int i;或第二条语句i = 3;是否按照C++标准进行初始化?

我认为是前者,它对不确定的值执行空初始化,因此开始对象的生命周期(后者不执行初始化,它执行对值 3 的赋值)。如果是这样,是否真的可以将存储分配与对象初始化分开?

【问题讨论】:

  • 嗯,extern int x; .... int x = 42; 不是将两者分开吗? (初始化声明)
  • 我不明白您引用的任何内容与您关于初始化分离声明的问题有何关系。
  • From Default initialization "默认初始化的效果是: .... 否则,不执行初始化:具有自动存储持续时间的对象(及其子对象)包含不确定的值。"
  • int i; i = 3; 是一个定义(和声明),没有初始化,后跟一个赋值.
  • @Maggyero 静态初始化是运行时过程的一部分,而声明和定义是编译时问题。我不清楚你在问他们之间的关系。

标签: c++ initialization language-lawyer object-lifetime


【解决方案1】:

如果是这样,是否真的可以将存储分配与对象初始化分开?

是的:

void *ptr = malloc(sizeof(int));

ptr 指向已分配的存储,但没有对象存在于该存储中(C++20 表示某些对象可能在那里,但现在没关系)。除非我们在那里创建对象,否则对象将不存在:

new(ptr) int;

【讨论】:

  • 谢谢!为什么不void* ptr = operator new(sizeof(int));?您能否引用支持不发生初始化的标准?
  • @Maggyero 请参阅eel.is/c++draft/basic.stc.dynamic#allocation-2.sentence-1。引用:“分配函数尝试分配请求的存储量。” 很明显operator new 只分配存储。不涉及初始化(没有要初始化的内容)。
  • @Maggyero 是的,我相信operator new 是一个更好的例子,因为malloc 因为C++20 隐式创建对象eel.is/c++draft/c.malloc#4.sentence-1operator new 似乎没有做同样的事情。
  • @DanielLangr: From [intro.object]/13: “任何隐式或显式调用名为 operator newoperator new[] 的函数都会在返回的存储区域中隐式创建对象”。另外,我注意到它可能会在 C++20 中创建对象,但出于本问题的目的而忽略它。
  • @Maggyero:正如我所说,出于这个问题的目的,请忽略隐式对象创建。它只是通过对记忆做某些事情来触发,而我发布的示例不会触发它
【解决方案2】:

您在为对象分配存储初始化对象之间感到困惑,它们绝对是不是 同样的事情。

在您的示例中,对象 i 从未初始化。作为一个本地值,它保留了存储空间,但没有用任何值初始化。

第二行的语句分配一个值3给它。这又不是初始化。

标准要求对具有全局存储的对象进行分配和初始化(为零或默认初始化程序所做的任何事情)。所有其他对象只有在书面语言结构支持时才会被初始化。

C++ allocators 处理同样的区别。 new 操作符,在幕后,分配初始化对象,之后你可以分配对象一个新的值。但是,如果需要,您可以使用底层语言结构来分别管理这两个东西。

在大多数情况下,您不需要关心代码中对象初始化和赋值之间的区别。如果您达到了重要的程度,您要么已经知道这些概念的不同之处,要么需要非常快速地学习。

【讨论】:

  • 关于“空初始化”的全部意义在于明确指出,就生命周期规则而言,“无初始化”被认为是一种初始化形式。
  • “虚初始化”是标准中的愚蠢语言律师,除了混淆概念边界之外没有任何用处。所以“不初始化”是初始化的一种形式只能得到我的单手掌声。不过,我知道我的答案可以改进。随意尝试一下。 (或者写一个更好的。如果是的话,我会投赞成票。)
  • 嗯,语言律师 == 拼出明显的东西,IMO。但是,嘿,我太爱当语言律师了。说真的,如果你们都想出一个更好的答案(或者 an 答案,也许?)我会很乐意删除我的答案并投票。
  • 感谢您的回答! “在您的示例中,对象i 从未初始化。”这似乎得到了标准的证实,该标准指定标量类型的默认初始化意味着‘no initialization is performed’。那为什么叫默认初始化呢?
  • @Maggyero 静态对象存储零初始化的整个事情就是这样说的。具有静态存储持续时间的对象需要存储在零初始化的内存中。因此,如果您尝试使用未“正常”初始化但可以通过 memset 将其表示形式合法地初始化为零的对象,那么您可以合法地继续使用它,就好像已经发生了一样。措辞是为了规避其他规则,例如从技术上讲,该对象不会在其生命周期内,否则它将具有不确定的状态。
【解决方案3】:

函数main的第一条语句int i;或第二条语句i = 3;是否按照C++标准进行初始化?

第一个。第二个语句是赋值,而不是初始化。第二个语句从您的标准引用中标记了“直到该值被替换([expr.ass])”这一点。

如果[initialization is the first statement]是这样,那么真的可以将存储分配与对象初始化分开吗?

是的,但不是像你这样简单的例子。一个常见的例子是std::vector。保留容量会分配存储空间,但在将对象添加到向量之前,该存储不会初始化。

std::vector<int> v;   // Allocates and initializes the vector object.
v.reserve(1);         // Ensures space has been allocated for an int object.
/*
 At this point, the first contained element has space allocated, but has
 not yet been initialized. If you want to do nutty things between allocation
 and object initialization, this is the place to do it. Note that you are
 not allowed to access the allocated space since it belongs to the vector.
 You'd have to replicate the inner workings of a vector to do that...
*/
v.emplace_back(3);    // Initializes the first contained object.

引用标准

短版:
没有什么可引用的,因为该标准并未明确禁止所有虚假行为。编译器会根据自己的意愿避免虚假操作。

加长版:

严格来说,标准并不能保证reserve() 不会初始化任何东西。在[vector.capacity] 中对reserve() 施加的要求更侧重于必须做什么,而不是禁止虚假活动。最接近此保证的是要求reserve() 的时间复杂度与容器的大小成线性关系(不是容量,而是当前大小)。这将使得不可能总是初始化所有保留的内容。但是,编译器仍然可以选择初始化固定数量的保留元素,比如多达 1000 万个。只要这个限制是固定的,它就被算作常数时间复杂度,所以是 [vector.capacity] 允许的。

现在让我们变得真实。编译器旨在生成快速代码,而不会引入不必要的、无用的忙碌工作。编译器不会仅仅因为标准没有禁止就寻找做额外工作的可能性。除了调试版本,没有编译器会在不需要时引入初始化。认为这种可能性值得考虑的人是语言律师,他们忽视了大局。你不需要为你不需要的东西付费。这里要问的问题不是“你能引用支持没有初始化发生的标准吗?”而是“你能引用支持没有初始化的标准是必需的?”由于不需要额外的初始化工作,所以在实践中不会发生。

不过,现实对某些语言律师来说意义不大,而这个问题确实有这个标签。为了彻底,我将证明“可以将存储分配与对象初始化分开”,即使您碰巧使用了一个病态但符合标准的编译器,该编译器被受虐狂过度设计。我只需要一个案例来证明“可能”,所以让我们放弃int,换一种更奇怪但完全合法的类型。

reserve() 的唯一前提条件是包含的类型可以移动插入到容器中。下面的类满足这个前提条件。

class C {
    // Default construction is not supported.
    C() = delete;
  public:
    // Move construction is allowed, even outside this class.
    C(C &&) = default;
};

我将这个类设计为很难初始化。唯一允许的构造是移动构造;为了初始化这种类型的对象,您需要已经有一个这种类型的对象。谁创建了第一个对象?没有人。这种类型的对象不能存在。但是,仍然可以创建这些对象的向量(一个空向量,但仍然是一个向量)。

定义std::vector&lt;C&gt; v; 是合法的,然后调用v.reserve(1);。这为C 类型的对象分配了空间(我的系统需要1 个字节),但该对象无法初始化。 QED。

【讨论】:

  • 谢谢。您能否引用支持不发生初始化的标准?
  • @Maggyero 答案已更新以引用标准。并不是说有很多要引用的;它更多地将信息应用于这种特殊情况。我认为其中大部分是显而易见的,但我承认“显而易见”是一个危险的词。
猜你喜欢
  • 1970-01-01
  • 2015-01-10
  • 2021-04-12
  • 2018-11-26
  • 2012-11-17
  • 2020-01-03
  • 2012-12-04
  • 1970-01-01
相关资源
最近更新 更多