6

Let's say I have a struct

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

Note that sizeof(Vector3) must remain the same.

EDIT: I am interested in solutions without setters.

Not let's create an instance of that struct Vector3 pos . How can I implement my struct so I can have something like this pos.xy = 10 // updates x and y or pos.yz = 20 // updates y and z or pos.xz = 30 // updates x and z?

Hrant Nurijanyan
  • 789
  • 2
  • 9
  • 26
  • 1
    you can do that but it needs lots of boilerplate, cant you use methods? `pos.set_yz(20)` ? Would that answer your quesiton or are you specifically looking for `pos.yz = 20;` ? – 463035818_is_not_an_ai Nov 26 '20 at 18:15
  • What would `pos.xy = 10` mean? `pos.x = pos.y = 10`? – PiCTo Nov 26 '20 at 18:16
  • @idclev463035818 Yeah there is a solution with accessors, but I wanted to know whether there was without them... what do you mean by saying boilerplate?? Note: `sizeof(Vector3)` must remain the same. – Hrant Nurijanyan Nov 26 '20 at 18:17
  • @PiCTo exactly. – Hrant Nurijanyan Nov 26 '20 at 18:17
  • You can't do anything like this. C++ does not work this way. – Sam Varshavchik Nov 26 '20 at 18:18
  • boilerplate is code that you need to write to achieve something, but it serves no other purpose, it isnt nice code, the only reason to write it is because you want to achieve something that you cannot achieve more easily – 463035818_is_not_an_ai Nov 26 '20 at 18:18
  • 1
    This looks something like what is called swizzling. You can see an implementation of this in the GLM library. – Andrew Tomazos Nov 26 '20 at 18:19
  • 1
    Glm doesn't have swizzling either. It has some fancy functions to approximate it, but nothing like glsl's swizzles. And I strongly doubt swizzles in C++ would be possible without functions, macros or compiler support for this feature. – Lukas-T Nov 26 '20 at 18:20
  • @AndrewTomazos Yeah the idea of doing this came from writing OpenGL shaders... I just need to overload some more operators for my struct, that's why I need to create a new one – Hrant Nurijanyan Nov 26 '20 at 18:20
  • 2
    @churill: See 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 Nov 26 '20 at 18:22
  • @AndrewTomazos this code is interesting, actually in the code you mentioned this generates 3! possible structs... or have I got something wrong? – Hrant Nurijanyan Nov 26 '20 at 18:36
  • @HrantNurijanyan: I think so, haven't looked at it closely for a while sorry. – Andrew Tomazos Nov 26 '20 at 18:43
  • I thnik my question is answered here https://stackoverflow.com/questions/51641131/how-to-achieve-vector-swizzling-in-c – Hrant Nurijanyan Nov 26 '20 at 18:46
  • See also: https://glm.g-truc.net/0.9.1/api/a00002.html – Andrew Tomazos Nov 26 '20 at 18:48
  • To anyone who might want to close as duplicate, please find a way to merge the answers. Note that they aren't exactly the same. – Passer By Nov 27 '20 at 07:23

5 Answers5

3

Here is a solution that has the desired syntax, and doesn't increase the size of the class. It is technically correct, but rather convoluted:

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;
};

Another which relies on owner_of implemented here: https://gist.github.com/xymopen/352cbb55ddc2a767ed7c5999cfed4d31 which probably depends on some technically implementation specific (possibly undefined) behaviour:

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;
};
eerorika
  • 232,697
  • 12
  • 197
  • 326
  • In your link `offset_of` is undefined behavior (uses derefence of null), and also `reinterpret_cast`'s member pointers to integral values (which I believe violates any valid casts in the C++ language). I don't think any solution based on `owner_of` is valid in C++, even if "it works:" – Human-Compiler Nov 26 '20 at 18:50
  • Oh, you had the same idea as I had :) – G. Sliepen Nov 26 '20 at 18:50
  • @Human-Compiler Except that the indirection is within an addressof operator and is therefore not evaluated. It's true that the defect [issue](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#232) regarding this is still unresolved. – eerorika Nov 26 '20 at 18:58
2

The simple way is to provide setters for the combinations you want to set:

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);
}

And if you need sizeof(Vector3) to stay the same, thats the only way.


Just "for fun" this is how you can get pos.set_xy = 20; literally:

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;
}

However, it has severe downsides. First it can have almost twice the size of the original Vector3. Moreover, because the two_setter stores references, Vector3 cannot be copied. If it would store pointers, copying would be possible, but then even more code would be required to get it right.

Alternatively it is possible to provide a xy method that returns a proxy that assigns the two members. But I am not going into detail, because pos.xy() = 3; looks really odd, has no advantage to pos.xy(3) and you really should provide a setter (or just rely on the user making two assignments when they want to make two assignments ;).

