c++ - 澄清 P0137 的细节

标签 c++ language-lawyer c++17

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

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

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

问题:

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

B. 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。其要点是,如果它们属于同一类型,则只能将前一个指针用作指向新对象的指针。他们不是你的情况。

现在,我可以告诉您清理 指针,但更合理的处理方式是只存储由 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 的使用会造成不必要的混淆)。

提供

这与您的其他示例具有相同的 [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 告诉我们, block 范围的静态对象按照它们在函数中遇到的顺序进行初始化。

因此,我们知道pCandidate存储之前会被销毁。只要您不在静态变量中保留该 shared_ptr 的拷贝,或者在终止前未能销毁/重置所有此类共享对象,您应该没问题。


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

关于c++ - 澄清 P0137 的细节,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40930475/

相关文章:

c++ - 临时对象最初是 const 吗?

c++ - 如何在 C++ 17 中移动字符串的字母?

c++ - 查看 std::future 是否已经启动

c++ - C++ 的性能测试实用程序

c++ - 对引用成员进行偏移(非 POD)

c++ - 为什么这段代码不能用 VS2010 和 gcc 4.8.1 编译

c++ - 字符串文字左值和右值引用的函数重载

c++ - CUDA,Memcpy 中的 "illegal memory access was encountered"

c++ - 为什么 C 和 C++ 允许表达式 (int) + 4*5?

c++ - 访问函数变体时出现 "Invalid conversion"错误