我(re?)发明了这种使用数据成员语法实现零成本属性的方法。我的意思是用户可以写:
some_struct.some_member = var;
var = some_struct.some_member;
并且这些成员访问以零开销重定向到成员函数。
虽然初步测试表明该方法在实践中确实有效,但我远不能确定它是否没有未定义的行为。以下是说明该方法的简化代码:
template <class Owner, class Type, Type& (Owner::*accessor)()>
struct property {
operator Type&() {
Owner* optr = reinterpret_cast<Owner*>(this);
return (optr->*accessor)();
}
Type& operator= (const Type& t) {
Owner* optr = reinterpret_cast<Owner*>(this);
return (optr->*accessor)() = t;
}
};
union Point
{
int& get_x() { return xy[0]; }
int& get_y() { return xy[1]; }
std::array<int, 2> xy;
property<Point, int, &Point::get_x> x;
property<Point, int, &Point::get_y> y;
};
测试驱动程序证明了这种方法是有效的,而且它确实是零成本的(属性不占用额外的内存):
int main()
{
Point m;
m.x = 42;
m.y = -1;
std::cout << m.xy[0] << " " << m.xy[1] << "n";
std::cout << sizeof(m) << " " << sizeof(m.x) << "n";
}
实际代码有点复杂,但方法的要点就在这里。它基于使用真实数据(本例中为xy
)和空属性对象的并集。(实际数据必须是一个标准布局类才能工作)。
需要并集,因为在其他情况下,尽管属性为空,但它们不必要地占用内存。
为什么我认为这里没有UB?该标准允许访问标准布局联合成员的公共初始序列。这里,公共初始序列是空的。x
和y
的数据成员根本不被访问,因为没有数据成员。我对标准的理解表明这是允许的。reinterpret_cast
应该是可以的,因为我们将一个并集成员强制转换为其包含的并集,并且这些是指针可交换的。
这确实是标准允许的吗,或者我在这里错过了一些UB?
TL;DR这是UB。
[基本生活]
类似地,在对象的生存期开始之前,但在分配对象将占用的存储之后,或者在对象的生命期结束之后,在重用或释放对象所占用的存储之前,可以使用任何引用原始对象的glvalue,但只能以有限的方式使用。对于正在构建或销毁的对象,请参阅[class.cdtor]。否则,这样的glvalue指的是已分配的存储,并且使用不依赖于其值的glvalue属性是定义良好的。程序有未定义的行为,如果:〔…〕
- glvalue用于调用对象的非静态成员函数,或者
根据定义,联合的非活动成员不在其生存期内。
一个可能的解决方法是使用C++20[[no_unique_address]]
struct Point
{
int& get_x() { return xy[0]; }
int& get_y() { return xy[1]; }
[[no_unique_address]] property<Point, int, &Point::get_x> x;
[[no_unique_address]] property<Point, int, &Point::get_y> y;
std::array<int, 2> xy;
};
static_assert(offsetof(Point, x) == 0 && offsetof(Point, y) == 0);
以下是关于并集的常见初始序列规则:
在具有结构类型
T1
的活动成员的标准布局联合中,允许读取结构类型T2
的另一个联合成员的非静态数据成员m
,前提是m是T1和T2的公共初始序列的一部分;行为就好像T1的相应成员被提名了一样。
您的代码不合格。为什么?因为你不是在阅读"另一个工会成员"的。你正在做m.x = 42;
。那不是读书;这就是调用另一个工会成员的成员函数。
因此,它不符合通用的初始序列规则。如果没有通用的初始序列规则来保护您,访问联盟的非活动成员就是UB。