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

0x4*_*D18 7 c++ pointers undefined-behavior c++14

我最近看到像这样的类用于"按需"构造对象,而不必出于各种原因使用动态内存分配.

#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)];
};
Run Code Online (Sandbox Code Playgroud)

这段代码,即将正确对齐的char数组转换为对象指针,被C++ 14标准视为未定义的行为,还是完全没问题?

asc*_*ler 8

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

[basic.compound]/3:

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

通常,T不会与布局兼容char或使用alignas(T) char[sizeof(T)],因此不要求指针T*具有与指针char*或表示相同的值表示void*.

[basic.compound]/4:

如果出现以下情况,则两个对象ab指针可互换的:

  • 它们是同一个对象,或者

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

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

  • 存在对象c,使得ac是指针可互换的,并且cb是指针可互换的.

如果两个对象是指针可互换的,那么它们具有相同的地址,并且可以通过a获得指向另一个指针的指针reinterpret_cast.[ 注意:数组对象及其第一个元素不是指针可互换的,即使它们具有相同的地址.- 结束说明 ]

[旁白:在C++ 17出版后,DR 2287在第二个子弹中将"标准布局联合"改为"联合".但这不会影响这个程序.]

T由放置新创建的对象不是指针互相转换用object_object_[0].并且该提示暗示这可能是演员阵容的问题......

对于C风格的演员表((T*)object_),我们需要看[expr.cast]/4:

由...执行的转换

  • 一个const_cast,

  • 一个static_cast,

  • a static_cast后跟一个const_cast,

  • a reinterpret_cast,或

  • 一个reinterpret_cast跟着一个const_cast

可以使用显式类型转换的强制转换表示法执行....

如果转换可以用上面列出的多种方式解释,则使用列表中首先出现的解释,即使由该解释产生的转换是格式错误的.

除非Tchar或者是cv-qualified char,否则这实际上是a reinterpret_cast,所以接下来我们看一下[expr.reinterpret.cast]/7:

可以将对象指针显式转换为不同类型的对象指针.当v对象指针类型的prvalue 被转换为对象指针类型"指向cv的 指针T"时,结果是static_­cast<cv T*>(static_­cast<cvvoid*>(v)).

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

类型为"指向cv的 指针"的prvalue T,其中T是一个对象类型,可以转换为类型为"指向cv的 指针"的prvalue void.此转换未改变指针值([basic.compound]).

接下来是[expr.static.cast]/13中描述的static_castfrom void*到:T*

"到指针类型的prvalue CV1 void "可以被转换成类型的prvalue"指针CV2 T ",其中T是一个对象类型和CV2是相同的CV-资格,或更大的CV-资格比,CV1.如果原始指针值表示A内存中字节的地址并且A不满足对齐要求T,则未指定结果指针值.否则,如果原始指针值指向对象a,并且存在类型(忽略cv-qualification)的对象b,该对象bT是指针可互换的a,则结果是指向b的指针.否则,转换指针值不变.

如前所述,类型的对象T不是指针可互换的object_[0],因此句子不适用,并且不能保证结果T*指向T对象!我们留下句子说"指针值不变",但如果值char*和表示的值表示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_;
    }
};
Run Code Online (Sandbox Code Playgroud)

或者甚至更好,因为这个类本质上是试图实现一个optional,只要std::optional你有它,或者boost::optional如果你没有它.


eer*_*ika 6

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

使用C样式转换将一个指针(数组衰减到指针)转换为另一个不在同一继承层次结构中的指针,执行重新解释转换.重新解释演员本身永远不会有UB.

但是,如果尚未将适当类型的对象构造到该地址中,则间接转换的指针可以具有UB.在这种情况下,在字符数组中构造了一个对象,因此间接具有明确定义的行为.编辑:如果不是严格的别名规则,间接将是UB免费的; 请看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_));
}
Run Code Online (Sandbox Code Playgroud)

构造对象成另一种类型的阵列,三个要求必须满足,以避免UB:另一种类型的必须允许别名对象类型(char,unsigned char并且std::byte满足所有对象类型此要求),该地址必须被对准到对象类型所需的内存边界,并且内存中的任何内存都不得与另一个对象的生命周期重叠(忽略允许对覆盖对象进行别名的数组的基础对象).您的计划满足所有这些要求.