如果我有两个具有相同类型的数据成员的并集,则仅在CV资格上有所不同:
template<typename T>
union A
{
private:
T x_priv;
public:
const T x_publ;
public:
// Accept-all constructor
template<typename... Args>
A(Args&&... args) : x_priv(args...) {}
// Destructor
~A() { x_priv.~T(); }
};
我有一个函数f,它声明一个联合A,从而使x_priv成为活动成员,然后从该联合中读取x_publ:
int f()
{
A<int> a {7};
return a.x_publ;
}
在我测试的每个编译器中,int类型和其他更复杂的类型(例如std :: string和std :: thread)的编译和运行时均没有错误。
我开始查看标准是否属于法律行为,并开始研究T
和const T
的区别:
6.7.3.1 [basic.type.qualifier]
一个类型的cv限定或cv不限定版本是不同的类型。但是,它们应具有相同的表示和对齐要求([basic.align])。
这意味着在声明const T
时,它在内存中的表示与T
完全相同。但是后来我发现该标准实际上不适用于某些类型,我觉得很奇怪,因为我认为没有理由。
我开始搜索非活跃成员。
如果两个都是标准布局类型,则访问T
和const T
的公共初始序列是合法的。
10.4.1 [class.union]
联合对象类型的最多一个非静态数据成员可以随时处于活动状态
[...] [注意:一种特殊保证这样做是为了简化联合的使用:如果标准布局联合包含多个共享公共初始序列([class.mem])的标准布局结构,并且该标准布局联合类型的对象是活动的,并且是标准布局结构之一,允许检查任何标准布局结构成员的公共初始序列;参见[class.mem]。 —尾注]
初始顺序基本上是非静态数据成员的顺序,但有一些例外,但是由于T
和const T
在相同布局中具有完全相同的成员,因此这意味着T
和const T
的初始序列是T
的所有成员。
10.3.22 [class.mem]
两种标准布局结构([class.prop])类型的公共初始序列是非静态数据成员和位字段的最长序列(按声明顺序),从每个结构中的第一个此类实体开始,以使对应的实体具有与布局兼容的类型,或者两个实体都使用no_unique_address属性([dcl.attr.nouniqueaddr])声明,或者都不是,并且两个实体都是具有相同宽度的位域,或者两者都不是位-领域。 [示例:
这里是出现限制的地方,它限制了某些类型的访问,即使它们在内存中具有完全相同的表示形式:
10.1.3 [class.prop]
如果满足以下条件,则S类为标准布局类:
- (3.1)没有类型为non-standard-layout的类(或此类数组)或引用的非静态数据成员,
- (3.2)没有虚函数,也没有虚基类,
- (3.3)对所有非静态数据成员具有相同的访问控制,
- (3.4)没有非标准布局的基类,
- (3.5)最多具有一个任何给定类型的基类子对象,
- (3.6)在该类中及其所有基类中首先声明了同一类中的所有非静态数据成员和位字段,并且
- (3.7)没有类型集合M(S)的元素作为基类,其中对于任何类型X,M(X)的定义如下。108[注:M(X)是X中可能偏移零的所有非基类子对象的类型。— [请注意]
- (3.7.1)如果X是不包含(可能是继承的)非静态数据成员的非联合类类型,则集合M(X)为空。
- (3.7.2)如果X是具有零大小的X_0类型的非静态数据成员或X的第一个非静态数据成员的非联盟类类型(其中所述成员可以是匿名联合),则集合M(X)由X_0和M(X_0)的元素组成。
- (3.7.3)如果X是联合类型,则集合M(X)是所有M(U_i)和包含所有U_i的集合的联合,其中每个U_i是第i个非静态数据的类型X的成员。
- (3.7.4)如果X是元素类型为X_e的数组类型,则集合M(X)由X e和M的元素(X_e)组成。
- (3.7.5)如果X是非类,非数组类型,则集合M(X)为空。
我的问题是是否有任何不合法行为的原因?。
本质上是:
标准制定者忘记考虑这一特殊情况了?
我还没有阅读允许这种行为的标准的某些部分?
还有一些更具体的原因导致此行为无效?
使其成为有效语法的原因是,例如,在类中具有“只读”变量,例如:
struct B;
struct A
{
... // Everything that struct A had before
friend B;
}
struct B
{
A member;
void f() { member.x_priv = 100; }
}
int main()
{
B b;
b.f(); // Modifies the value of member.x_priv
//b.member.x_priv = 100; // Invalid, x_priv is private
int x = b.member.x_publ; // Fine, x_publ is public
}
这样,您就不需要getter函数,这可能会导致性能开销,尽管大多数编译器会对其进行优化,但这仍然会增加您的类,而您必须编写int x = b.get_x()
才能获取变量。
您也不需要对该变量的const引用(如this question中所述),尽管该变量很好用,但会增加类的大小,这对于足够大的类或需要删除的类可能是不利的尽可能的小。
不得不写b.member.x_priv
而不是b.x_priv
很奇怪,但是如果我们可以在匿名联合中拥有私有成员,那么可以这样重写:
struct B
{
union
{
private:
int x_priv;
public:
int x_publ;
friend B;
};
void f() { x_priv = 100; }
}
int main()
{
B b;
b.f(); // Modifies the value of member.x_priv
//b.x_priv = 100; // Invalid, x_priv is private
int x = b.x_publ; // Fine, x_publ is public
}
另一个用例可能是给同一个数据成员以各种名称,例如在Shape中,用户可能希望将该位置引用为shape.pos
,shape.position
,{{ 1}}或shape.cur_pos
。
尽管这可能会产生比其价值更大的问题,但例如在不建议使用名称的情况下,这种用例可能是有利的。