如何同时更新结构体的多个字段?

6
假设我有一个结构体:
struct Vector3 {
    float x;
    float y;
    float z;
};

请注意,sizeof(Vector3)必须保持不变。

编辑:我对不使用setter的解决方案感兴趣。

现在让我们创建一个结构体实例Vector3 pos。如何实现我的结构体,以便我可以像这样使用pos.xy = 10 //更新x和ypos.yz = 20 //更新y和zpos.xz = 30 //更新x和z


1
你可以这样做,但需要大量样板代码,你不能使用方法吗? pos.set_yz(20)?那能回答你的问题吗,还是你特别想要pos.yz = 20; - 463035818_is_not_a_number
@idclev463035818 是的,使用访问器有解决方案,但我想知道是否有不用它们的方法...你所说的样板代码是什么意思?注意:sizeof(Vector3)必须保持不变。 - Hrant Nurijanyan
1
这看起来有点像所谓的“swizzling”。您可以在GLM库中看到此实现。 - Andrew Tomazos
1
Glm 也没有 swizzling。它有一些复杂的函数来近似它,但没有像 GLSL 的 swizzles 那样的东西。我坚信在 C++ 中没有函数、宏或编译器支持这个特性是不可能实现 swizzles 的。 - Lukas-T
2
@churill:可以看一下 GLM_CONFIG_SWIZZLE... https://github.com/g-truc/glm/blob/master/glm/detail/_swizzle.hpp https://github.com/g-truc/glm/blob/master/glm/detail/type_vec4.hpp - Andrew Tomazos
显示剩余12条评论
5个回答

3

这是一个既符合语法又不增加类大小的解决方案。它在技术上是正确的,但相当复杂:

union Vector3 {
    struct {
        float x, y, z;
        auto& operator=(float f) { x = f; return *this; }
        operator       float&() &        { return  x; }
        operator const float&() const &  { return  x; }
        operator       float () &&       { return  x; }
        float* operator&()               { return &x; }
    } x;
    
    struct {
        float x, y, z;
        auto& operator=(float f) { y = f; return *this; }
        operator       float&() &        { return  y; }
        operator const float&() const &  { return  y; }
        operator       float () &&       { return  y; }
        float* operator&()               { return &y; }
    } y;
    
    struct {
        float x, y, z;
        auto& operator=(float f) { z = f; return *this; }
        operator       float&() &        { return  z; }
        operator const float&() const &  { return  z; }
        operator       float () &&       { return  z; }
        float* operator&()               { return &z; }
    } z;
    
    struct {
        float x, y, z;
        auto& operator=(float f) { x = y = f; return *this; }
    } xy;
    
    struct {
        float x, y, z;
        auto& operator=(float f) { y = z = f; return *this; }
    } yz;
    
    struct {
        float x, y, z;
        auto& operator=(float f) { z = x = f; return *this; }
    } zx;
};

另一个程序依赖于此处实现的owner_of函数:https://gist.github.com/xymopen/352cbb55ddc2a767ed7c5999cfed4d31,这可能取决于某些技术上特定(可能未定义)的行为。
struct Vector3 {
    float x;
    float y;
    float z;
    
    [[no_unique_address]]
    struct {
        auto& operator=(float f) {
            Vector3* v = owner_of(this, &Vector3::xy);
            v->x = v->y = f;
            return *this;
        }
    } xy;
    [[no_unique_address]]
    struct {
        auto& operator=(float f) {
            Vector3* v = owner_of(this, &Vector3::yz);
            v->y = v->z = f;
            return *this;
        } 
    } yz;
    [[no_unique_address]]
    struct {
        auto& operator=(float f) {
            Vector3* v = owner_of(this, &Vector3::zx);
            v->z = v->x = f;
            return *this;
        }
    } zx;
    [[no_unique_address]]
    struct {
        auto& operator=(float f) {
            Vector3* v = owner_of(this, &Vector3::zx);
            v->x = v->y = v->z = f;
            return *this;
        }
    } xyz;
};

