是在格式良好的char数组中构造对象

时间:2017-07-11 17:25:33

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

这几乎是标准教科书使用新的

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是作弊行为,但这只是为了举例。

2 个答案:

答案 0 :(得分:2)

从无数有用评论的提示中收集,这是我对发生的事情的解释。

TLDR 其结构良好 ‡见编辑

按照我[basic.life]

找到更符合逻辑的顺序报价
  

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

  

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

     
      
  • 获取具有T类型的正确对齐和大小的存储,

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

  •   
  

类型T的对象o的生命周期在以下时间结束:

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

  •   
  • 释放对象占用的存储空间,或者由未嵌套在o

  • 中的对象重用   

来自[basic.lval]

  

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

     
      
  • 对象的动态类型

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

  •   
  • 类似于对象的动态类型的类型

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

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

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

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

  •   
  • charunsigned charstd​::​byte类型。

  •   

我们推断出

  1. 当另一个对象重用该空格时,charchar[]的生命周期结束。

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

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

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

  5. 在评论中回答我自己的问题是否使用任何内存作为存储也是格式正确的

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

    在上述4点之后,答案是 ‡请参阅编辑,只要U的对齐方式为不弱于T

    ‡编辑:我忽略了[basic.life]

    的另一段
      

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

         
        
    • 新对象的存储空间恰好覆盖原始对象占用的存储位置,

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

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

    •   
    • 原始对象是类型T的派生程度最高的对象,新对象是类型T的派生程度最高的对象(即,它们不是基类子对象)。 / p>

    •   

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

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

    在C ++ 17之前,解决方法是使用从placement new获取的指针

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

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

答案 1 :(得分:0)

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

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