将char数组强制转换为对象指针-这是UB吗?

时间:2018-07-08 11:55:58

标签: c++ pointers c++14 undefined-behavior

最近,我看到了一个类似的类,该类用于“按需”构造对象,而由于各种原因而不必使用动态内存分配。

#include <cassert>

template<typename T>
class StaticObject
{
public:
    StaticObject() : constructed_(false)
    {
    }

    ~StaticObject()
    {
        if (constructed_)
            ((T*)object_)->~T();
    }

    void construct()
    {
        assert(!constructed_);

        new ((T*)object_) T;
        constructed_ = true;
    }

    T& operator*()
    {
        assert(constructed_);

        return *((T*)object_);
    }

    const T& operator*() const
    {
        assert(constructed_);

        return *((T*)object_);
    }

private:
    bool constructed_;
    alignas(alignof(T)) char object_[sizeof(T)];
};

这是代码,即将正确对齐的char数组强制转换为对象指针,是C ++ 14标准认为未定义的行为吗?还是完全可以?

5 个答案:

答案 0 :(得分:7)

该程序在技术上具有未定义的行为,尽管它可能适用于大多数实现。问题是,即使char*指针表示,也不能保证从T*T的强制转换会导致有效的指针指向由new放置创建的char*对象用于存储T对象的第一个字节的地址。

[basic.compound]/3

  

与布局兼容的类型的指针必须具有相同的值表示和对齐要求([basic.align])。

