澄清 P0137 的细节

clarification of specifics of P0137

在下面的代码中,我一直在仔细遵循标准中关于对象生命周期的措辞(加上 P0137 的措辞)。

请注意,根据 P0137,所有内存分配都是通过 unsigned char 类型的适当对齐存储进行的。

另请注意,Foo 是一个 POD,具有简单的构造函数。

问题:

一个。如果我对标准理解有误,这里有UB,请指出(或者确认没有UB)

乙。 A、B、C、D、E、F 处的初始化 严格 是否必要,因为构造是微不足道的,并且不执行实际初始化。如果是这样,请指出标准的哪一部分在这方面与[object.lifetime]矛盾或澄清。

代码:

#include <memory>

// a POD with trivial constructor
struct Foo 
{
  int x;
};

struct destroy1
{
  void operator()(Foo* p)
  {
    // RAII to guarantee correct destruction order
    auto memory = std::unique_ptr<unsigned char[]>(reinterpret_cast<unsigned char*>(p));
    p->~Foo(); // A
  }
};
std::unique_ptr<Foo, destroy1> create1()
{
  // RAII to guarantee correct exception handling
  auto p = std::make_unique<unsigned char[]>(sizeof(Foo));
  auto pCandidate = reinterpret_cast<Foo*>(p.get());
  new (pCandidate) Foo(); // B
  return std::unique_ptr<Foo, destroy1>(reinterpret_cast<Foo*>(p.release()), 
                                        destroy1());
}

struct call_free
{
  void operator()(void *p) const { std::free(p); } 
};
using malloc_ptr = std::unique_ptr<unsigned char, call_free>;

struct destroy2
{
  void operator()(Foo *pfoo) const {
    // RAII to guarantee correct destruction order
    auto memory = malloc_ptr(reinterpret_cast<unsigned char*>(pfoo));
    pfoo->~Foo(); // C
  }
};

std::unique_ptr<Foo, destroy2> create2()
{
    // RAII to guarantee correct exception handling
  auto p = malloc_ptr(reinterpret_cast<unsigned char*>(std::malloc(sizeof(Foo))));
  auto pCandidate = reinterpret_cast<Foo*>(p.get());
  new (pCandidate) Foo(); // D
  return std::unique_ptr<Foo, destroy2>(reinterpret_cast<Foo*>(p.release()), 
                                        destroy2());
}

struct nodelete {
  void operator()(Foo * p) {
    p->~Foo();  // E
  }
};

std::shared_ptr<Foo> provide()
{
  alignas(Foo) static unsigned char  storage[sizeof(Foo)];

  auto make = [] {
    auto p = reinterpret_cast<Foo*>(storage);
    new (p) Foo (); // F
    return std::shared_ptr<Foo>(p, nodelete());
  };

  static std::shared_ptr<Foo> pCandidate = make();

  return pCandidate;
}


int main()
{
  auto foo1 = create1();
  auto foo2 = create2();
  auto foo3 = provide();

  foo1->x = 1;
  foo2->x = 2;
  foo3->x = 3;
}

create1

std::unique_ptr<Foo, destroy1>(reinterpret_cast<Foo*>(p.release()), destroy1());

这不起作用,因为您使用了错误的指针。

p.release()认为它指向一个unsigned char[]。但是,这不是您要指向的对象。您要指向的是位于此数组中的对象,即您创建的 Foo

所以您现在受 [basic.life]/8 的约束。其要点是,如果它们属于同一类型,则只能将前一个指针用作指向新对象的指针。他们不是你的情况。

现在,我可以告诉你 launder 指针,但更合理的处理方法是只存储 placement-new 调用返回的指针:

auto p = std::make_unique<unsigned char[]>(sizeof(Foo));
auto ret = std::unique_ptr<Foo, destroy1>(new(p.get()) Foo(), destroy1());
p.release();
return ret;

那个指针永远是正确的。

您对 placement-new 的使用不是可选的。 [intro.object]/1 告诉我们:

