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

n. *_* m. 18 c++ union undefined-behavior language-lawyer

我(re?)用数据成员语法发明了这种零成本属性的方法.通过这个我的意思是用户可以写:

some_struct.some_member = var;
var = some_struct.some_member;
Run Code Online (Sandbox Code Playgroud)

并且这些成员访问重定向到成员函数,零开销.

虽然初步测试表明该方法在实践中确实有效,但我很难确定它没有未定义的行为.以下是说明该方法的简化代码:

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;
};
Run Code Online (Sandbox Code Playgroud)

测试驱动程序演示了该方法的工作原理并且确实是零成本(属性不占用额外的内存):

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";
}
Run Code Online (Sandbox Code Playgroud)

真正的代码有点复杂,但方法的要点就在这里.它基于使用真实数据(xy在此示例中)和空属性对象的并集.(实际数据必须是标准布局类才能实现).

需要联盟,因为否则属性不必占用内存,尽管是空的.

为什么我认为这里没有UB?该标准允许访问标准布局联合成员的公共初始序列.这里,公共初始序列是空的.的数据成员x,并y没有在所有被访问的,因为没有数据成员.我对标准的阅读表明这是允许的.reinterpret_cast应该没问题,因为我们正在将一个联盟成员转换为其包含的union,这些是指针可互换的.

标准是否允许这样做,或者我在这里错过了一些UB?

Pas*_* By 14

TL; DR这是UB.

[basic.life]

类似地,在对象的生命周期开始之前但在对象将占用的存储已经被分配之后,或者在对象的生命周期结束之后并且在对象占用的存储被重用或释放之前,任何引用的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);
Run Code Online (Sandbox Code Playgroud)

  • @nm:`no_unique_address`解决方案令人讨厌的部分是你自然希望将实际成员*私有*同时保留"属性"公开,但这样做会破坏标准布局.如果你打破标准布局,那么类型的布局会受到"no_unique_address"成员的影响(更不用说破坏`offsetof`)的可能性更大.这就是我认为"属性"应该有的原因.是一个关注其背后的实际行为的关键词,而不仅仅是一个建议. (2认同)

Nic*_*las 6

以下是关于联合共同初始序列规则:

在具有struct类型的活动成员的标准布局并集中T1,允许读取mstruct类型的另一个union成员的非静态数据成员,T2前提是m是T1和T2的公共初始序列的一部分; 行为就像提名T1的相应成员一样.

您的代码不符合条件.为什么?因为你不是在 "另一个工会会员".你在做m.x = 42;.那不是读书; 那是在调用另一个工会成员的成员函数.

因此它不符合常见的初始序列规则.如果没有保护您的共同初始序列规则,访问联合的非活动成员就是UB.