【问题标题】:How to update multiple fields of a struct simultaneously?如何同时更新结构的多个字段?
【发布时间】:2020-11-26 18:14:20
【问题描述】:

假设我有一个结构

struct Vector3 {
    float x;
    float y;
    float z;
};

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

编辑:我对没有设置器的解决方案感兴趣。

不要让我们创建该结构 Vector3 pos 的实例。我怎样才能实现我的结构,以便我可以拥有类似pos.xy = 10 // updates x and ypos.yz = 20 // updates y and zpos.xz = 30 // updates x and z 的东西?

【问题讨论】:

  • 你可以这样做,但它需要大量的样板,你不能使用方法吗? pos.set_yz(20) ?这会回答您的问题还是您专门寻找pos.yz = 20;
  • pos.xy = 10 是什么意思? pos.x = pos.y = 10?
  • 这看起来有点像所谓的 swizzling。您可以在 GLM 库中看到它的实现。
  • Glm 也没有 swizzling。它有一些花哨的函数来近似它,但没有像 glsl 的 swizzles 那样。而且我强烈怀疑如果没有函数、宏或编译器对此特性的支持,C++ 中的 swizzles 是可能的。

标签: c++ struct field accessor


【解决方案1】:

这是一个具有所需语法的解决方案,并且不会增加类的大小。它在技术上是正确的,但相当复杂:

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 是未定义的行为(使用 null 的取消引用),还有 reinterpret_cast 的指向整数值的成员指针(我认为这违反了 C++ 语言中的任何有效转换)。我不认为任何基于owner_of 的解决方案在 C++ 中是有效的,即使“它有效:”
  • 哦,你的想法和我一样:)
  • @Human-Compiler 除非间接是在 addressof 运算符内,因此不进行评估。确实,关于这个的缺陷issue仍然没有解决。
【解决方案2】:

简单的方法是为要设置的组合提供设置器:

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) 没有任何优势,你真的应该提供一个设置器(或者当他们想要进行两个分配时,只依赖于用户进行两个分配;)。

TL;DR 使用方法而不是尝试获取 C++ 开箱即用不支持的语法。

【讨论】:

  • Vector3 有这么大的尺寸是一件可悲的事情。
  • @Evg 同意。这只是为了展示一种原则上如何做到这一点,因为 OP 似乎只是出于好奇才问,也许我应该更清楚“不要这样做”
【解决方案3】:

可以在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 上运行。

【讨论】:

  • 也许我可以在不使用 [[no_unique_address]] 的情况下以某种方式合并它们?
  • 将成员子对象别名为包含类型是否有效?我知道反之亦然(您可以将reinterpret_cast foo 成为foo 的第一个成员),但我认为这种演员阵容在技术上是未定义的行为
  • 类似union { struct {(Your struct); z}; struct { float x; float y; float z}};
  • 是的,reinterpret_cast 对您的答案效果不佳,但从技术上讲,这是一个答案)
  • @G.Sliepen 不幸的是,产生正确结果的编译器(或多个编译器)并不意味着该解决方案已被标准明确定义,或者保证可以继续在不同的编译器、优化级别上工作等。编译器通常让您将事物别名为扩展,因为许多有效的 C 代码无法以其他方式编译。关于 Swizzling 的第二点:我认为这是解决此问题的唯一有效(按语言规范)解决方案......具有相同公共初始序列的代理类型的联合
【解决方案4】:

由于您的类型是标准布局,我认为按照 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的所有成员都被使用了——但是它们的存在是为了满足“公共初始序列”的要求,这将允许我们通过@中的其他标准布局类型来查看它987654328@.

这可以根据您的需要构建; 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}

Live Example

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


注意:在 C++ 中使用 union 和匿名 structs 是无效的,尽管一些编译器支持它。因此,尽管将其重写为:

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

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

【讨论】:

    【解决方案5】:

    我怎样才能实现我的结构,这样我才能拥有类似 pos.xy = 10 // updates x and ypos.yz = 20 // updates y and zpos.xz = 30 // updates x and 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; }
    };
    

    【讨论】:

    • 是的,有一个带有访问器的解决方案我很感兴趣是否有一个没有它们的解决方案?
    • 不,没有。
    • @HrantNurijanyan 正如 @Sam 所述,没有其他方法可以做到这一点。但这有什么问题?您是在问如何解决XY problem?你能编辑你的问题并详细说明你的具体用例吗?
    • @πάνταῥεῖ 不,我只是好奇能不能实现这样的目标)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-12-28
    • 1970-01-01
    • 2012-08-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多