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标准视为未定义的行为,还是完全没问题?
该程序在技术上具有未定义的行为,尽管它可能适用于大多数实现.问题是,即使指针表示用于存储对象的第一个字节的地址,也不能保证来自char*to T*的强制转换会导致指向T由placement new创建的对象的有效指针.char*T
布局兼容类型的指针应具有相同的值表示和对齐要求([basic.align]).
通常,T不会与布局兼容char或使用alignas(T) char[sizeof(T)],因此不要求指针T*具有与指针char*或表示相同的值表示void*.
如果出现以下情况,则两个对象a和b是指针可互换的:
它们是同一个对象,或者
一个是union对象,另一个是该对象的非静态数据成员([class.union]),或
一个是标准布局类对象,另一个是该对象的第一个非静态数据成员,或者,如果该对象没有非静态数据成员,则该对象的任何基类子对象([class.mem]) , 要么
存在对象c,使得a和c是指针可互换的,并且c和b是指针可互换的.
如果两个对象是指针可互换的,那么它们具有相同的地址,并且可以通过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可以使用显式类型转换的强制转换表示法执行....
如果转换可以用上面列出的多种方式解释,则使用列表中首先出现的解释,即使由该解释产生的转换是格式错误的.
除非T是char或者是cv-qualified char,否则这实际上是a reinterpret_cast,所以接下来我们看一下[expr.reinterpret.cast]/7:
可以将对象指针显式转换为不同类型的对象指针.当
v对象指针类型的prvalue 被转换为对象指针类型"指向cv的 指针T"时,结果是static_cast<cvT*>(static_cast<cvvoid*>(v)).
因此,首先,我们有一个static_cast从char*到void*,它不中所述的标准转换[conv.ptr]/2:
类型为"指向cv的 指针"的prvalue
T,其中T是一个对象类型,可以转换为类型为"指向cv的 指针"的prvaluevoid.此转换未改变指针值([basic.compound]).
接下来是[expr.static.cast]/13中描述的static_castfrom void*到:T*
"到指针类型的prvalue CV1
void"可以被转换成类型的prvalue"指针CV2T",其中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如果你没有它.
将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满足所有对象类型此要求),该地址必须被对准到对象类型所需的内存边界,并且内存中的任何内存都不得与另一个对象的生命周期重叠(忽略允许对覆盖对象进行别名的数组的基础对象).您的计划满足所有这些要求.
| 归档时间: |
|
| 查看次数: |
656 次 |
| 最近记录: |