类模板特化中的成员变量别名

Posted

技术标签:

【中文标题】类模板特化中的成员变量别名【英文标题】:Member variable alias in class template specialization 【发布时间】:2019-01-03 18:47:24 【问题描述】:

假设我正在编写一个Vector 模板类来表示 N 维空间中的点和向量。类似于以下内容:

template <typename T, int N>
struct Vector

    T data[N];
    // ...
;

让我们进一步假设,无论出于何种原因,我希望用户能够在较小向量的情况下使用有意义的名称访问data,例如使用v.xv.y代替v.data[0]v.data[1]

我有两个额外的限制。

    对向量的xy 组件的访问不应写为函数调用(例如,它应该是v.x,而不是v.x())。 以下等式必须满足sizeof(Vector&lt;T, N&gt;) == N * sizeof(T)

我查看了不同的可能方法,包括成员变量引用、标签调度甚至CRTP,但没有一个能满足我的所有要求。

甚至可以创建这样的别名吗?如果是的话,怎么能做到呢?

【问题讨论】:

v.x() 有什么问题? 对象必须是唯一可寻址的,成员从不共享地址。这意味着非静态数据成员,即使是无状态的数据成员,对类的大小都有有效的非零影响。所以坚持添加数据成员(Vector::x) 并坚持不增加班级规模对我来说似乎是矛盾的。 unions 是个例外,但即使在这种情况下,在给定时间也只能有一个成员处于活动状态。它不适合使用别名。 C++ 没有变量别名。它确实有参考,但那些可以/将会带来空间成本。作为一个可能的零成本抽象可以写 int&amp; x = my_vector.data[0]; 在使用站点中创建自己的别名,并且可能会被优化掉。 @FrançoisAndrieux v.x() 没有任何问题。这个问题只是一个借口,试图解决我一直在摆弄的一个问题。 @FrançoisAndrieux 就像 NathanOliver 指出的那样,关于不增加类大小的评论与我提到的一种方法有关,即成员引用。如果我添加对data 的每个单元格的引用,有效地给它们命名,我将增加Vector 的整体大小。 【参考方案1】:

(这不是一个答案,它是一个带有代码示例的注释,它不适合作为注释,并且如果它可以塞进注释中,则格式不好。)

你能否换个方向,将向量表示为一堆字段,然后将索引 getter/setter 映射到每个字段?

取出N个模板参数简化问题:

#include <iostream>
#include <stdexcept>

template <typename T>
struct Vector3

    T x;
    T y;
    T z;
    T operator[](int i) const
    
        switch(i)
        
            case 0:
                return x;
            case 1:
                return y;
            case 2:
                return z;
            default:
                throw std::out_of_range("out of range");
        
    
    T& operator[](int i)
    
        switch(i)
        
            case 0:
                return x;
            case 1:
                return y;
            case 2:
                return z;
            default:
                throw std::out_of_range("out of range");
        
    
;

int main()

    Vector3<float> v;
    v.x = 1.0f;
    v[1] = 2.0f;
    v.z = 3.0f;
    std::cout << v[0] << " " << v.y << " " << v[2] << '\n';

【讨论】:

N 模板参数很重要,因为它首先解决了我的问题。我已经有一个运行良好的 Vector3&lt;T&gt; 类,但我想分解代码并引入任意大小的向量。 嗯,但是每个命名字段都需要对每个支持的任意大小进行部分特化。哪一种违背了目的,除非它能够说Vector&lt;int, 3&gt;而不是Vector3&lt;int&gt; 最大的挑战是禁用基于N 的成员。也许您可以有条件地从提供xyz 的基本类型继承? @Eljay 目标是拥有类似using Vector3&lt;T&gt; = Vector&lt;T, 3&gt; 的东西,其中Vector&lt;T, 3&gt;Vector&lt;T, N&gt; 的专用版本,它将包含xyz 访问器。 @FrançoisAndrieux 这类似于标签调度,但用于成员变量。不幸的是,我没有找到任何方法将这些技术应用于变量,仅应用于成员函数。【参考方案2】:

如果允许,那么它似乎是可行的。

第一次尝试(不错但不完美...)

int main() 
    Vector<int, 4> vec;
    vec[0] = 1; // same as: vec.t1 = 1;
    vec[1] = 2; // same as: vec.t2 = 2;
    vec[2] = 3; // same as: vec.t3 = 3;
    vec[3] = 4; // same as: vec.t4 = 4;
    std::cout << vec.t1 + vec.t2 + vec.t3 + vec.t4; // 10

要达到上述目的:

#define VAR_NAME(num) t##num

#define DefineVector(num) \
    template<typename T> \
    struct Vector<T, num> : Vector<T, num-1>  \
        T VAR_NAME(num); \
        T& operator[](int index)  \
            if(index == num-1) return VAR_NAME(num); \
            return Vector<T, num-1>::operator[](index); \
         \
    

template<typename T, size_t N>
struct Vector;

template<typename T>
struct Vector<T, 1> 
    T t1;
    T& operator[](int index) 
        // in case index != 0 this is UB
        return t1;
    
;

DefineVector(2);
DefineVector(3);
DefineVector(4);

// TODO:
// replace 3 declarations above with a single *DefineVectorsRecursively(4);*
// by using recursive macros
// see: https://***.com/questions/12447557/can-we-have-recursive-macros
// leaving this as a further exercise...

http://coliru.stacked-crooked.com/a/42625e9c198e1e58

编辑:添加 operator[] 以解决评论中提出的问题。


第二次尝试:使用更好的字段名称

OP 要求字段具有更好的名称,例如 x、y、z。

这是一个挑战。但是宏又来了:

