20

I have a custom vector container that internally stores item a linear array. Last night, I was trying to implement custom iterators for my class to be able to use them with STL algorithms. I have had some success that you can see in here:

Live example with custom iterators

While doing so, I discovered I can merely pass raw pointers to STL algorithm and they just seem to work fine. Here's the example without any iterators:

#include <cstddef>
#include <iostream>
#include <iterator>
#include <algorithm>

template<typename T>
class my_array{
    T* data_;
    std::size_t size_;

public:

    my_array()
        : data_(NULL), size_(0)
    {}
    my_array(std::size_t size)
        : data_(new T[size]), size_(size)
    {}
    my_array(const my_array<T>& other){
        size_ = other.size_;
        data_ = new T[size_];
        for (std::size_t i = 0; i<size_; i++)
            data_[i] = other.data_[i];
    }
    my_array(const T* first, const T* last){
        size_ = last - first;
        data_ = new T[size_];

        for (std::size_t i = 0; i<size_; i++)
            data_[i] = first[i];
    }

    ~my_array(){
        delete [] data_;
    }
    const my_array<T>& operator=(const my_array<T>& other){
        size_ = other.size_;
        data_ = new T[size_];
        for (std::size_t i = 0; i<size_; i++)
            data_[i] = other.data_[i];
        return other;
    }
    const T& operator[](std::size_t idx) const {return data_[idx];}
    T& operator[](std::size_t& idx) {return data_[idx];}
    std::size_t size(){return size_;}

    T* begin(){return data_;}
    T* end(){return data_+size_;}
};

template<typename T>
void print(T t) {
    std::cout << t << std::endl;
}

int main(){


    typedef float scalar_t;
    scalar_t list [] = {1, 3, 5, 2, 4, 3, 5, 10, 10};
    my_array<scalar_t> a(list, list+sizeof(list)/sizeof(scalar_t));

    // works!
    for (scalar_t* it = a.begin(), *end = a.end();
         it != end; ++it)
        std::cout << ' ' << *it;
    std::cout << std::endl;

    // works!
    std::for_each(a.begin(), a.end(), print<scalar_t>);
    std::cout << std::endl;

    // works!
    my_array<int> b(a.size());
    std::copy(a.begin(), a.end(), b.begin());

    // works!
    scalar_t* end = std::remove(a.begin(), a.end(), 5);
    std::for_each(a.begin(), end, print<scalar_t>);
    std::cout << std::endl;

    // works!
    std::random_shuffle(a.begin(), end);
    std::for_each(a.begin(), end, print<scalar_t>);
    std::cout << std::endl;

    // works!
    std::cout << "Counts of 3 in array = " << std::count(a.begin(), end, 3) << std::endl << std::endl;

    // works!
    std::sort(a.begin(), end);
    std::for_each(a.begin(), end, print<scalar_t>);
    std::cout << std::endl;

    // works!
    if (!std::binary_search(a.begin(), a.end(), 5))
        std::cout << "Removed!" << std::endl;

    return 0;
}

Live example without iterators

My questions here are the following:

  1. Does this always work for containers that have linear storage? I know that this would not work for linked-lists for example.
  2. If they do work in this situation, why should I ever go through the hassle of implementing iterators anyway? I know how iterators generalize my code and whatnot, but if this simple array is all I ever need then I don't see the point.
  3. What are the negative issues of what I'm doing if this approach would always work? For one thing, I can see I'm breaking data encapsulation.
4

4 回答 4

22

基于运算符重载的迭代器的特性之一是指针已经是随机访问迭代器。在 STL 的早期,这是一个重大的设计胜利,因为它使算法更容易与现有代码一起使用(以及使程序员更熟悉界面)。包装一个数组,添加typedef T* iterator; typedef const T* const_iterator&array[0]从您的begin()&array[size]从您的返回end(),然后将您的容器与任何基于迭代器的算法一起使用是完全合理的。正如您已经意识到的那样,这适用于任何元素在内存中连续的容器(例如数组)。

如果出现以下情况,您可能会实现“真正的”迭代器:

  • 你有一个不同形状的容器(例如树或列表);
  • 您希望能够在不使迭代器无效的情况下调整数组大小;
  • 您想在迭代器使用中添加调试检查,例如检查迭代器是否在失效后或容器被删除后使用,或者边界检查;
  • 您想引入类型安全,并确保人们不会意外地将任意值分配T*my_array::iterator.

我想说,仅此最后一个优势就非常值得为其编写一个微不足道的包装类。如果您不通过使不同种类的事物具有不同类型来利用 C++ 的类型系统,那么您不妨切换到 Javascript :-)

于 2013-05-08T17:03:32.400 回答
9
  1. 是的。请参阅Effective STL,第 16 项,它使用线性存储容器演示了您可以简单地获取项目的地址并使用该指针,就好像它指向一个简单的数组一样。
  2. 我想你回答了你自己的问题——你可能不应该,如果你知道简单的数组就是你所需要的。
  3. 可能最大的问题就是——破坏数据封装。考虑一下诸如显式迭代器类型之类的抽象是否会为您带来任何收益而不是成本。
于 2013-05-08T16:59:33.700 回答
4

碰巧指针提供了随机访问迭代器所需的接口(取消引用、递增、加法、差异等),并且可以像迭代器一样对待。

  1. 它应该始终适用于具有连续存储的容器。
  2. 您可能希望创建自己的迭代器,原因与您在类中使用方法而不是所有公共数据的原因相同:要封装接口发生的事情,您可以根据需要进行修改。只要您将您T*的类型定义为迭代器类型,这可能不是一个重要问题。此外,某些算法可能会受益于使用迭代器类型标记的迭代器,而对于简单的指针类型则无法做到这一点。
于 2013-05-08T16:57:43.943 回答
4

这是否总是适用于具有线性存储的容器?

是的,迭代器概念的设计是为了让指针可以作为数组上的迭代器。

如果他们确实在这种情况下工作,我为什么还要经历实现迭代器的麻烦呢?

在这种情况下,没有充分的理由定义您自己的迭代器类型,除非您想做一些无法使用简单指针完成的边界检查。

一个小小的好处是你可以为迭代器的特征包含嵌套的 typedef,就像一些标准的迭代器类型一样;但是使用指针,std::iterator_traits<T*>无论如何都可以使用这些指针。

如果这种方法总是有效,我正在做的事情有哪些负面问题?一方面,我可以看到我正在破坏数据封装。

为了使接口与 STL 风格的容器更加一致,您应该定义iteratorconst_iterator类型(typedef指针的别名),并提供和的const重载;也许和C++11 的兼容性。beginendcbegincend

您可能还需要遵守其他各种要求;有关详细信息,请参阅 C++ 标准的第 23.2 节。但总的来说,使迭代器符合它们的要求更为重要,因为 STL 风格的算法使用迭代器而不是容器,并且通过使用指针,您已经符合这些要求。

于 2013-05-08T17:11:20.170 回答