【问题标题】:Best way to access elements of custom 2-dim C++ container访问自定义 2-dim C++ 容器元素的最佳方式
【发布时间】:2020-12-16 20:59:06
【问题描述】:

我有一个非常基本的 2D C++ 容器,它依赖于一维容器(std::array 或 std::vector)和“高级”索引。该容器预计不会存储很多元素,甚至不会 500 个。访问其元素的最佳方法是什么?

起初我使用int 在迭代容器时直接索引元素。然而,在阅读了一下之后,this one 之类的帖子让我改用了std::size_t。 (切换主要是为了养成好习惯,而不是因为我的容器的要求。但other sources 让我怀疑这是否真的是一个好习惯。)

因此,我不得不重新调整几个 for() 循环以避免下溢引起的错误,例如在检查彼此相邻的元素的值时,或者在一行或一列中向后迭代元素时。这些变化反过来又降低了可读性并使索引更容易出错。 (MyIiexperience 绝对是这件事的一个因素。)

我该怎么办?

  1. 坚持std::size_t
  2. 继续使用int(同时还限制我的 2D 存储的大小以避免不太可能的溢出)
  3. 坚持使用基于范围或迭代器的循环,为列和行创建循环(欢迎提供建议和链接)
  4. 别的东西

提前谢谢你!

P.S.:甚至 C++20 功能也受到欢迎!

【问题讨论】:

  • 假设您希望“常规”行或 col 主要顺序,考虑发明一个方便的函数来计算从 2d (row,col) 坐标的单维向量 索引。可能类似于“size_t to_1d_Indx (size_t row, size_t col) { return (row * maxCol) + col; }”(我相信您可以使用 int 处理 2d 索引,使用 size_t 处理 1d 索引。)

标签: c++ stl containers c++17


【解决方案1】:

std::ptrdiff_t 是签名的std::size_t

我用那个。我听说过标准应该使用的论点,但代价是最大大小。

编写一个对跨步友好的迭代器有点痛苦。这是一个草图:

template<class T, class Stride=std::integral_constant<std::size_t, 1>>
struct stride_iterator {
  using difference_type = std::ptrdiff_t;
  using value_type = T;
  using reference= T&;
private:
  T* dataptr = nullptr;
  Stride datastride = {};
  T* data() const { return dataptr; }
  T*& data() { return dataptr; }
  Stride stride() const { return datastride; }
public:
  explicit stride_iterator( T* ptr, Stride s ):
    dataptr(ptr),
    datastride{ std::move(s) }
  {}
  explicit stride_iterator( T* ptr ):
    dataptr(ptr)
  {}
  stride_iterator():stride_iterator(nullptr) {}
  stride_iterator(stride_iterator const&)=default;
  stride_iterator& operator=(stride_iterator const&)& =default;
  stride_iterator(stride_iterator &&)=default;
  stride_iterator& operator=(stride_iterator &&)& =default;
  
  T& operator*() const { return *data(); }
  T* operator->() const { return data(); }
  T& operator[](std::ptrdiff_t n) const {
    return *(*this+n);
  }
  stride_iterator& operator+=( std::ptrdiff_t n )& {
    data() += (n*stride());
    return *this;
  }
  stride_iterator& operator-=( std::ptrdiff_t n )& {
    data() -= (n*stride());
    return *this;
  }
  friend stride_iterator operator+( std::ptrdiff_t rhs, stride_iterator lhs ) {
    return std::move(lhs)+rhs;
  }
  friend stride_iterator operator+( stride_iterator lhs, std::ptrdiff_t rhs ) {
    lhs += rhs; 
    return lhs;
  }
  friend stride_iterator operator-( stride_iterator lhs, std::ptrdiff_t rhs ) {
    lhs += rhs; 
    return lhs;
  }
  stride_iterator& operator++() {
    *this += 1;
    return *this;
  }
  stride_iterator& operator--() {
    *this -= 1;
    return *this;
  }
  stride_iterator operator++(int) {
    auto r = *this;
    ++*this;
    return r;
  }
  stride_iterator operator--(int) {
    auto r = *this;
    --*this;
    return r;
  }
  friend std::ptrdiff_t operator-( stride_iterator const& lhs, stride_iterator const& rhs ) {
    return (lhs.data()-rhs.data())/stride();
  }
  friend bool operator<( stride_iterator const& lhs, stride_iterator const& rhs ) {
    return lhs.data() < rhs.data();
  }
  friend bool operator<=( stride_iterator const& lhs, stride_iterator const& rhs ) {
    return lhs.data() <= rhs.data();
  }
  friend bool operator>( stride_iterator const& lhs, stride_iterator const& rhs ) {
    return lhs.data() > rhs.data();
  }
  friend bool operator>=( stride_iterator const& lhs, stride_iterator const& rhs ) {
    return lhs.data() >= rhs.data();
  }
  friend bool operator==( stride_iterator const& lhs, stride_iterator const& rhs ) {
    return lhs.data() == rhs.data();
  }
  friend bool operator!=( stride_iterator const& lhs, stride_iterator const& rhs ) {
    return lhs.data() != rhs.data();
  }
};

