如何使用相同的呼叫签名索引和分配张量中的元素?

时间:2012-12-03 07:56:55

标签: c++ arrays vector matrix variable-assignment

好吧,我一直在谷歌上搜索太长时间,我只是不确定该怎么称呼这种技术,所以我认为最好在这里询问SO。如果我有一个明显的名称和/或解决方案我忽略了,请指出正确的方向。

对于外行人来说: tensor 是矩阵的逻辑扩展,就像矩阵是向量的逻辑扩展一样。向量是秩-1张量(在编程术语中,1D数字数组),矩阵是秩-2张量(数字的2D数组),而秩-N张量则是数字的ND数组。

现在,假设我有类似Tensor类的东西:

template<typename T = double> // possibly also with size parameters
class Tensor
{
  private: 
    T *M;  // Tensor data (C-array)
           // alternatively, std::vector<T> *M 
           // or std::array<T> *M 
           // etc., or possibly their constant-sized versions
           // using Tensor<>'s template parameters

  public: 

    ... // insert trivial fluffy stuff here 

    // read elements
    const T & operator() (size_t a, size_t b) const {
        ... // error checks etc.
        return M[a + rows*b];
    }

    // write elements
    T & operator() (size_t a, size_t b) {
        ... // error checks etc.
        return M[a + rows*b];
    }

    ... 

};

使用operator()(...)的这些定义,索引/分配单个元素则具有相同的调用签名:

Tensor<> B(5,5);
double a = B(3,4);   // operator() (size_t,size_t) used to both GET elements
B(3,4) = 5.5;        // and SET elements

将其扩展到任意张量等级是相当简单的。但我希望能够实现的是一种更高级的索引/分配元素的方法:

Tensor<> B(5,5);
Tensor<> C = B( Slice(0,4,2), 2 );  // operator() (Slice(),size_t) used to GET elements
B( Slice(0,4,2), 2 ) = C;           // and SET elements 
         // (C is another tensor of the correct dimensions)

我知道std::valarray(以及其他许多人)已经做了一件非常相似的事情,但是完成行为不是我的目标;我的目标是学习如何优雅高效安全将以下功能添加到我的Tensor<>课程中:

// Indexing/assigning with Tensor<bool>
B( B>0 ) += 1.0;   

// Indexing/assigning arbitrary amount of dimensions, each dimension indexed 
// with either Tensor<bool>, size_t, Tensor<size_t>, or Slice()
B( Slice(0,2,FINAL), 3, Slice(0,3,FINAL), 4 ) = C; 

// double indexing/assignment operation
B(3, Slice(0,4,FINAL))(mask) = C;  // [mask] == Tensor<bool>

.. etc.

请注意,我打算将operator[]用于operator()的未检查版本。或者,我会更加坚持使用std::vector<>方法对.at()的已检查版本使用operator[]方法。无论如何,这是一个设计选择,除了现在的问题。

我想到了以下不完整的“解决方案”。这种方法只能用于矢量/矩阵(rank-1或rank-2张量),并且有许多不良的副作用:

// define a simple slice class
Slice () 
{ 
  private:
    size_t 
        start, stride, end; 

  public: 
    Slice(size_t s, size_t e) : start(s), stride(1), end(e) {}
    Slice(size_t s, size_t S, size_t e) : start(s), stride(S), end(e) {}
    ...

};

template<typename T = double>
class Tensor
{
    ... // same as before

  public:       

    // define two operators() for use with slices:     