在您的链接中,offset_of 是未定义行为(使用了空指针的解除引用),而且 reinterpret_cast 的成员指针转换为整数值(我相信违反了 C++ 语言中的任何有效转换)。我认为基于 owner_of 的任何解决方案在 C++ 中都是无效的,即使“它可以工作”。 - Human-Compiler
哦,你和我想的一样 :) - G. Sliepen
除了间接性在地址运算符内,因此不被评估。这是真的,关于这个缺陷问题尚未解决。 - eerorika

2
简单的方法是为您想要设置的组合提供setter函数:
struct Vector3 {
    float x = 0;
    float y = 0;
    float z = 0;
    void set_xy(float v) {
        x = v;
        y = v;
    }
};

int main(){
    Vector3 pos;
    pos.set_xy(42);
}

如果你需要保持sizeof(Vector3)不变,那么这是唯一的方法。


仅仅是"为了好玩",这就是你可以字面上获取pos.set_xy = 20;的方法:

struct two_setter {
    float& one;    
    float& two;
    void operator=(float v){
        one = v;
        two = v;
    }
};

struct Vector3 {
    float x = 0;
    float y = 0;
    float z = 0;
    two_setter set_xy{x,y};
};

int main(){
    Vector3 pos;
    pos.set_xy = 42;
}

然而,它有严重的缺点。首先,它几乎可以达到原始Vector3的两倍大小。此外,由于two_setter存储引用,因此无法复制Vector3。如果它存储指针,则可以进行复制,但是需要更多的代码才能使其正确。
或者,可以提供一个xy方法,返回分配两个成员的代理。但我不会详细介绍,因为pos.xy() = 3;看起来非常奇怪,与pos.xy(3)没有优势,并且您应该提供一个setter(或仅在用户想要进行两次赋值时依靠用户进行两次赋值;)。
TL;DR使用方法而不是尝试获得C++不支持的语法。

拥有如此大的Vector3大小将是一件令人沮丧的事情。 - Evg
@Evg同意。这只是为了展示原则上如何完成,因为OP似乎只是出于好奇而提问,也许我应该更清楚地表明“不要这样做”。 - 463035818_is_not_a_number

2
Vector3 内部可以创建一个空结构体,并使用 operator=() 设置外部结构体的变量。当然,为了确实不占用任何空间,必须使用 [[no_unique_address]],这仅在 C++20 中可用。以下是可能的示例:
struct Vector3 {
    [[no_unique_address]] struct {
        auto &operator=(float val) {
            Vector3 *self = (Vector3 *)(this);
            self->x = val;
            self->y = val;
            return *this;
        }
    } xy;

    // Add similar code for xz and yz

    float x;
    float y;
    float z;
};

请到godbolt.org 上查看它的运行情况。

是的,reinterpret_cast 对你的答案来说效果不佳,但从技术上讲这也是一个答案。 - Hrant Nurijanyan
@HrantNurijanyan 在 C++ 中,联合体中的匿名结构体是不存在的(就像你的代码示例一样)。这只能通过编译器扩展来实现,但从技术上讲,它不是有效的代码。然而,仍然可能存在一种使用联合体的有效方法。 - Human-Compiler
如果union内部的匿名结构体无法使用,您仍然可以有一个命名的结构体来保存实际值,但是您需要为xyz创建单个变量"swizzle"操作符 :) - G. Sliepen
1
@G.Sliepen很遗憾,编译器(或多个编译器)生成正确结果并不意味着该解决方案符合标准,或保证在不同的编译器、优化级别等环境下继续工作。因为许多有效的C代码否则无法被编译,编译器经常让您将一些内容作为扩展别名使用。关于换位的第二点:我认为需要做的就是采用一个代理类型的联合体,共享相同的公共初始序列,这也是唯一有效的(根据语言规范)解决此问题的方法。 - Human-Compiler
如果结构体具有标准布局,则允许在这些指针之间进行转换。由于实际上在该地址处存在一个“Vector3”对象,因此别名在这里并不起作用。 - Quimby
显示剩余5条评论