template<class It>
struct range {
    It b, e;
    It begin() const { return b; }
    It end() const { return e; }
    decltype(auto) operator[]( std::ptrdiff_t i ) const {
        return b[i];
    }
    std::size_t size() const { return end()-begin(); }
    bool empty() const { return end()==begin(); }
};


struct toy_matrix {
    std::ptrdiff_t width = 1;
    std::ptrdiff_t height = 1;
    std::vector<int> data = std::vector<int>( 1 );
    
    toy_matrix() = default;
    toy_matrix( std::size_t w, std::size_t h ):width(w),height(h), data(w*h) {}
    toy_matrix( std::size_t w, std::size_t h, std::initializer_list<int> il ):width(w),height(h), data(il) {
        data.resize(w*h);
    }
    
    range<stride_iterator<int>> row( std::ptrdiff_t i ) {
        int* ptr = data.data();
        ptr += height * i;
        return { stride_iterator<int>{ ptr }, stride_iterator<int>{ ptr + width } };
    }
    range<stride_iterator<int, std::ptrdiff_t>> column( std::ptrdiff_t i ) {
        int* ptr = data.data();
        ptr += i;
        return { stride_iterator<int, std::ptrdiff_t>{ ptr, width }, stride_iterator<int, std::ptrdiff_t>{ ptr + height * width, width } };
    }
    range<stride_iterator<int>> operator[]( std::ptrdiff_t i ) {
        return row(i);
    }
    int& operator()( std::ptrdiff_t x, std::ptrdiff_t y ) {
        return (*this)[x][y];
    }
};

测试代码:

toy_matrix m{ 5, 4, {
    1,2,3,4,5,
    10,20,30,40,50,
    100,200,300,400,500,
    1000,2000,3000,4000,5000,
}};

for (auto x : m.column(0)) {
    std::cout << x << "\n";
}
for (auto x : m.column(1)) {
    std::cout << x << "\n";
}
for (auto x : m.column(2)) {
    std::cout << x << "\n";
}

输出:

1
10
100
1000
2
20
200
2000
3
30
300
3000

Live example.

【讨论】:

  • 回复:cost of a single bit - 正如他们所说,这里有 20 亿,那里有 20 亿,很快你就会开始谈论大笔资金......还是数万亿? :)
  • @VladFeinstein 在 32 位及以下平台上,单个位仅适用于应用程序破坏大小的单字节数据。如果您在单个 std::vector 中使用超过一半的地址空间,也许您应该使用特殊用途的容器,让其他人不必处理无符号索引的痛苦。
  • 非常感谢您的示例代码!这(在您编辑之后)正是我想要实现的功能,但缺乏如何实现的知识。
  • @Yakk-AdamNevraumont "std::ptrdiff_t [...] I use that",如果 C++20 的 std::ssize 不可用,您能否详细说明如何你会用它来迭代索引吗?会不会是 for(std::ptrdiff_t i = 0; i != static_cast&lt;std::ptrdiff_t&gt;(container.size()); ++i){ ... } 之类的东西?
  • @Dudly01 我自己,我写了一个索引迭代器和一个工厂函数,它从类似范围的东西中将大小转换为有符号的索引迭代器范围并在其上执行 range-for 循环,所以我得到了@ 987654329@。我的范围也有.without_back(n=1),所以我可以做for (auto i: indexes_of(container).without_back())。你应该怎么做,我不知道。
【解决方案2】:

Stroustrup 更喜欢有符号整数类型,除非有充分的理由使用无符号整数类型。 STL 的创建者 Alexander Stepanov 首选 size_t(根据定义未签名)作为存储容器大小的类型。 所以差别是非常细微的。

个人意见:

  • 如果您是组织的一员,请使用他们的编码标准
  • 始终避免使用旧式 for 循环,而更喜欢基于范围的 for 循环(for (auto x: vec) 等)。有了这个,没有索引,没有错误的空间。
  • 如果您需要循环内的索引,请首选 int 并使用 C++20 的 std::ssize(容器的有符号大小)。

理由:

  • std::ptrdiff_t 写得太长了 :-) 而且你几乎从来没有拥有超过 20 亿个元素的容器。
  • 表达式中的int 更自然,更不容易出错
  • 在循环中(或在任何表达式中)仅使用一个无符号变量通常会强制将许多其他变量转换为无符号类型,以消除编译器警告。代码变得丑陋,非常丑陋。
  • 有符号整数更容易优化(如问题中的链接中所述)

也就是说,多年来我一直使用size_t(组织)或粗心的(int)v.size()(私人代码)。 std::ssize 早就应该添加到库中了。

【讨论】:

  • 有趣。不知道ssize() 虽然我通常避免强制转换,但我对 int(v.size()) 没有任何问题,因为它清晰明确。我在其他几个需要的地方使用 c++11 样式转换,通常是外部库接口,因为它们不太容易出错并且在重构时更容易搜索。
猜你喜欢
  • 1970-01-01
  • 2020-02-17
  • 1970-01-01
  • 2017-02-09
  • 1970-01-01
  • 2020-12-03
  • 2014-09-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多