int main() 
    Vector<int, 3> vec;
    vec[0] = 1;
    vec[1] = 2;
    vec[2] = 3;
    std::cout << vec.x + vec.y + vec.z; // 6

使用以下代码:

#include <boost/preprocessor/variadic/size.hpp>

template<typename T, size_t DIMENSIONS>
struct Vector;

#define DefineVector(VAR, ...) \
    template<typename T> \
    struct Vector<T, BOOST_PP_VARIADIC_SIZE(__VA_ARGS__) + 1> \
      : Vector<T, BOOST_PP_VARIADIC_SIZE(__VA_ARGS__)>  \
        T VAR; \
        T& operator[](int index)  \
            if(index == BOOST_PP_VARIADIC_SIZE(__VA_ARGS__)) return VAR; \
            return Vector<T, BOOST_PP_VARIADIC_SIZE(__VA_ARGS__)>::operator[](index); \
         \
    

#define DefineVector1(VAR) \
    template<typename T> \
    struct Vector<T, 1>  \
        T VAR; \
        T& operator[](int index)  \
            /* in case index != 0 this is UB */ \
            return VAR; \
         \
    

DefineVector1(x);
DefineVector(y, x);
DefineVector(z, y, x);
// TODO: create recursive macro for DefineVector(z, y, x)
// that will create the two above recursively

代码:http://coliru.stacked-crooked.com/a/2550eede71dc9b5e


但是等等,operator[] 效率不高

如果 T 是标准布局类型,我想有一个更有效的 operator[],并具有以下实现:

#define DefineVector(VAR, ...) \
    template<typename T> \
    struct Vector<T, BOOST_PP_VARIADIC_SIZE(__VA_ARGS__) + 1> \
      : Vector<T, BOOST_PP_VARIADIC_SIZE(__VA_ARGS__)>  \
        T VAR; \
    T& operator[](int index)  \
        if constexpr(std::is_standard_layout_v<T>)  \
            return *(&VAR - (BOOST_PP_VARIADIC_SIZE(__VA_ARGS__) - index)); \
         else  \
            if(index == BOOST_PP_VARIADIC_SIZE(__VA_ARGS__)) return VAR; \
            return Vector<T, BOOST_PP_VARIADIC_SIZE(__VA_ARGS__)>::operator[](index); \
         \
     \

(错误)尝试:http://coliru.stacked-crooked.com/a/d367e770f107995f

很遗憾 - 上述优化是非法的

对于呈现的实现,任何 DIMENSIONS > 1 的 Vector 不是标准布局类(即使 T 是)because it has members both in base class and in the derived class:

10.1 [class.prop]

[3] 类 S 是标准布局类,如果它: ...

[3.6] 具有类及其基类中的所有非静态数据成员和位域 在同一个类中首先声明的类...

所以上面的优化尝试具有未定义的行为 - 编译器没有义务按顺序保持继承层次结构中成员的地址。

最初的解决方案仍然有效。

【讨论】:

这样T data 数组就丢失了。 OP 希望通过别名访问它:“我希望用户能够在较小的向量的情况下访问具有有意义名称的数据,例如通过使用 v.x 或 v.y 而不是 v.data[0] 和 v.data[1] ”。否则,他们可以声明... Vector&lt;T, 3&gt; T x, y, z; ; 添加运算符 []【参考方案3】:

这是可能的解决方案(尽管我认为这是不好的做法,并且不确定是否可移植):

template <typename T, int N>
union Vector

    struct  T x, y, z; ;
    T data[N];
;

这里是发生了什么的例子:

int main() 
    Vector<int, 10> vec;
    vec.x = 100;
    vec.y = 200;
    vec.z = 300;
    vec.data[3] = vec.data[2] + 100;

    printf("%d %d %d %d\n", vec.data[0], vec.data[1], vec.data[2], vec.data[3]);
    printf("size = %d\n", (int) sizeof(vec));

    return 0;


Output:
    100 200 300 400
    size = 40

更新: 并让well defined 成为您可以这样做:

template <typename T, int N> union Vector;

template <typename T> union Vector<T, 1> 
    struct  T x; ;
    T data[1];
;

template <typename T> union Vector<T, 2> 
    struct  T x, y; ;
    T data[2];
;

template <typename T> union Vector<T, 3> 
    struct  T x, y, z; ;
    T data[3];
;

template <typename T> union Vector<T, 4> 
    struct  T x, y, z, w; ;
    T data[4];
;

只要确保struct 是标准布局(即这适用于 T = int、float、double 等)。


更新 2: 请注意,上面可能仍然是 UB,因为 T x, y, zT data[3] 似乎实际上不兼容布局(请参阅 here)。尽管如此,这种模式似乎被用于各种库中,用于实现简单的向量类型 - example1 (GLM)、example2 video、example3

【讨论】:

如果它不是最后一个被分配的联合成员,则从它读取是未定义的行为。这不是解决方案。 这是所有非平凡类型的 UB。 此外,即使Vector&lt;T, 2&gt; (?),此解决方案也可以使z 访问。 @NathanOliver 据我了解,它是所有类型的 UB。您能否链接到解释为什么它可以用于琐碎类型的参考? @GeorgiGerganov 这似乎是所有提供向量类型的小型数学库使用的通用模式。

以上是关于类模板特化中的成员变量别名的主要内容,如果未能解决你的问题,请参考以下文章

通过类模板特化访问成员数据

C++模板编程中只特化模板类的一个成员函数(花样特化一个成员函数)

通过类的模板参数特化成员模板结构

C++中模板类成员函数的特化

这是为成员函数的 C++ 模板部分特化构造类的好方法吗?

如何制作特定类的成员函数或变量的别名(如 STL 容器)