TL;DR Use a method instead of trying to get a syntax that C++ does not support out of the box.

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
  • It would be a sad thing to have `Vector3` that large in size. – Evg Nov 26 '20 at 18:32
  • @Evg agree. It was just to show a way how it can be done in principle, because OP seems to be asking only out of curiosity, maybe I should be more clear about the "don't do it" – 463035818_is_not_an_ai Nov 26 '20 at 18:34
2

It is possible to create an empty struct inside Vector3 with an operator=() that sets the variables of the outer struct. Of course for a variable to really take no space itself, you have to use [[no_unique_address]], which is only available since C++20. But here is an example of how it might work:

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;
};

See it running on godbolt.org.

G. Sliepen
  • 7,637
  • 1
  • 15
  • 31
  • Maybe I can union them somehow, without using [[no_unique_address]]? – Hrant Nurijanyan Nov 26 '20 at 18:52
  • Is it valid to alias a member sub-object to the containing type? I know the inverse is true (you can `reinterpret_cast` a `foo` to `foo`'s first member), but I think this cast is technically undefined behavior – Human-Compiler Nov 26 '20 at 18:52
  • Something like this `union { struct {(Your struct); z}; struct { float x; float y; float z}};` – Hrant Nurijanyan Nov 26 '20 at 18:53
  • Yeap reinterpret_cast will work badly for your answer, but technically this is an answer) – Hrant Nurijanyan Nov 26 '20 at 18:54
  • @HrantNurijanyan anonymous `struct`s in unions don't exist in C++ (as your code example has). This would only work through compiler extensions, but is technically not valid code. However, there may still be a valid approach using `union`s – Human-Compiler Nov 26 '20 at 18:55
  • @Human-Compiler I'm not sure actually, but it seems like both GCC and Clang do seem to recognize that I'm aliasing the types, and calculate the expected output at compile time, see [this godbolt example](https://godbolt.org/z/frYTMz). – G. Sliepen Nov 26 '20 at 18:57
  • @Human-Compiler If anonymous structs inside the `union` don't work, you can still have a named struct holding the actual values, but then you just have to create single variable "swizzle" operators for `x`, `y` and `z` :) – G. Sliepen Nov 26 '20 at 19:00
  • 1
    @G.Sliepen Unfortunately, the compiler (or multiple compilers) producing the correct result does not mean that the solution is well-defined by the standard, or guaranteed to continue working on different compilers, optimization levels, etc. Compilers often let you alias things as an extension since a lot of valid C code can't be compiled otherwise. To your second point on Swizzling: that's what I think would need to be done as the only valid (per language spec) solution for this problem... A union of proxy types with the same common initial sequence – Human-Compiler Nov 26 '20 at 19:03
  • @Human-Compiler I agree it looks shady, however, GCC normally is very agressive with aliasing rules, and even if I hide the definition of `xy::operator=()` from the compiler, it seems to think it might change other member variables, see [this example](https://godbolt.org/z/sP7szW). Maybe there is some rule in the standard that makes this work after all? – G. Sliepen Nov 26 '20 at 19:14
  • It is allowed to cast between such pointers if the struct has standard layout. Aliasing does not play a role here, since there really is a `Vector3` object living at that address. – Quimby Nov 26 '20 at 19:33
1

How can I implement my struct so I can have something like this pos.xy = 10 // updates x and y or pos.yz = 20 // updates y and z or pos.xz = 30 // updates x and z?

Just add the necessary class member functions to do this:

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; }
};
πάντα ῥεῖ
  • 1
  • 13
  • 116
  • 190
1

Since your type is standard-layout, I think the only legal way to do this, as per the C++ standard, is with a union that contains sub-objects with custom operator= definitions.

With a union, you're allowed to view the common-initial sequence of the active member, provided all types are standard-layout types. So if we carefully craft an object that shares the same common members (e.g. 3 float objects in the same order), then we can "swizzle" between them without violating strict-aliasing.

For us to accomplish this, we will need to create a bunch of members that all have the same data in the same order, in standard-layout type.

As a simple example, lets create a basic proxy type:

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;
};

In this example, not all members of m_data are used -- but they exist so that the "common-initial sequence" requirement is satisfied, which will allow us to view it through other standard-layout types within the union.

This can be built up as much as you need; float conversion for single-component operators, support for arithmetic, etc.

With a type like this, we can now build a Vector3 objects out of these proxy types

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;
        // ...
    };
};

Then the type can easily be used to assign to multiple values at once:

Vector3 x = {1,2,3};

x.xy = 5;

Or to assign components of one part to another:

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

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

Live Example

This solution also ensures that sizeof(Vector3) does not change, since all proxy objects are the same size.


Note: It's not valid in C++ to use a union with anonymous structs, though some compilers support it. So although it might be tempting to rewrite this like:

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

This is not valid in standard C++, and would not be a portable solution.

Human-Compiler
  • 11,022
  • 1
  • 32
  • 59