首页 > 解决方案 > 具有数据成员语法的零成本属性

问题描述

我已经(重新?)发明了这种使用数据成员语法的零成本属性方法。我的意思是用户可以写:

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?

标签: c++language-lawyerunionundefined-behavior

解决方案


TL;DR 这是 UB。

[基本.生活]

类似地,在对象的生命周期开始之前但在对象将占用的存储空间已分配之后,或者在对象的生命周期结束之后并且在对象占用的存储空间被重用或释放之前,引用的任何泛左值可以使用原始对象,但只能以有限的方式使用。对于正在构造或销毁的对象,请参阅 [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);

推荐阅读