具有数据成员语法的零成本属性



我(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?该标准允许访问标准布局联合成员的公共初始序列。这里,公共初始序列是空的。xy的数据成员根本不被访问,因为没有数据成员。我对标准的理解表明这是允许的。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。

最新更新