An object is created by a definition (3.1), by a new-expression (5.3.4), when implicitly changing the active member of a union (9.3), or when a temporary object is created (4.4, 12.2).

当您分配 unsigned char[] 时,这就是您在该存储中创建的对象。你不能简单地假装它是一个 Foo,仅仅因为 Foo 是一个聚合。 [intro.object]/1 不允许这样做。您必须通过上面列出的机制之一显式创建该对象。由于您不能使用定义、union 成员激活或具有任意内存缓冲区的临时对象来从现有存储中创建对象,因此创建对象的唯一途径是 new 表达式。

具体来说,placement-new。

至于 delete1,您确实需要自定义删除器,因为默认删除器会在 Foo 指针上调用 delete。您的代码如下:

auto memory = std::unique_ptr<unsigned char[]>(reinterpret_cast<unsigned char*>(p));
p->~Foo();
由于 [intro.object]/3-4,

unsigned char[] 有一些特殊的逻辑,就对象在其存储中分配时的行为而言。如果对象完全覆盖 unsigned char[] 的存储,那么它的功能就好像对象是在数组中分配的一样。这意味着 unsigned char[] 在技术上仍然存在;它没有破坏字节数组。

因此,您仍然可以删除字节数组,您的代码会这样做。

create2

这也是错误的,因为进一步违反了 [basic.life]/8。固定版本与上述类似:

auto p = malloc_ptr(reinterpret_cast<unsigned char*>(std::malloc(sizeof(Foo))));
auto ret std::unique_ptr<Foo, destroy2>(new(p.get()) Foo(), destroy2());
p.release();
return ret;

与 new 表达式不同,malloc 从不通过 [intro.object]/1 创建对象;它只获取存储空间。因此,再次需要 placement-new。

同理,free只是释放内存;它不处理对象。所以你的 delete2 基本上没问题(尽管那里使用 malloc_ptr 会造成不必要的混淆)。

provide

这与您的其他示例具有相同的 [basic.life]/8 个问题:

alignas(Foo) static unsigned char storage[sizeof(Foo)];
static auto pCandidate = std::shared_ptr<Foo>(new(storage) Foo(), nodelete());
return pCandidate;

但除此之外,它很好(只要你不在其他地方破坏它)。为什么?这很复杂。

[basic.start.term]/1 告诉我们静态对象的销毁顺序与其初始化相反。 [stmt.decl]/4 告诉我们,块范围的静态对象按照它们在函数中遇到的顺序进行初始化。

因此,我们知道pCandidate会在之前被销毁storage。只要您不在静态变量中保留该 shared_ptr 的副本,或者在终止前无法 destroy/reset 所有此类共享对象,您应该没问题。


综上所述,使用 unsigned char 块确实是 C++11 之前的做法。我们现在有 std::aligned_storage and std::aligned_union。使用它们。

"Core Issue 1776: Replacement of class objects containing reference members" 是基于一个明显而严重的解释错误,因此应该被驳回。错误在这里:

Drafting note: this maintains the status quo that malloc alone is not sufficient to create an object

这与 "malloc alone" 确实足以创建对象的现状背道而驰,因为 malloc returns 适当对齐的存储,这一直足以创建对象。

一个核心问题就是没有标准。这是关于标准的意见。这个观点是错误的。

如果您认真对待 Core Issue 1776 并且从未投票赞成 "that malloc alone is not sufficient to create an object" 的想法,那么您必须认真对待这些想法:

  • 直到最近,在没有 UB 的情况下,联合才在 C++ 中可用,因为它们的成员的生命周期没有定义
  • 无法检查字符串文字,因为它们的生命周期从未开始

以及许多其他更深入、更棘手的争论和矛盾,比如什么是左值,什么是对象,是一个已经存在的对象(存在于它的生命之外)的属性生命周期等

但我看不到人们至少认真对待这两个要点。为什么DR中的声明会受到重视?