【问题标题】:Can raw pointers be used instead of iterators with STL algorithms for containers with linear storage?对于具有线性存储的容器,可以使用原始指针而不是具有 STL 算法的迭代器吗?
【发布时间】:2013-05-02 23:53:09
【问题描述】:

我有一个自定义向量容器,它在内部存储项目一个线性数组。昨晚,我试图为我的类实现自定义迭代器,以便能够将它们与 STL 算法一起使用。我取得了一些成功,您可以在这里看到:

Live example with custom iterators

这样做时,我发现我只能将原始指针传递给 STL 算法,它们似乎工作正常。这是没有任何迭代器的示例:

#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

我的问题如下:

  1. 这是否总是适用于具有线性存储的容器?我知道这不适用于例如链表。
  2. 如果它们确实在这种情况下工作,我为什么还要经历实现迭代器的麻烦呢?我知道迭代器如何泛化我的代码等等,但如果这个简单的数组 就是 我所需要的,那我就看不到重点了。
  3. 如果这种方法始终有效,我正在做的事情有哪些负面问题?一方面,我可以看到我正在破坏数据封装。

【问题讨论】:

    标签: c++ pointers stl iterator


    【解决方案1】:

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

    如果满足以下条件,您可能会实现“真正的”迭代器:

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

    我想说,仅此最后一个优势就非常值得为其编写一个微不足道的包装类。如果你不利用 C++ 的类型系统让不同种类的东西有不同的类型,你还不如切换到 Javascript :-)

    【讨论】:

    • 小心&amp;array[size] 它比人们想象的要深入得多:stackoverflow.com/q/26420348/893406
    • @v.oddou 不,&amp;array[size] 始终有效(只要您不取消引用它),正如您链接中的答案所解释的那样。你需要推理什么是正确的代码,什么不是正确的代码:你不能只使用神秘主义。
    • 根据我链接页面中的整个讨论,如果该地址未映射,则在指针寄存器中加载地址就足以导致崩溃;在一些架构上。所以这不仅仅是一个不取消引用它的问题。这里没有货物崇拜。
    • @v.oddou 无效指针确实如此,但您链接的问题的重点是 C++ 标准定义指向数组末尾之后的指针是有效的。问题是关于对实现的影响;例如在您描述的机器上,运行时必须确保内存中有一个额外的空间来保存数组。问题的第一行是,“C++ 标准(以及 C 就此而言)允许创建(虽然不是取消引用)指向数组末尾之后的一个元素的指针。”
    • @v.oddou 为了避免担心&amp;array[size],您可以随时使用array + size
    【解决方案2】:
    1. 是的。请参阅Effective STL, Item 16,它演示了使用线性存储容器,您可以简单地获取项目的地址并使用该指针,就像它指向一个简单的数组一样。
    2. 我认为您已经回答了自己的问题 - 如果您知道简单数组就是您所需要的一切,您可能不应该回答。
    3. 可能最大的问题就是——破坏数据封装。考虑一下诸如显式迭代器类型之类的抽象是否会为您带来任何收益而不是成本。

    【讨论】:

    • "如果你知道简单的数组就是你所需要的一切" -> 太多的约束。 “是你现在需要的一切”在很大程度上就足够了。看在上帝的份上,尊重 YAGNI 和 KISS。否则投赞成票。
    【解决方案3】:

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

    是的,迭代器的概念被设计成指针可以作为数组的迭代器。

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

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

    一个小小的好处是您可以为迭代器的特征包含嵌套的 typedef,就像一些标准迭代器类型所做的那样;但无论如何使用指针,这些都可以从std::iterator_traits&lt;T*&gt; 获得。

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

    为了使接口与 STL 风格的容器更加一致,您应该定义 iteratorconst_iterator 类型(指针的别名为 typedef),并提供 beginendconst 重载;可能还有 cbegincend 用于 C++11 兼容性。

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

    【讨论】:

      【解决方案4】:

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

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

      【讨论】:

      • 指针实际上是一个完全有效的迭代器,它不仅提供了大部分,而且提供了所需的整个接口。诀窍是指针类型不直接提供的功能(例如定义value_type typedef)由std::iterator_traits&lt;T&gt; 类模板的特化提供。仅供参考:)
      猜你喜欢
      • 1970-01-01
      • 2014-12-05
      • 1970-01-01
      • 2013-01-05
      • 2012-07-11
      • 1970-01-01
      • 1970-01-01
      • 2017-07-04
      相关资源
      最近更新 更多