澄清P0137

时间:2016-12-02 10:52:10

标签: 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;
}

3 个答案:

答案 0 :(得分:6)

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告诉我们:

  

当隐式更改并集的活动成员(9.3)或创建临时对象时,由定义(3.1),new-expression(5.3.4)创建对象(4.4,12.2)

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

具体来说,就是新的。

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

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;

与新表达式不同,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的副本,或者在终止之前未能销毁/重置所有此类共享对象,您应该没问题。

尽管如此,使用unsigned char的块实际上是预先C ++ 11。我们现在有std::aligned_storagestd::aligned_union。使用它们。

答案 1 :(得分:1)

如果您认真对待核心问题1776,并且从未投票赞成“仅使用malloc不足以创建对象”的想法,那么您就必须认真对待这些想法:

  • 工会直到最近才在C ++中没有UB才能使用,因为其成员的生存期尚未定义
  • 字符串文字无法检查,因为它们的生命周期永远不会开始

还有许多其他更深层次的难题和矛盾,例如什么是左值,一个对象,生命周期是先前存在的对象(存在于其寿命之外)的属性等。

但是我看不到人们至少认真对待这两个要点。那么为什么要认真对待DR中的主张?

答案 2 :(得分:-1)

&#34;核心问题1776:替换包含参考成员的类对象&#34;基于明显而严重的解释错误,因此应该被驳回。错误在这里:

  

起草说明:这样可以维持单独使用malloc的现状   足以创建一个对象

这违背了现在&#34; malloc的现状&#34;确实足以创建一个对象,因为malloc返回适当对齐的存储,这一直是足以创建一个对象。

核心问题不是标准。这是关于标准的意见。这种观点是错误的。