最近,我看到了一个类似的类,该类用于“按需”构造对象,而由于各种原因而不必使用动态内存分配。
#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标准认为未定义的行为吗?还是完全可以?
答案 0 :(得分:7)
该程序在技术上具有未定义的行为,尽管它可能适用于大多数实现。问题是,即使char*
指针表示,也不能保证从T*
到T
的强制转换会导致有效的指针指向由new放置创建的char*
对象用于存储T
对象的第一个字节的地址。
与布局兼容的类型的指针必须具有相同的值表示和对齐要求([basic.align])。
通常,T
与char
或alignas(T) char[sizeof(T)]
的布局不兼容,因此不要求指针T*
与值{指针char*
或void*
。
如果满足以下条件,则两个对象 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
可以使用显式类型转换的强制转换符号来执行。...
如果可以采用以上列出的一种以上方式来解释转换,则即使该解释产生的转换格式不正确,也要使用列表中最先出现的解释。
除非T
是char
或具有简历资格的char
,否则实际上是reinterpret_cast
,因此接下来我们来看一下[expr.reinterpret.cast]/7:
可以将对象指针显式转换为其他类型的对象指针。将对象指针类型的prvalue
v
转换为对象指针类型“指向 cv 的指针T
”时,结果为static_cast<
cv < / em>T*>(static_cast<
cvvoid*>(v))
。
因此首先我们有一个static_cast
从char*
到void*
,它执行[conv.ptr]/2中所述的标准转换:
类型“指向 cv
T
的prvalue”(其中T
是对象类型)可以转换为“指向的指针”的prvalue cvvoid
”。指针值([basic.compound])在此转换后保持不变。
其后是static_cast
至void*
的{{1}},如[expr.static.cast]/13中所述:
可以将“指向 cv1
T*
的指针的prvalue转换为”指向 cv2void
的指针的prvalue,其中T
是对象类型,而 cv2 是与 cv1 相同的cv限定,或具有更大的cv限定。如果原始指针值表示存储器中字节的地址T
,并且A
不满足A
的对齐要求,则未指定结果指针值。否则,如果原始指针值指向对象 a ,并且存在类型为T
(忽略cv限定)的对象 b ,则该指针为-与 a 可相互转换,结果是指向 b 的指针。否则,转换后指针值将保持不变。
如前所述,类型T
的对象不能与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_;
}
};
,所以如果有的话就使用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:必须允许其他类型为对象类型起别名(char
,unsigned char
和{{1 }}满足所有对象类型的这一要求),地址必须按照对象类型的要求与内存边界对齐,并且任何一个内存都不得与另一个对象的生存期重叠(忽略允许的数组基础对象)为重叠对象起别名)。您的程序满足了所有这些要求。
答案 2 :(得分:0)
在对@aschepler答案写评论之后,我认为我找到了正确的答案:
不,它不是UB!
非常有力的暗示:aligned_storage正是这样做的。
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 *类型的指针指向该对象。
指针类型的每个值都是以下之一:
(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限定或引用类型的数据成员,并且
第二点不正确(T
与char[]
),因此不能将object_
用作指向新创建的对象t
的指针。