C++向量模板分量操作

4

我正在重写项目中的向量数学部分,并希望通过它们的类型和维度数来概括向量。一个vector<T, N>表示类型为T,维度数为N的向量。

template<typename T, int N>
struct vector {
    T data[N];
};

我需要重写很多数学函数,其中大部分将按照每个组件的方式进行操作。下面是加法运算符的直接实现。

template<typename T, int N>
vector<T, N> operator+(vector<T, N> lhs, vector<T, N> rhs) {
    vector<T, N> result;
    for (int i = 0; i < N; i++) {
        result[i] = lhs[i] + rhs[i];
    }
    return result;
}

我的问题是:是否有一种方法(通过模板技巧?)可以不使用for循环和临时变量来实现这个问题?我知道编译器很可能会展开循环并优化它。但我不喜欢以这种方式实现所有关键的性能数学函数的想法。它们都将被内联和在头文件中,因此许多这样的函数也会使头文件变得又大又丑。
我想知道是否有一种方法可以产生更优化的源代码。可能像可变参数模板那样工作的方式。大致如下:
template<typename T, int N>
vector<T, N> operator+(vector<T, N> lhs, vector<T, N> rhs) {
    return vector<T, N>(lhs[0] + rhs[0], lhs[1] + rhs[1]...);
}
3个回答

0

一种实现这个的方法是通过较低级别的“map”函数:

下面是一个完整的工作示例

#include <iostream>
#include <math.h>

template<typename T, int N>
struct vector {
    T data[N];
};

首先声明你的工作器 "map" 函数 - 我这里有 3 个 mapmap2foreach

template<typename T, int N, typename FN>
static void foreach(const vector<T,N> & vec, FN f) {
   for(int i=0; i<N ;++i) {
      f(vec.data[i]);
   }
}

template<typename T, int N, typename FN>
static auto map(const vector<T,N> & vec, FN f) -> vector<decltype(f(T(0))), N> {
   vector<decltype(f(T(0))), N> result;
   for(int i=0; i<N ;++i) {
      result.data[i] = f(vec.data[i]);
   }
   return result;
}

template<typename T1, typename T2, int N, typename FN>
static auto map2(const vector<T1,N> & vecA, 
                 const vector<T2,N> & vecB, 
                 FN f)
 -> vector<decltype(f(T1(0), T2(0))), N> {
   vector<decltype(f(T1(0), T2(0))), N> result;
   for(int i=0; i<N ;++i) {
      result.data[i] = f(vecA.data[i], vecB.data[i]);
   }
   return result;
}

现在使用辅助函数通过lambda定义您的高级函数。我将定义二进制+、二进制-、一元-和e^x。哦,还有operator<<,这样我们就可以看到发生了什么。

我相信在operator+operator-中使用的lambda有更好的替代方案,但我记不起来了。

template<typename T, int N>
vector<T,N> operator+(const vector<T,N> &lhs, const vector<T,N> &rhs) {
  return map2(lhs, rhs, [](T a,T b) { return a+b;} );
}

template<typename T, int N>
vector<T,N> operator-(const vector<T,N> &lhs, const vector<T,N> &rhs) {
  return map2(lhs, rhs, [](T a,T b) { return a-b;} );
}

template<typename T, int N>
vector<T,N> operator-(const vector<T,N> &vec) {
  return map(vec, [](T a) { return -a;} );
}

template<typename T, int N>
auto exp(const vector<T,N> &vec) -> vector<decltype(exp(T(0))), N> {
  return map(vec, [](T a) { return exp(a); } );
}

template<typename T, int N>
std::ostream & operator<<(std::ostream& os, const vector<T,N> &vec) {
  os<<"{";
  foreach(vec, [&os](T v) { os<<v<<", "; } );
  os<<"}";
  return os;
}

现在看看它们是如何正常工作的...

int main() {
  vector<int, 5> v1 = {1,2,3,4,5};
  vector<int, 5> v2 = {2,4,6,8,10};

  std::cout<<v1 << " + " << v2 << " = " << v1+v2<<std::endl;
  std::cout<<v1 << " - " << v2 << " = " << v1-v2<<std::endl;
  std::cout<<" exp( - " << v2 << " )= " << exp(-v1)<<std::endl;
}

Lambda map函数是一个有趣的想法。它们肯定会缩短代码的长度,但仍然依赖于for循环。 - Janz
for循环的限制是编译时常量 - 编译器会展开它们。然而,如果您发现函数成为瓶颈,那么您可能需要选择更强大和经过测试的数值向量类 - 或手动专门处理瓶颈情况。 - Michael Anderson

