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

C++ 具有数据成员语法的零成本属性,c++,language-lawyer,union,undefined-behavior,C++,Language Lawyer,Union,Undefined Behavior,我(重新?)发明了这种使用数据成员语法实现零成本属性的方法。我的意思是用户可以写: some_struct.some_member = var; var = some_struct.some_member; 这些成员访问重定向到成员函数,开销为零 虽然最初的测试表明该方法在实践中确实有效,但我远不能确定它是否没有未定义的行为。下面是说明该方法的简化代码: template <class Owner, class Type, Type& (Owner::*accessor)()&g

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

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;
};
模板
结构属性{
运算符类型&(){
所有者*optr=重新解释铸件(本);
返回(optr->*访问器)();
}
类型和运算符=(常量类型和t){
所有者*optr=重新解释铸件(本);
返回(optr->*访问器)()=t;
}
};
联合点
{
int&get_x(){return xy[0];}
int&get_y(){return xy[1];}
std::数组xy;
财产x;
财产y;
};
测试驱动程序证明了该方法是可行的,并且确实是零成本的(属性不占用额外内存):

intmain()
{
m点;
m、 x=42;
m、 y=-1;
std::coutTL;DR这是UB

类似地,在对象的生存期开始之前,但在分配对象将占用的存储之后,或者在对象的生存期结束之后,在重用或释放对象占用的存储之前,可以使用引用原始对象的任何glvalue,但只能以有限的方式使用。对于对象er构造或销毁,请参见[class.cdtor]。否则,此类glvalue引用已分配的存储,并且使用不依赖于其值的glvalue属性是定义良好的。如果:[……],则程序具有未定义的行为

  • glvalue用于调用对象的非静态成员函数,或
到目前为止,工会的非活跃成员不在其生命周期内


一种可能的解决方法是使用C++20

结构点
{
int&get_x(){return xy[0];}
int&get_y(){return xy[1];}
[[无唯一地址]]财产x;
[[无唯一地址]]财产y;
std::数组xy;
};
静态断言(偏移量(点,x)==0和偏移量(点,y)==0);
以下是:

在具有结构类型为
T1
的活动成员的标准布局联合中,允许读取另一个结构类型为
T2
的联合成员的非静态数据成员
m
,前提是m是T1和T2的公共初始序列的一部分;其行为就好像指定了T1的对应成员一样

您的代码不合格。为什么?因为您没有从“另一个工会成员”读取。您正在执行
m.x=42;
。这不是读取;这是调用另一个工会成员的成员函数


因此,它不符合通用初始顺序规则。如果没有通用初始顺序规则来保护您,访问联盟的非活动成员就是UB。

我认为没有UB,至少在c++11及更高版本中没有。不过,我不会指出联盟,而只是放置数据成员并将相应的属性转换为点内的匿名联合。然后在属性中使用reinterpret_cast转换到数据成员(而不是类点)。这样,您可以从点继承,并且该方法可能会更好地扩展,因为您(或子类)可以在类中放置多个匿名联合。@AndreasH。我正按照您在实际代码中的建议进行操作,但这会使事情变得更复杂。为了表示的目的,我对其进行了简化。指针互易性是否意味着对象处于活动状态,以更改指向它的指针值?或者这只是
std::launder
?我能想到的唯一UB可能性是[class.mfct.non static]/2。当调用对象的成员函数时,该对象处于非活动状态。@LanguageLawyer“union对象及其非静态数据成员是指针可相互转换的”对我来说已经足够了。如果你认为这句话不能真正保证所有成员的可转换性,而不是只保证活动成员的可转换性,那么欢迎你提交一份缺陷报告。哦,所以检查非活动对象成员的权限不扩展到成员功能。这很不幸,对我来说似乎是一个缺陷。@n.m.我也很惊讶,没想到[基本生活]将彻底禁止这种用法。特别是因为通过空指针调用可以说是定义良好的。@n.m:因为它没有意义。允许您访问
x.y
,因为编译器可以清楚地看到您正在访问一个特定的成员变量。您的操作的范围是有界的,所有人都清楚sta是什么最重要的是,调用成员函数可以做任何事情(正如这个例子所证明的,在这个例子中,您可以接触到其他对象以获取引用)。该操作的范围是无限的。我个人认为,允许它是对对象模型的嘲弄。@n.m.:
no\u unique\u address
解决方案中令人恼火的部分是,您自然希望在保留“属性”的同时将实际成员私有化公共,但这样做会破坏标准布局。如果您破坏标准布局,则类型的布局更有可能受到
无唯一地址
成员的干扰(更不用说破坏
偏移量
。这就是为什么我认为“属性”应该是一个有实际行为的关键词,而不仅仅是一个建议。@n.m.:“如果我调用一个成员函数,那是因为我希望它执行某个动作,而不是因为我想做某种深刻的哲学陈述。”但如果你作为成员编写函数,你就是在做一个“深刻的哲学陈述”关于函数和它所属对象之间的关系。你个人不关心“哲学陈述”并不意味着它不存在。这就是为什么统一函数调用语法不存在的部分原因
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";
}
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);