使用成员变量创建数学向量,例如 XYZW 和 RGBA 的访问

Create mathematical vector with member variable like access for XYZW and RGBA

提问人:Joel 提问时间:9/15/2023 更新时间:9/15/2023 访问量:88

问:

我想创建一个类(不像,而是一个数学向量),它接受基础类型及其大小的模板参数。Vectorstd::vector

访问其元素将如下所示:

// template <typename T, std::size_t N>
Vector<std::uint8_t, 3> vec{};

vec.x = 10;
vec.y = 12;
vec[2] = 14;

// if the size is 4: 
// vec.w = 16;

我想要 ,如果它的大小是 3 和 4,则可以访问,但如果它的大小小于 3 或大于 4,则它们都不能访问,并且与 RGBA 相同。我不想使用方法(如),只需像普通成员变量一样访问它们。在所有情况下,我仍然希望能够拥有自己的下标运算符。.x.y.z.w.x()

我的想法是创建一个“基”类,该类将包含所有通用方法,然后从中派生。Vector

template <typename T, std::size_t N>
struct Vector {
};

template <typename T>
struct Vector<T, 3> : Vector<T, 1> {
    union {
        T x, r;
    };
    union {
        T y, g;
    };
    union {
        T z, b;
    };
};

template <typename T>
struct Vector<T, 4> : Vector<T, 3> {
    union {
        T w, a;
    };
};

但是,要实现某些方法(例如下标运算符),我需要在“基”类中为数据(如)创建一个数组。这将使数组和联合成为内存中的不同对象,并且根本不起作用。T data[N];

我现在想知道是否有一些方法可以实现所需的行为,而无需创建一些额外的类(例如)并为那些大小从 2 到 4 的类提供重复代码。Vector3<T>Vector

C++ 向量 联合

评论

2赞 Sam Varshavchik 9/15/2023
可以重载。所以,你看到的是一个专门的类,不需要任何联合,只是带有这些元素的普通字段,以及一个重载的 .打字的公制废话,但绝对可行。[]operator[]
0赞 Joel 9/15/2023
@SamVarshavchik我确实知道运算符重载,但是当我不使用 、 、 (没有函数调用)时,我怎样才能访问相同的数据?data[N]xyzwunion
0赞 HolyBlackCat 9/15/2023
我不会这样做。虽然从表面上看,联合是良性的,但对于非平凡的类(具有非平凡的构造函数/析构函数的类)来说,它们并不开箱即用,虽然这些类很少以向量结束,但它仍然是一个问题。

答:

1赞 Jan Schultke 9/15/2023 #1

您不一定需要将数组作为成员。你只需要实现,所有其他运算符(如、等)都变得很容易实现:operator[]+-

#include <cstddef>
#include <utility>

template <typename T, std::size_t N>
struct Vector {
    const T& operator[](std::size_t i) const {
        // obtain pointer to the bytes of this object
        auto bytes = reinterpret_cast<const std::byte*>(this);
        // access the T object which is presumably stored at this byte offset
        return *reinterpret_cast<const T*>(bytes + i * sizeof(T));
    }

    T& operator[](std::size_t i) {
        // One of the few safe use cases of const_cast:
        // Add an unnecessary const and remove it with const_cast.
        return const_cast<T&>(std::as_const(*this)[i]);
    }
};

这种实现假设所有的 s 都布置在一个数组中,这是一个合理的假设。 请注意,这是无效的,因为不是 的数组,并且对此类指针进行指针算术操作将是未定义的行为。Tstructreinterpret_cast<T*>VectorT

您可以通过以下方式确保布局正确:

static_assert(sizeof(Vector<int, 4>) == sizeof(int[4]));

如果您的类型具有 (但是实现,则其他操作将变得易于实现。operator[]

template <typename T, std::size_t N>
Vector<T, N> operator+(const Vector<T, N>& a, const Vector<T, N>& b) {
    Vector<T, N> result;
    for (std::size_t i = 0; i < N; ++i) {
        result[i] = a[i] + b[i];
    }
    return result;
}

指针算术注意事项std::byte

从技术上讲,对对象的底层序列进行指针算术是未定义的行为,但这更像是标准的缺陷,没有编译器强制执行。看:std::byte

评论

0赞 Joel 9/15/2023
谢谢你的这个想法。当我以与我的问题类似的方式实现我的 s(从 3 继承 4,但从 0 继承 3)并重载/指定(不确定术语)那些具有 1、2 和高于 4 的值时,我可能会得到想要的结果。我明天会研究这个问题。Vector
0赞 Mooing Duck 9/15/2023
我宁愿切换未定义的行为 coliru.stacked-crooked.com/a/a61fc448d08a102f
0赞 Jan Schultke 9/15/2023
@MooingDuck a 需要为每个长度执行额外的工作,并且不会产生最佳的 codegen。请参见 godbolt.org/z/zPdE3TMs5。这是一个可能的解决方案,只是远非最佳。switch
0赞 Mooing Duck 9/15/2023
@JanSchultke:对我来说看起来是最佳的 godbolt.org/z/9WjG58E4nmov eax, DWORD PTR [rsi]
0赞 Jan Schultke 9/15/2023
@MooingDuck它通过内联和持续传播得到优化,因为这是一个微不足道的例子。你可以很容易地想出一个不那么平凡的例子:godbolt.org/z/bsYWx4Wrb 使用函数获得完全幼稚的代码生成,而使用函数的函数获得矢量化操作。您可能会看到基准测试存在显着差异。(tbf 我认为 GCC 在这里绊倒了;在这种特定情况下,矢量化版本是否更好并不明显)switchreinterpret_cast