0

你可以这样做,我会指向一个解决方案(编译和运行)。你想要摆脱循环,最好是通过内联它,希望编译器为你优化事情。

在实践中,我发现指定所需的维度即N = 3、4、5就足够了,因为这比你要求的更细粒度地控制了编译器的操作。但是,你可以使用递归和部分模板特化来实现你的运算符。我已经演示了加法。

所以,不要这样做:

template<typename T, int N>
vector<T, N> operator+(vector<T, N> lhs, vector<T, N> rhs) {
    vector<T, N> result;
    for (int i = 0; i < N; i++) {
        result[i] = lhs[i] + rhs[i];
    }
    return result;
}

你想要有效地实现以下代码:
   template<typename T, int N>
    vector<T, N> operator+(vector<T, N> lhs, vector<T, N> rhs) {
        vector<T, N> result;
        result[0] = lhs[0] + rhs[0];
        result[1] = lhs[1] + rhs[1];
        ...
        result[N-1] = lhs[N-1] + rhs[N-1];
        return result;
    }

如果N为1,那么很容易,你只需要这样做... 模板 vector operator+(vector lhs, vector rhs) { vector result; result[0] = lhs[0] + rhs[0]; return result; }

如果N为2,那么很容易,你只需要这样做... 模板 vector operator+(vector lhs, vector rhs) { vector result; result[0] = lhs[0] + rhs[0]; result[1] = lhs[1] + rhs[1]; return result; }

最简单的方法是将其定义为您预计使用的N的数量,而不是您正在寻找的答案,因为实际上您可能不需要超过N=5或N=6,对吧?

但是,您也可以使用部分模板特化和递归来实现。考虑这个结构体,它递归调用自身,然后分配索引:

template<typename T, int N, int IDX>
struct Plus
{
    void operator()(vector<T,N>& lhs, vector<T,N>& rhs, vector<T,N>& result)
    {
        Plus<T,N,IDX-1>()(lhs,rhs,result);
        result.data[IDX] = lhs.data[IDX] + rhs.data[IDX];
    }
};

还有这个偏特化的情况,看起来什么也没有做,但是处理索引为0的情况,并结束递归:

template<typename T, int N>
struct Plus<T,N,-1>
{
    void operator()(vector<T,N>& lhs, vector<T,N>& rhs, vector<T,N>& result)
    {
        //noop
    }
};

最后是 operator+ 的实现,它实例化 Plus 并调用它:
template<typename T, int N>
vector<T, N> operator+(vector<T, N> lhs, vector<T, N> rhs) {
    vector<T, N> result;
    Plus<T,N,N-1>()(lhs,rhs,result);
    return result;
}

你需要将这个转换成一个操作符,使其更具有通用性,但你已经有了想法。然而,这对编译器来说是一种负担,在大型项目中可能需要花费很长时间,即使它非常酷。实际上,我发现手动输入所需的重载或编写脚本代码以生成C++结果会导致更易于调试的体验和最终更简单易读、更容易优化的代码。更具体地说,如果你编写一个脚本来生成C++,你可以在第一时间包含SIMD指令,而不是留下任何机会。


我考虑过“模板循环”,暂且这么称呼吧。我同意它们可能会对编译器造成一些压力。我的原始计划是为最常见的向量大小专门设计函数,并将通用的for循环函数留给其他任何大小。 - Janz

0
  • 首先,编译器可能会展开循环。
  • 其次,为了更好的性能,通过const引用传递参数而不是值,以避免额外的拷贝。

回答你的问题,你可以使用std::index_sequence来构建一个地方,类似于:

namespace detail
{

template<typename T, int N, std::size_t...Is>
vector<T, N> add(std::index_sequence<Is...>,
                 const vector<T, N>& lhs,
                 const vector<T, N>& rhs)
{
    return {{ (lhs[Is] + rhs[Is])... }};
}

}

template<typename T, int N>
vector<T, N> operator+(const vector<T, N>& lhs, const vector<T, N>& rhs) {
    return detail::add(std::make_index_sequence<N>{}, lhs, rhs);
}

演示


我目前通过const引用传递向量,但我计划最终进行一些分析,以查看通过引用或值传递它们是否更好。感谢您让我注意到index_sequence。它看起来可能非常有用。 - Janz

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