【问题标题】:Is it practically OK to delete object not constructed using the new expression?删除不是使用新表达式构造的对象实际上可以吗?
【发布时间】:2018-09-07 20:40:53
【问题描述】:

学究式,这可能不太好。根据cppref

如果表达式是其他任何东西,包括如果它是通过new-expression的数组形式获得的指针,则行为未定义。

抛开这一点,下面的代码在实践中是否可行(T 是非数组,并假设 new 未被替换)?

auto p = (T*)operator new(sizeof(T));
new(p) T{};
delete p;

据说在cppref那个

调用分配函数时,new-expression 传递 作为第一个参数请求的字节数,类型 std::size_t,也就是非数组 T 的 sizeof(T)

所以我想这可能没问题。不过也有说从C++14开始,

允许新表达式省略或合并分配 通过可替换的分配函数。在省略的情况下, 存储可以由编译器提供而无需调用 分配函数(这也允许优化未使用的 新表达式)。在合并的情况下,由 a 进行的分配 new-expression E1 可以扩展为提供额外的存储 如果以下所有条件都为真,则另一个新表达式 E2:[...]

请注意,这种优化只在 new-expressions 是允许的 使用,而不是任何其他方法来调用可替换的分配函数: delete[] new int[10]; 可以优化,但operator delete(operator new(10)); 不能。

我不太确定其中的含义。那么,这在 C++14 中可以吗?

我为什么要问这个问题? (source)

有时,内存分配和初始化不能一步完成。 您必须手动分配内存,执行其他操作,然后初始化对象, 例如,提供强大的异常安全性。 在这种情况下,如果不能在结果指针上使用 delete 表达式,则必须手动取消初始化和解除分配,这很繁琐。 更糟糕的是,如果同时使用新的表达方式和手动方法, 您必须跟踪每个对象使用了哪一个。

【问题讨论】:

  • 在您到达delete 之前,您必须确保您的placement new 使用适当对齐的内存,在这种情况下不会发生这种情况。
  • @SamVarshavchik 它确实发生了。 operator new() 保证这一点。 此函数需要返回一个适当对齐的指针以保存任何基本对齐的对象。请参阅here
  • “实际上可以吗”很难回答;您似乎同意代码是未定义的行为,因此您确实要求调查所有存在的编译器如何表现出这种未定义的行为。此外,它们可能会在未来的编译器版本或模式中以不同的方式表现出来。
  • 我想知道你为什么还要问。如果你不这样做,你不应该这样做,你永远不必找到答案,处理错误报告,重写整个事情,当它不适用于一些新的平台甚至一些新的编译器版本,...
  • @Lingxi 从技术上讲,使用您编写的代码,p 并不指向新构造的T。内存地址是相同的,但编译器可以假定它引用了在放置 new 之前存在的(不存在的)T。在这么简单的代码中,我认为不会有什么不同,但如果T 有一个const 成员或其他东西,编译器可能会在调用new 之前从内存中加载它。 std::launder 调用告诉它不要进行这种优化,但是将放置的结果 new 分配回 p 会更容易和更清晰。

标签: c++ c++14 new-operator dynamic-memory-allocation delete-operator


【解决方案1】:

我没有完全理解 OP 的最后一段。显式调用析构函数然后释放指针有什么乏味的?这就是我所见过的 STL 和任何有信誉的库一直在做的事情。是的,您必须跟踪如何分配内存,以便将其重新分配。阅读 std::vector 和/或 std::shared_ptr 的 api 文档可以使人们熟悉正确的做事方式。 如果你不熟悉显式 dtor 调用的语法,这里是一个简单的 sn-p:

void * raw_ptr= my_alloc(sizeof(my_type),alignof(my_type));
if (!raw_ptr)
     throw my_bad_alloc();
my_type* ptr {new(raw_ptr) my_type{init_params}};
raw_ptr=nullptr;
    //...
ptr->~my_type();//hello! Is it me you're lookin' for?
if (!my_dealloc(ptr))
     throw my_runtime_error();
ptr=nullptr;

【讨论】:

  • 如果在使指针失效后将指针归零,则应将 ptr = nullptr; 直接移到析构函数调用之后,在 my_dealloc() 调用中将 ptr 替换为 raw_ptr,并以raw_ptr = nullptr;...
  • 好点;我错过了那个。但我更关注显式析构函数调用。
  • 那么,你为什么不把它编辑到你的答案中呢?我的cmets总有一天会被删除,你的答案是不会。
  • 阅读这个问题似乎很明显,OP 知道你在说什么,你没有回答这个问题。您正在公开您的观点:您不认为在销毁和分配步骤中分解 delete 表达式是乏味的。问题是:在OP案例中使用delete表达式在实践中可以吗?
  • 我做了一些编辑,但不是全部;一些冗长是故意的;不鼓励使用原始指针,并且应该将 [de] 分配放在 ctor/dtor 中。为了说明和简单起见,在 sn-p 中进行了归零。
【解决方案2】:

如果您有p = new(p) T{};,则此代码具有明确定义的行为,假设没有自定义(取消)分配功能在起作用。 (它可以很容易地针对此类事情进行强化;这样做留给读者作为练习。)

规则是你必须给非数组delete一个指向由非数组new创建的对象的指针(或此类对象的基类(带有虚拟析构函数)子对象,或空指针)。有了这个改变,你的代码就可以做到这一点。

不要求非数组new 是非放置形式。不可能,因为你最好能删除new(std::nothrow) int。这也是一个展示位置new-expression,尽管人们在谈论“展示位置新”时通常不是这个意思。

delete 被定义为调用一个释放函数(忽略省略的情况,这在此处无关紧要,因为 new-expression 调用的唯一分配函数不是可替换的全局函数分配函数)。如果您进行设置以便将无效参数传递给该释放函数,那么您将获得未定义的行为。但是这里它传递了正确的地址(如果使用了大小的释放函数,则传递了正确的大小),所以它是明确定义的。

【讨论】:

  • 如果T 过度对齐,我可以看到潜在的问题(因此释放函数将采用std::align_val_tT 是数组类型(在这种情况下,即使虽然看起来不像,但放置 new 表达式实际上是一个新数组,它引入了一些复杂性)。如果这是通用代码,那么这两个都可以处理,如果它不是通用的,除了这个问题你应该知道你是否正在处理这些案件。
  • 哇~ O_O 一个小问题。我认为这里的变化纯粹是迂腐的,对吧?据说here 在标准分配函数void* operator new(std::size_t, void*) 之后,这种分配函数被称为“新位置”,它只是简单地返回其第二个参数不变。
  • @Lingxi 正如我在回复您对该问题的评论时所说的那样,确实存在差异。假设您有struct T { const int i };T* p = new T{5};。如果您执行int sum = p->i; p->~T; new(p) T{20}; sum += p->i;,您将调用UB,并且编译器很有可能只查看p->i 一次(毕竟,它是const;它不可能改变),留下sum == 10。您需要使用new 表达式的结果重新分配p,或者使用std::launder 不进行优化。
  • const 可能是更容易理解的例子,但不是唯一的例子。另一个可以在 N4303 中找到,该论文最初提出 launder
  • 直到最近几年才真正完全理解指针模型的全部含义。 There is a paper in flight 更改为 construct。 @丹尼尔H
猜你喜欢
  • 2016-03-15
  • 1970-01-01
  • 2017-12-17
  • 2010-11-23
  • 2021-11-07
  • 1970-01-01
  • 1970-01-01
  • 2010-09-15
相关资源
最近更新 更多