1
由于您的类型是标准布局,我认为根据C++标准,唯一合法的方法是使用包含具有自定义operator=定义的子对象的union。
使用union,您可以查看活动成员的公共初始序列,前提是所有类型都是标准布局类型。因此,如果我们精心制作一个对象,该对象共享相同的常规成员(例如按相同顺序排列的3个float对象),那么我们可以在不违反严格别名的情况下在它们之间“交换”。
为了实现这一点,我们需要创建一堆具有相同数据以标准布局类型相同顺序的成员。
作为一个简单的例子,让我们创建一个基本的代理类型:
template <int...Idx>
class Vector3Proxy
{
public:

    // ...

    template <int...UIdx, 
              typename = std::enable_if_t<(sizeof...(Idx)==sizeof...(UIdx))>>
    auto operator=(const Vector3Proxy<UIdx...>& other) -> Vector3Proxy&
    {
        ((m_data[Idx] = other.m_data[UIdx]),...);
        return (*this);
    }

    auto operator=(float x) -> Vector3Proxy&
    {
        ((m_data[Idx] = x),...);
        return (*this);
    }

    // ...

private:

    float m_data[3];
    template <int...> friend class Vector3Proxy;
};

在这个例子中,不是所有的m_data成员都被使用了——但它们存在是为了满足“公共初始序列”的要求,这将允许我们通过其他标准布局类型在union中查看它。
可以根据需要构建更多内容;单组分运算符的float转换,支持算术等。
有了这样的类型,我们现在可以用这些代理类型构建Vector3对象。
struct Vector3
{
    union {
        float _storage[3]; // for easy initialization
        Vector3Proxy<0> x;
        Vector3Proxy<1> y;
        Vector3Proxy<2> z;
        Vector3Proxy<0,1> xy;
        Vector3Proxy<1,2> yz;
        Vector3Proxy<0,2> xz;
        // ...
    };
};

然后,这种类型可以轻松地用于一次性赋值给多个变量:
Vector3 x = {1,2,3};

x.xy = 5;

或将一个部分的组件分配给另一个部分:

Vector3 a = {1,2,3};
Vector3 b = {4,5,6};

a.xy = b.yz; // produces {5,6,3}

实时示例

此解决方案还确保sizeof(Vector3)不会改变,因为所有代理对象的大小相同。


注意:在C++中使用带有匿名structunion是无效的,尽管一些编译器支持它。因此,虽然重写如下可能很诱人:

union {
    struct {
        float x;
        float y;
        float z;
    }; // invalid, since this is anonymous
    struct {
        ...
    } xy;
}

这在标准C++中是无效的,也不是可移植的解决方案。

1
我该如何实现我的结构体,以便我可以像这样使用pos.xy = 10 // 更新x和ypos.yz = 20 // 更新y和zpos.xz = 30 // 更新x和z
只需添加必要的类成员函数即可:
struct Vector3 {
    float x;
    float y;
    float z;
    void update_xy(float value) { x = y = value; }
    void update_yz(float value) { y = z = value; }
    void update_xz(float value) { x = z = value; }
};

是的,有一种使用访问器的解决方案,但我想知道是否有一种不使用访问器的解决方案? - Hrant Nurijanyan
2
没有。 - Sam Varshavchik
@HrantNurijanyan 正如 @Sam 所提到的,没有其他方法可以解决这个问题。但是这样做有什么问题吗?您是在询问如何解决 XY 问题 吗?请编辑您的问题并详细说明您的具体用例。 - πάντα ῥεῖ
@πάνταῥεῖ 我只是好奇,像这样的东西是否能够实现。 - Hrant Nurijanyan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接