    // version for retrieving data
    const Tensor<T> & operator() (Slice r, size_t c) const {
        // use slicing logic to construct return tensor
        ...
        return M;
    {

    // version for assigning data
    Sass operator() (Slice r, size_t c) {
        // returns Sass object, defined below
        return Sass(*this, r,c);
    }

  protected:

    class Sass 
    {
        friend class Tensor<T>;

     private:        
        Tensor<T>& M;
        const Slice &R;
        const size_t c;

      public:

        Sass(Tensor<T> &M, const Slice &R, const size_t c)
            : M(M)
            , R(R)
            , c(c)
        {}

        operator Tensor<T>() const { return M; }

        Tensor<T> & operator= (const Tensor<T> &M2) {
            // use R/c to copy contents of M2 into M using the same 
            // Slice-logic as in "Tensor<T>::operator()(...) const" above
            ...

            return M;
        }

    };  

但这感觉不对......

对于上面列出的每种索引/分配方法,我必须为每个此类构建器定义一个单独的Tensor<T>::Sass::Sass(...)构造函数,一个新的Tensor<T>::Sass::operator=(...)和一个新的Tensor<T>::operator()(...)操作。此外,Tensor<T>::Sass::operators=(...)需要包含相应Tensor<T>::operator()(...)中已有的大部分相同内容,并使所有内容适合任意等级的Tensor<> 这种方法相当丑陋,过于冗长,更重要的是,完全无法管理。

所以,我认为对所有这些都有更有效的方法。

有什么建议吗?

3 个答案:

答案 0 :(得分:3)

首先,我想指出一些设计问题:

T & operator() (size_t a, size_t b) const; 

建议您不能通过此方法更改矩阵,因为它是const。但是你给了一个矩阵元素的nonconst引用,所以实际上你可以改变它。这只是因为你正在使用的原始指针而编译。我建议改用std::vector,它会为你做内存管理,并会给你一个错误,因为vector {const} operator[]的const版本给出了一个像它应该的const引用。

关于你的实际问题,我不确定Slice构造函数的参数应该做什么,也不确定Sass对象是什么意思(我不是母语,而“Sass”只给了我一个字典翻译,意思是......像“无礼”,“无礼”。 但是,我想用切片创建一个对象,可以访问由切片参数定义的矩阵子集。

我建议不要使用operator()来访问矩阵。具有两个索引来访问给定元素的op()似乎很自然。使用类似的运算符来获取整个矩阵似乎不太直观。

这是一个想法:创建一个Slice类,其中包含对Matrix的引用以及定义Matrix的哪个部分由Slice表示的必要参数。这样,Slice就像它定义的Matrix子集的代理,类似于一对迭代器,可以看作是它们指向的容器子范围的代理。为您的Matrix提供一对slice()方法(const和nonconst),这些方法返回Slice / ConstSlice,引用您调用方法的Matrix。这样,您甚至可以将检查放入方法中,以查看Slice的参数是否对其引用的Matrix有意义。如果它有意义并且是必要的,您还可以添加转换运算符,将Slice转换为自己的Matrix。

一次又一次地重载operator()并使用参数作为掩码,因为线性索引和其他东西比帮助imo更令人困惑。 operator()是光滑的,如果它做了一些自然的东西,每个人都希望它。如果代码在任何地方使用,它只会混淆代码。改为使用命名方法。

答案 1 :(得分:0)

不是答案,只是跟进我的评论的注意事项:

Tensor<bool> T(false);
// T (whatever its rank) contains all false
auto lazy = T(Slice(0,4,2));
// if I use lazy here, it will be all false
T = true;
// now T contains all true
// if I use lazy here, it will be all true

这个可能是您想要的,或者可能是意料之外的。

一般来说,这可以使用不可变张量干净地工作,但是允许变异会产生与COW字符串相同的问题。

答案 2 :(得分:0)

如果你允许你的Tensor隐含为double,那么你只能从operator()重载中返回Tensors。

operator double() { 
    return M.size() == 1 ? M[0] : std::numeric_limits<double>::quiet_NaN(); 
};

应该允许

double a = B(3,4);
Tensor<> a = B(Slice(1,2,3),4); 

要让operator()使用Slice和integer来处理多个重载是另一个问题。我可能只是使用Slice并创建另一个隐式转换,因此整数可以是Slice,然后可能使用变量参数elipses。

const Tensor<T> & operator() (int numOfDimensions, ...)

虽然变量参数路由是最好的,只有8个特殊化的Slice 1-8参数。