C++Union,两个活跃成员,仅简历资格不同

C++ Union, two active members only differing by CV-qualification

本文关键字:活跃 C++Union 两个 成员      更新时间:2023-10-16

如果我有一个具有两个相同类型的数据成员的并集,仅在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)在编译和运行时都没有错误。

我去看了一下标准,看看这是否是合法行为,然后我开始研究Tconst T的区别:

6.7.3.1[基本类型限定符]

一个类型的cv合格或cv不合格版本是不同的类型;但是,它们应具有相同的表示和对齐要求([basic.align])。

这意味着当声明const T时,它在内存中的表示与T完全相同。但后来我发现,标准实际上不允许某些类型这样做,我觉得这很奇怪,因为我看不出有什么理由

我开始搜索访问非活动成员。

只有当Tconst T都是标准布局类型时,访问它们的公共初始序列才是合法的。

10.4.1[class.union]

联合类型的对象的非静态数据成员中最多有一个可以在任何时候处于活动状态[…][注意:为了简化并集的使用,有一个特殊的保证:如果一个标准布局并集包含多个共享公共初始序列([class.mem])的标准布局结构,并且如果此标准布局并立类型的对象的非静态数据成员是活动的并且是标准布局结构之一,允许检查任何标准布局结构体成员的公共初始序列;请参见[class.mem]--尾注]

初始序列基本上是非静态数据成员的顺序,只有少数例外,但由于Tconst T在同一布局中具有完全相同的成员,这意味着Tconst T的公共初始序列是T的所有成员。

10.3.22[class.mem]

两个标准布局结构([class.prp])类型的公共初始序列是非静态数据成员和位字段按声明顺序排列的最长序列,从每个结构中的第一个这样的实体开始,这样对应的实体就具有布局兼容的类型,要么两个实体都是用no-unique_address属性([dcl.attr.nouniqueaddr])声明的,要么两者都不是,要么这两个实体是宽度相同的位字段,要么都不是位字段。[示例:

这里是限制的来源,它限制了某些类型的访问,即使它们在内存中有完全相同的表示:

10.1.3[class.prop]

一个类S是一个标准布局类,如果它:

  • (3.1)不具有非标准布局类(或此类类型的数组)或引用类型的非静态数据成员
  • (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_0大小为零或者是X的第一个非静态数据会员(其中所述会员可以是匿名并集),则集合M(X)由X_0和M(X_0)的元素组成
    • (3.7.3)如果X是并集类型,则集合M(X)是所有M(U_i)和包含所有U_i的集合的并集,其中每个U_i是X的第i个非静态数据成员的类型
    • (3.7.4)如果X是元素类型为X_e的数组类型,则集合M(X)由Xe和M(X_e)的元素组成
    • (3.7.5)如果X是非类、非数组类型,则集合M(X)为空

我的问题是是否有任何原因导致这种行为无效

本质上是这样的:

  • 标准制定者忘记解释这种特殊情况了吗?

  • 我还没有读过允许这种行为的标准的某些部分?

  • 有什么更具体的原因导致这种行为无效?

这是有效语法的一个原因是,例如,在类中有一个"readonly"变量,例如:

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引用(如本问题所述),虽然它工作得很好,但它会增加类的大小,这对于足够大的类或需要尽可能小的类来说可能是不好的。

必须写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.posshape.positionshape.cur_posshape.shape_pos

尽管这可能会产生比实际情况更多的问题,但当例如一个名称应该被弃用时,这样的用例可能是有利的。

这样的代码:

struct A { int i; };
struct B { int j; };
union U {
struct A a;
struct B b;
};
int main() {
union U u;
u.a.i = 1;
printf("%dn", u.b.j);
}

在C中是有效的。为了向后兼容性,人们认为需要确保它在C++中也是有效的。关于标准布局结构的公共初始序列的特殊规则确保了这种向后兼容性。扩展规则以允许定义更多的情况——涉及非标准布局结构的情况——对于C兼容性来说是不必要的,因为所有可以在C和C++的公共子集中定义的结构都是C++中的标准布局结构。

实际上,C++规则比C兼容性所要求的稍微宽松一些。它们也允许一些涉及基类的情况:

struct A { int i; };
struct B { int j; };
struct C : A { };
struct D : B { };
// C and D have a common initial sequence consisting of C::i and D::j

但一般来说,C++中的结构可能比C中的结构复杂得多。例如,它们可以具有虚拟函数和虚拟基类,并且这些函数和基类可以以实现定义的方式影响它们的布局。出于这个原因,通过C++中定义的并集生成更多类型双关的情况并不容易。你真的必须与实现者坐下来讨论条件是什么,委员会应该要求两个类在其共同的初始序列中具有相同的布局,而不是由实现决定。目前,该命令仅适用于标准布局类。

该标准中存在各种规则,这些规则足够强,以暗示即使T不是标准布局类,Tconst T也总是具有完全相同的布局。因此,即使T不是标准布局,也可以在并集的T成员和const T成员之间定义特定形式的类型双关。然而,只将这个非常特殊的案例添加到语言中具有可疑的价值,我认为委员会不太可能接受这样的提议,除非你有一个真正令人信服的用例。不想提供一个返回const引用的getter,仅仅因为你不想编写()来在每次需要访问时调用getter,这不太可能说服委员会。