【问题标题】:Is constructing objects in a char array well-formed是否在格式良好的 char 数组中构造对象
【发布时间】:2017-07-11 17:25:32
【问题描述】:

这几乎是教科书对新位置的标准用法

template<size_t Len, size_t Align>
class aligned_memory
{
public:
    aligned_memory() : data((char*)(((std::uintptr_t)mem + Align - 1) & -Align)) {}
    char* get() const {return data;}
private:
    char mem[Len + Align - 1];
    char* data;
};

template<typename T, size_t N>
class Array
{
public:
    Array() : sz(0) {}
    void push_back(const T& t)
    {
        new (data.get() + sz++ * sizeof(T)) T(t);
    }
    void pop_back()
    {
        ((T*)data.get() + --sz)->~T();
    }

private:
    aligned_memory<N * sizeof(T), alignof(T)> data;
    size_t sz;
};

看起来还不错,在我们研究严格混叠之前,这是否格式正确似乎存在一些冲突

阵营不良

阵营整齐

他们都同意char* 可能总是引用另一个对象,但有人指出反过来这样做是不正确的。

显然,我们的char[] 转换为char*,然后转换为T*,用于调用其析构函数。

那么,上面的程序是否违反了严格的别名规则?具体来说,标准中的什么地方说它是格式良好的还是格式不正确的?

编辑:作为背景信息,这是在 alignasstd::launder 出现之前为 C++0x 编写的。没有专门要求 C++0x 解决方案,但它是首选。

alignof 是作弊,但这里是为了举例。

【问题讨论】:

  • 阅读[basic.life] 已经十亿次了,无法决定答案
  • 在内部,std::vector 类通常将其动态数组实现为字节数组。
  • @Someprogrammerdude 标准库不需要(通常也不能)以 100% 严格符合 C++ 的方式实现。
  • Those answers 如果您还没有阅读它们,可能会有所帮助。
  • @Rakete1111 我之前读过(并赞成)这个答案,aligned_memory 不适用于unions。我在回答问题时遇到了这个问题,它需要 C++0x,没有alignas

标签: c++ language-lawyer c++03


【解决方案1】:

从无数有用的 cmets 中收集到的提示,这是我对正在发生的事情的解释。

TLDR 格式正确‡见编辑


按我从[basic.life]

中发现更合乎逻辑的顺序引用

本国际标准中赋予对象和引用的属性仅在其生命周期内适用于给定对象或引用。


如果一个对象是一个类或聚合类型,并且它或它的一个子对象由一个普通的默认构造函数以外的构造函数初始化,则称它具有非空初始化。 [...]T 类型对象的生命周期开始于:

  • 获得与T类型正确对齐和大小的存储,并且

  • 如果对象有非空初始化,则其初始化完成。


T 类型的对象 o 的生命周期结束于:

  • 如果 T 是具有非平凡析构函数的类类型,则析构函数调用开始,或者

  • 对象占用的存储被释放,或者被未嵌套在o中的对象重用

来自[basic.lval]

如果程序尝试通过以下类型之一以外的左值访问对象的存储值,则行为未定义

  • 对象的动态类型,

  • 对象的动态类型的 cv 限定版本,

  • 一种类似于对象的动态类型的类型,

  • 对象的动态类型对应的有符号或无符号类型,

  • 对应于对象动态类型的 cv 限定版本的有符号或无符号类型,

  • 在其元素或非静态数据成员(递归地包括子聚合或包含的联合的元素或非静态数据成员)中包含上述类型之一的聚合或联合类型,

  • 一种类型,它是对象的动态类型的(可能是 cv 限定的)基类类型,

  • charunsigned charstd​::​byte 类型。

我们推断

  1. char[] 中的chars 的生命周期在另一个对象重用该空间时结束。

  2. T 类型对象的生命周期在调用 push_back 时开始。

  3. 由于地址((T*)data.get() + --sz)始终是类型为T的对象的地址,其生命周期已经开始但尚未结束,因此使用它调用~T()是有效的。

  4. 在此过程中,aligned_memory 中的 char[]char* 别名为 T 类型的对象,但这样做是合法的。此外,没有从它们获得泛左值,因此它们可能是任何类型的指针。

在 cmets 中回答我自己的问题是否使用 any 内存作为存储也是格式正确的

U u;
u->~U();
new (&u) T;
((T*)&u)->~T();
new (&u) U;

按照上面的4点,答案是yes‡见编辑,只要U的对齐不弱比T

‡ 编辑:我忽略了[basic.life] 的另一段

如果在对象的生命周期结束之后,在对象占用的存储空间被重用或释放之前,在原对象占用的存储位置创建一个新对象,一个指向原对象的指针,引用原始对象的引用或原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,可用于操作新对象,如果:

  • 新对象的存储空间正好覆盖了原始对象占用的存储位置,并且

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

  • 原始对象的类型不是 const 限定的,并且,如果是类类型,则不包含任何类型为 const 限定或引用类型的非静态数据成员,并且

  • 原始对象是T 类型的最衍生对象,而新对象是T 类型的最衍生对象(也就是说,它们不是基类子对象)。

这意味着即使使用对象是格式良好的,但获得对象的手段却不是。具体来说,在 C++17 之后,std::launder 必须被调用

(std::launder((T*)data.get()) + --sz)->~T();

在 C++17 之前,一种解决方法是改用从放置 new 获取的指针

T* p = new (data.get() + sz++ * sizeof(T)) T(t);  // store p somewhere

† 据我所知,引自 n4659,同样适用于 n1905

【讨论】:

    【解决方案2】:

    Placement-new 在指定位置 (C++14 expr.new/1) 创建一个对象,并结束占用该位置 (basic.life/1.4) 的任何其他对象的生命周期。

    代码((T*)data.get() + --sz)-&gt;~T(); 在存在T 类型对象的位置访问T 类型对象。这可以。如果该位置曾经有一个 char 数组,则无关紧要。

    【讨论】:

      猜你喜欢
      • 2013-06-12
      • 1970-01-01
      • 2020-02-13
      • 2021-11-02
      • 1970-01-01
      • 1970-01-01
      • 2013-10-14
      • 1970-01-01
      相关资源
      最近更新 更多