通常,Tcharalignas(T) char[sizeof(T)]的布局不兼容,因此不要求指针T*与值{指针char*void*

[basic.compound]/4

  

如果满足以下条件,则两个对象 a b pointer-interconvertible 的指针:

     
      
  • 它们是同一对象,或者

  •   
  • 一个是联合对象,另一个是该对象的非静态数据成员([class.union]),或者

  •   
  • 一个是标准布局类对象,另一个是该对象的第一个非静态数据成员,或者,如果该对象没有非静态数据成员,则是该对象的任何基类子对象( [class.mem]),或

  •   
  • 存在一个对象 c ,使得 a c 是指针可互换的,而 c b 是指针可互换的。

  •   
     

如果两个对象是指针可互换的,则它们具有相同的地址,并且可以通过reinterpret_cast从指向另一个的指针获得指向一个的指针。 [注意:数组对象及其第一个元素即使指针具有相同的地址,也不能指针可相互转换。 — 尾注]

[Aside:DR 2287在C ++ 17发布后的第二个项目符号中将“标准布局联合”更改为“联合”。但这并不影响该程序。]

由new放置创建的T对象不能与object_object_[0]进行指针互换。注释暗示这可能是演员的问题...

对于C样式的强制转换((T*)object_),我们需要查看[expr.cast]/4

  

执行的转换      
      
  • 一个const_cast

  •   
  • 一个static_cast

  •   
  • 一个static_cast后跟一个const_cast

  •   
  • 一个reinterpret_cast

  •   
  • 一个reinterpret_cast和一个const_cast

  •   
     

可以使用显式类型转换的强制转换符号来执行。...

     

如果可以采用以上列出的一种以上方式来解释转换,则即使该解释产生的转换格式不正确,也要使用列表中最先出现的解释。

除非Tchar或具有简历资格的char,否则实际上是reinterpret_cast,因此接下来我们来看一下[expr.reinterpret.cast]/7

  

可以将对象指针显式转换为其他类型的对象指针。将对象指针类型的prvalue v转换为对象指针类型“指向 cv 的指针T”时,结果为static_­cast< cv < / em> T*>(static_­cast< cv void*>(v))

因此首先我们有一个static_castchar*void*,它执行[conv.ptr]/2中所述的标准转换:

  

类型“指向 cv T的prvalue”(其中T是对象类型)可以转换为“指向的指针”的prvalue cv void”。指针值([basic.compound])在此转换后保持不变。

其后是static_castvoid*的{​​{1}},如[expr.static.cast]/13中所述:

  

可以将“指向 cv1 T*的指针的prvalue转换为”指向 cv2 void的指针的prvalue,其中T是对象类型,而 cv2 是与 cv1 相同的cv限定,或具有更大的cv限定。如果原始指针值表示存储器中字节的地址T,并且A不满足A的对齐要求,则未指定结果指针值。否则,如果原始指针值指向对象 a ,并且存在类型为T(忽略cv限定)的对象 b ,则该指针为-与 a 可相互转换,结果是指向 b 的指针。否则,转换后指针值将保持不变。

如前所述,类型T的对象不能与T进行指针互转换,因此该语句不适用,并且不能保证结果object_[0]指向T*个对象!我们只剩下说“指针值未更改”的句子,但是如果Tchar*指针的值表示形式太不同,这可能不是我们想要的结果。

可以使用T*实现该类的标准兼容版本:

union

甚至更好,因为此类实际上是在尝试实现template<typename T> class StaticObject { public: StaticObject() : constructed_(false), dummy_(0) {} ~StaticObject() { if (constructed_) object_.~T(); } StaticObject(const StaticObject&) = delete; // or implement StaticObject& operator=(const StaticObject&) = delete; // or implement void construct() { assert(!constructed_); new(&object_) T; constructed_ = true; } T& operator*() { assert(constructed_); return object_; } const T& operator*() const { assert(constructed_); return object_; } private: bool constructed_; union { unsigned char dummy_; T object_; } }; ,所以如果有的话就使用std::optional,否则就用boost::optional

答案 1 :(得分:5)

  

将char数组投射到对象指针-这是UB吗?

使用C样式强制转换将一个指针(数组衰减为一个指针)投射到不在同一继承层次结构中的另一个指针,将执行重新解释转换。重新解释的演员本身从来就没有UB。

但是,如果尚未在该地址中构造适当类型的对象,则间接转换指针可以具有UB。在这种情况下,已在字符数组中构造了一个对象,,因此间接调用具有良好定义的行为。编辑:如果不是严格的别名规则,则该间接将是UB free;有关详细信息,请参见ascheplers答案。 aschepler显示了符合C ++ 14的解决方案。在C ++ 17中,可以通过以下更改来更正您的代码:

void construct()
{
    assert(!constructed_);
    new (object_) T; // removed cast
    constructed_ = true;
}

T& operator*()
{
    assert(constructed_);
    return *(std::launder((T*)object_));
}

要将对象构造为其他类型的数组,必须满足三个条件以避免UB:必须允许其他类型为对象类型起别名(charunsigned char和{{1 }}满足所有对象类型的这一要求),地址必须按照对象类型的要求与内存边界对齐,并且任何一个内存都不得与另一个对象的生存期重叠(忽略允许的数组基础对象)为重叠对象起别名)。您的程序满足了所有这些要求。

答案 2 :(得分:0)

在对@aschepler答案写评论之后,我认为我找到了正确的答案:

不,它不是UB!

非常有力的暗示:aligned_storage正是这样做的。

  • basic.compound[4]为我们提供了“指针可相互转换”的定义。没有一种情况适用,因此T*unsigned char[...]不能指针可互换。
  • conv.ptr[2]expr.static.cast[13]告诉我们reinterprer_cast<T*>(object_)发生了什么。基本上,(中间)强制类型转换为void*不会更改指针的值,并且从void*强制类型转换为T*也不会更改指针的值:

      

    如果原始指针值表示内存中字节的地址A,而A 不满足T的对齐要求,则结果指针值将为 unspecified 。否则,如果原始指针值指向对象a,并且存在类型为T(忽略cv限定)的对象b,且该对象 pointer-interconvertible (指针可互换),则结果是指向b的指针。 否则,转换后指针值将保持不变。

    在这里,我们有一个正确对齐的,不是指针可互换的类型。因此值不变。

  • 现在P0137(在another answer中找到)basic.compound [3]说:

      

    如果类型T的对象位于地址A,则无论其值如何获得,都将以其值为地址A的cv T *类型的指针指向该对象。

    现在它说basic.compound[3]

      

    指针类型的每个值都是以下之一:

         

    (3.1)      指向对象或函数的指针(据说该指针指向对象或函数),[...]

    为此,我认为是等效的。

  • 最后我们需要basic.lval[11]

      

    如果程序试图通过glvalue访问对象的存储值,该glvalue的类型与以下一种类型不相似([conv.qual]),则行为未定义:52   [...]

         

    (11.3)   字符,无符号字符或std :: byte类型。

    这归结为别名规则,该规则仅允许某些类型别名,而我们的unsigned char就是其中的一部分。

总而言之:

  • 我们符合对齐和别名规则
  • 我们获得了一个指向T*的指针值(与unsigned char*相同)
  • 因此我们在那个地方有一个有效的物体

这基本上是@eerorika所具有的。但是我从上述观点认为,至少在T没有任何const的情况下,代码才是完全有效的,在这种情况下,必须使用std::launder。即使那样,如果内存没有被重用(仅用于创建1 T),那么它也应该是有效的。

尽管年龄较大的海湾合作委员会(<7.2)抱怨严格混叠违规:https://godbolt.org/z/Gjs05C,尽管docu指出:

  

例如,无符号的int可以为int加上别名,但不能为void *或double。 **字符类型可以是其他任何类型的别名。 **

这是一个bug

答案 3 :(得分:-1)

创建这样的StaticObject时,它确实会为T对象和正确的大小保留适当的对齐约束,但不会构造该对象。

调用construct()时,它将调用placement-new来在保留的存储区中构造对象(正确对齐且不为null)。这不是最自然的处理方式,但是这里没有UB。

唯一可能是UB的地方是,如果new放置会覆盖已经存在的对象。但这是通过assert()来防止的。

答案 4 :(得分:-1)

您确实有未定义的行为。

object_不是T*,因此强制转换和取消引用它是UB。您不能使用object_来引用新创建的对象。这也称为严格别名。

但是,修复很容易:只需创建一个新的成员变量T*,即可使用该成员变量访问构造的对象。然后,您需要将新的放置结果分配给该指针:

ptr = new(object_) T;

[basic.life] p1说:

  

类型为T的对象 o 的生存期在以下情况下终止:

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

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

  •   

因此,通过执行new (object_) T;,您将结束原始char[]对象的生存期,并开始我们称为T的新t对象的生存期。

现在我们必须检查*((T*)object_)是否有效。

[basic.life] p8突出显示了重要的内容:

  

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

     
      
  • 新对象的存储空间正好覆盖了原始对象所占据的存储位置,   和

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

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

  •   

第二点不正确(Tchar[]),因此不能将object_用作指向新创建的对象t的指针。