如何使我自己的容器索引和可分配

how to make my own container indexed and assignable

本文关键字:索引 可分配 何使我 自己的      更新时间:2023-10-16

我有一个这样的容器类:

class A {
public:
    // assuming all the other operators used below 
    // have been defined.
    // ...
    A operator()(const A& a) const { 
      A r(a.size());
      for (int i = 0;i < a.size();++i) r[i] = data[a[i]]; 
      return r; 
    }
private:
    std::vector<int> data;
};

所以我可以这样做:

A a, b, c;
// ... initialize data here...
c = a(b); // I can index a by b

现在我想让索引容器a(b)可赋值,例如

a(b) = c;
例如,如果a{1, 2, 3, 4}, b{0,2}, c{0,0},上面的行应该给我a = {0,2,0,4}。因为{0,2}索引的aa中的{1,3},设置它们为c {0,0}会得到这个。怎么做呢?

不能直接使用A类型。您需要一个中间类型来映射或引用回对象。作为一个简单的非最优示例:

#include <functional>
#include <vector>
class A
{
public:
    class Ref
    {
    private:
        friend class A;
        std::vector<std::reference_wrapper<int>> refs;
        Ref( A & obj, A & idx )
        {
            refs.reserve( idx.data.size() );
            auto val_it = obj.data.begin();
            for( auto i : idx.data ) {
                ref.emplace_back( std::ref( data[i] ) );
            }
        }
    public:
        Ref & operator=( const A & obj )
        {
            auto obj_it = obj.data.begin();
            for( auto ref : refs ) {
                ref.get() = *obj_it++;
            }
            return this;
        }
    };
    // ...
    Ref operator()( const A & idx )
    {
        return Ref( *this, idx );
    }
private:
    std::vector<int> data;
};

然后事情开始变得棘手,因为你会想要能够在这些基于引用的视图和原始类型之间转换

简单的方法是保持const操作符的实现(注意我的操作符是非const):即它仍然返回类型A,这是有意义的。但是您可能希望能够从A::Ref构建新的A,因此您需要这样的构造函数:

A::A( const Ref & r )
{
    data.reserve( r.refs.size() );
    auto r_it = r.refs.begin();
    for( auto ref : r.refs ) {
        data.push_back( ref.get() );
    }
}

无论如何,这是一个简单的概念,可以让您开始使用。

数组视图是T连续缓冲区中的视图:

template<class T>
struct array_view {
  T* b=0;
  T* e=0;
  T* begin() const { return b; }
  T* end() const { return e; }
  std::size_t size() const { return end()-begin(); }
  bool empty() const { return end()==begin(); }
  T& operator[](std::size_t i)const { return begin()[i]; }
  array_view( array_view const& ) = default;
  array_view& operator=( array_view const& ) = default;
  array_view() = default;
  array_view( T* s, T* f ):b(s),e(f) {}
  array_view( T* s, std::size_t n ):array_view(s, s+n) {}
};
template<class Src>
array_view< std::remove_reference_t<
  decltype(*(std::declval<Src&>().data()))
> >
make_array_view( Src& src ) {
  return {src.data(), src.size()};
}

(为了使它更好,做一般的"范围"升级。在上面的例子中,const指的是"改变被查看的范围",而不是内容——如果你想要const的内容,创建一个array_view<const T>。另一个改进是让vae构造函数做make_array_view做的事情,也支持initializer_list、右值和原始C数组)。

给出T的数组视图的排列视图。

首先,置换是从size_t的有界集合到另一个size_t的有界集合的函数。

struct permutation {
  std::function< std::size_t(std::size_t) > mapping;
  std::size_t count = 0;
  std::size_t size() const { return count; }
  permutation( std::function< std::size_t(std::size_t) > m, std::size_t c):
    mapping(std::move(m)),
    count(c)
  {}
  std::size_t operator()( std::size_t i )const {
    return mapping(i);
  }
};

这不是最安全的,因为我们没有检查输出范围是否合理。

工厂函数:

template<class T>
permutation make_permutation_from( T src ) {
  auto size = src.size();
  return {
    [src = std::move(src)]( std::size_t in ) {
      return src[in];
    },
    size
  };
}
// optimization
permutation make_permutation_from( permutation src ) {
  return src;
}

和1组成两个排列。未检查size字段的有效性。

// if they don't align, we are screwed, but...
permutation chain_permutation( permutation first, permutation second ) {
  auto first_size = first.size();
  return {
    [first=std::move(first), second=std::move(second)](std::size_t i){
      return second(first(i));
    },
    first_size
  };
}

这将我们引向permuted view,它是array_view的一个视图,用于排列索引。

template<class T>
struct permuted_view {
  array_view<T> source;
  permutation permute;
  std::size_t size() const { return permute.size(); }
  T& operator[]( std::size_t i ) const {
    return source[ permute(i) ];
  }
  template<class Src>
  void assign_from( Src const& src ) {
    if (src.size() != size()) exit(-1);
    for (std::size_t i = 0; i < size(); ++i)
      (*this)[i] = src[i];
  }
  void operator=( permuted_view const& src ) { assign_from(src); }
  template<class U>
  void operator=( U const& src ) { assign_from(src); }
  template<class U,
    std::enable_if_t< !std::is_integral<U>{}, int> =0
  >
  permuted_view<T> operator[]( U u )const {
    return {
      source,
      chain_permutation( make_permutation_from(std::move(u)), permute )
    };
  }
};

现在permuted_view<int>int数组上的一个排列。

注意permuted_view没有拥有任何东西。它只是指别人的存储。所有权是你必须自己解决的问题。也许可以通过智能指针,或者其他方式。

用于此目的的高效库可能具有写时复制稀疏数组。要做到这一点,需要做很多工作,或者你应该找一个像Eigen这样的库。

生活例子。

你需要添加一个迭代器和begin/endpermuted_view。我会让迭代器存储一个指向视图的指针和一个索引,并在解引用时对view使用operator[]

如果你用T*迭代器的专门化或子类将array_view<T>重构为range_view<Iterator>,那么你可以用CRTP将range_view<Iterator>重构为range_helper<Iterator, Derived>。然后为permuted_viewrange_view重用range_helper,为array_view重用range_view。但这有点太过分了。

各种库,包括Rangesv3和boost以及c++ 17 std和c++ 20 std::experimental等,要么已经编写了这些类型,要么使编写这些类型变得更容易。