当前位置: 首页 > 知识库问答 >
问题:

通过在封闭对象的生命周期内放置新的来重用数据成员存储

龚苏燕
2023-03-14

这是我上一个问题的后续问题,在这个问题上,我似乎使问题比我最初打算的更复杂。(见问答评论中的讨论。)这个问题是对原始问题的一个轻微修改,删除了在构建/销毁封闭对象期间的特殊规则问题。

是否允许在其封闭对象的生存期内重用非静态数据成员的存储?如果允许,在什么条件下?

考虑一下程序

#include<new>
#include<type_traits>

using T = /*some type*/;
using U = /*some type*/;

static_assert(std::is_object_v<T>);
static_assert(std::is_object_v<U>);
static_assert(sizeof(U) <= sizeof(T));
static_assert(alignof(U) <= alignof(T));

struct A {
    T t /*initializer*/;
    U* u;

    void construct() {
        t.~T();
        u = ::new(static_cast<void*>(&t)) U /*initializer*/;
    }

    void destruct() {
        u->~U();
        ::new(static_cast<void*>(&t)) T /*initializer*/;
    }

    A() = default;
    A(const A&) = delete;
    A(A&&) = delete;
    A& operator=(const A&) = delete;
    A& operator=(A&&) = delete;
};

int main() {
    auto a = new A;
    a->construct();
    *(a->u) = /*some assignment*/;
    a->destruct(); /*optional*/
    delete a; /*optional*/

    A b; /*alternative*/
    b.construct(); /*alternative*/
    *(b.u) = /*some assignment*/; /*alternative*/
    b.destruct(); /*alternative*/
}

除了静态断言之外,假设初始化器、析构函数和T和U的赋值不抛出。

对象类型TU需要额外满足哪些条件,以便程序具有定义的行为(如果有)?

它是否取决于实际调用的的析构函数(例如,取决于是否存在/*可选*/或/*备选*/行)?。

它是否取决于A的存储时间,例如是否使用主中的/*备选*/行?

请注意,除了在析构函数和destruct函数中,程序在新放置后不使用t成员。当然,不允许在其存储被其他类型占用时使用它。

另请注意,程序在所有执行路径中调用其析构函数之前在t中构造了一个原始类型的对象,因为我不允许TU抛出异常。

还请注意,我不鼓励任何人编写这样的代码。我的目的是更好地理解语言的细节。特别是,我没有发现任何禁止此类放置的新闻,至少,只要没有调用析构函数。

共有2个答案

陆雅志
2023-03-14

此答案基于以下网站上提供的草稿:http://eel.is/c草稿/

我们可以尝试(通过检查每个条件)将我决定调用的“不死对象”子句应用于曾经存在的任何先前对象,这里我们将其应用于T类型的成员t

寿命[基本寿命]/8

如果在对象的生存期结束后且在重用或释放该对象占用的存储之前,在原始对象占用的存储位置创建了新对象,则指向原始对象的指针、引用原始对象的引用或原始对象的名称将自动引用新对象,并且,一旦新对象的生存期开始,可以用于操纵新对象,如果:

(8.1)新对象的存储正好覆盖原始对象占用的存储位置,并且

(8.2)新对象与原始对象的类型相同(忽略顶级cv限定符),并且

(8.3)原始对象既不是const限定的完整对象,也不是此类对象的子对象,并且

(8.4)原始对象和新对象都不是潜在重叠的子对象([简介对象])。

通过在旧构件上使用新放置,条件1和2自动得到保证:

struct A {
    T t /*initializer*/; (...)

    void destruct() { (...)
        ::new(static_cast<void*>(&t)) T /*initializer*/;
    }

位置相同,类型相同。这两种情况都很容易验证。

创建的对象都不是:

auto a = new A;
...
A b; /*alternative*/

是常量限定的完整对象,因此t不是常量限定的完整对象的成员。满足条件3。

现在潜在重叠的定义在对象模型[intro.object]/7中:

可能重叠的子对象是:

(7.1)基类子对象,或

(7.2)使用no_­unique_­address属性声明的非静态数据成员。

t成员两者都不是并且满足条件4。

满足所有4个条件,因此可以使用成员名t来命名新对象。

[请注意,在任何时候我都没有提到子对象不是const成员,不是它的子对象。这不是最新草案的一部分。

这意味着const子对象可以合法地更改其值,并且引用成员可以为现有对象更改其referent。这不仅令人不安,而且可能不被许多编译器支持。结束注释。]

周博达
2023-03-14

如果一个被销毁(无论是通过删除还是不在范围内),则调用t.t(),如果t实际上不是一个t(通过不调用destruct)。

这不适用,如果

  • T的析构函数是平凡的,或
  • 对于删除,U是从T或li派生出来的
  • 您正在使用销毁删除

调用destruct后,如果T具有const或引用成员(直到C 20),则不允许使用t

除此之外,就我所见,你对这个类所做的事情没有任何限制。

 类似资料:
  • 所有的数据都有生命周期,生命周期越长,需要的手续费(Energy)越多。生命周期终止,数据将被删除。从而可以淘汰无用数据。 日志型数据的生命周期是固定的一年,通过数据的生命周期,可以计算出日志写入时间,跨链读取将以这个时间和区块的时间比较,如果大于n*5分钟(n为两条链的逻辑距离),则日志数据是有效的。从而实现可信的跨链数据读取。

  • 每个响应对象只有当在 servlet 的 service 方法的范围内或在 filter 的 doFilter 方法范围内是有效的,除非该组件关联的请求对象已经开启异步处理。如果相关的请求已经启动异步处理,那么直到AsyncContext 的 complete 方法被调用,请求对象一直有效。为了避免响应对象创建的性能开销,容器通常回收响应对象。在相关的请求的startAsync 还没有调用时,开发

  • 每个请求对象只在一个 servlet 的 service 方法的作用域内,或过滤器的 doFilter 方法的作用域内有效,除非该组件启用了异步处理并且调用了请求对象的 startAsync 方法。在发生异步处理的情况下,请求对象一直有效,直到调用 AsyncContext 的 complete 方法。容器通常会重复利用请求对象,以避免创建请求对象而产生的性能开销。开发人员必须注意的是,不建议在上

  • 对象的地址在其生命周期内是不变的还是可以改变的?我只是认为对象的地址永远不会改变。它依赖于JVM吗?我没有找到任何明确的规范。

  • 在Spring MVC中,我可以使用ThreadLocal通过请求在不同组件之间共享数据,当请求完成时数据将被自动清除。使用WebFlux,由于一个请求可以由多个线程处理,因此该解决方案将不起作用。如何实现类似的解决方案,以便最初一个WebFilter可以在请求上下文中设置一些数据,然后可以在控制器中访问和修改数据,以及请求经过的任何事件处理程序? 我尝试了订阅者Context,但它不起作用。这是

  • 我有两个程序。第一个分配共享内存文件,第二个从中读取。。我使用placement new将对象放置到此内存中,以确保对象不会使用新的或分配共享内存文件之外的任何内存。 我的阵列结构: 方案1: 方案2: > 程序一将SHMArray放置在内存中,位置为new。程序二在程序一已经放置的对象上做同样的事情(覆盖它)。这是未定义的行为吗?我认为不是,但我想确认一下。 两个程序都不调用析构函数数组- 我基