Подтвердить что ты не робот

Могут ли использоваться необработанные указатели вместо итераторов с алгоритмами STL для контейнеров с линейным хранилищем?

У меня есть пользовательский векторный контейнер, который внутренне сохраняет элемент в линейном массиве. Прошлой ночью я пытался реализовать пользовательские итераторы для моего класса, чтобы иметь возможность использовать их с алгоритмами STL. У меня был некоторый успех, который вы можете увидеть здесь:

Живой пример с пользовательскими итераторами

При этом я обнаружил, что могу просто передать исходные указатели на алгоритм 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;
}

Живой пример без итераторов

Мои вопросы здесь:

  • Это всегда работает для контейнеров с линейным хранилищем? Я знаю, что это не будет работать для связанных списков, например.
  • Если они работают в этой ситуации, почему я должен когда-либо преодолевать трудности с внедрением итераторов? Я знаю, как итераторы обобщают мой код и еще много чего, но если этот простой массив мне нужен, я не вижу смысла.
  • Каковы негативные проблемы того, что я делаю, если такой подход всегда будет работать? Во-первых, я вижу, что я нарушаю инкапсуляцию данных.
4b9b3361

Ответ 1

Одна из особенностей итераторов, основанная на перегрузке оператора, заключается в том, что указатели уже являются итераторами с произвольным доступом. Это была большая победа в дизайне в первые дни STL, так как она упростила использование алгоритмов с существующим кодом (а также упростила интерфейс для программистов). Разумно обернуть массив, добавить typedef T* iterator; typedef const T* const_iterator, вернуть &array[0] из ваших begin() и &array[size] из вашего end(), а затем использовать ваш контейнер с любым алгоритмом на основе итератора. Как вы уже поняли, это будет работать для любого контейнера, где элементы смежны в памяти (например, массив).

Вы можете реализовать "настоящие" итераторы, если:

  • У вас есть контейнер другой формы (например, дерево или список);
  • Вы хотите иметь возможность изменять размер массива без аннулирования итераторов;
  • Вы хотите добавить проверки отладки на использование итератора, например, чтобы проверить, используется ли итератор после того, как он был признан недействительным или после удаления контейнера, или для проверки границ;
  • Вы хотите ввести безопасность типа и убедитесь, что люди не могут случайно назначить произвольное T* на my_array::iterator.

Я бы сказал, что это последнее преимущество само по себе стоит писать тривиальный класс-оболочку. Если вы не пользуетесь системой типа С++, создавая разные типы вещей, вы можете переключиться на Javascript: -)

Ответ 2

  • Да. См. Эффективный STL, пункт 16, который демонстрируется с помощью линейных контейнеров для хранения, вы можете просто взять адрес элемента и работать с этим указателем, как если бы он указывал к простому массиву.
  • Я думаю, вы ответили на свой собственный вопрос - ndash; вы, вероятно, не должны, если вы знаете, что простой массив - это все, что вам когда-либо понадобится.
  • Наверное, самая большая проблема - это просто: ndash; прерывание инкапсуляции данных. Рассмотрите, может ли абстракция, такая как явный тип итератора, купить вам что-либо по сравнению с затратами.

Ответ 3

Бывает, что указатели предоставляют интерфейс, необходимый для итераторов произвольного доступа (разыменование, приращение, добавление, разность и т.д.) и могут обрабатываться точно так же, как итераторы.

  • Он всегда должен работать для контейнеров со смежным хранилищем.
  • Возможно, вы захотите создать свои собственные итераторы по той же причине, что и методы, а не все общедоступные данные в ваших классах. Чтобы инкапсулировать то, что происходит с интерфейсом, вы можете изменить, если вам нужно. Пока вы печатаете свой тип T* с типом итератора, это, вероятно, не является существенной проблемой. Кроме того, некоторые алгоритмы могут извлечь выгоду из итератора, который помечен типом итератора, который вы не можете сделать для простых типов указателей.

Ответ 4

Всегда ли это работает для контейнеров с линейным хранилищем?

Да, понятия итератора были разработаны так, чтобы указатели могли выступать в качестве итераторов по массивам.

Если они работают в этой ситуации, почему я должен когда-либо преодолевать трудности с выполнением итераторов?

В этой ситуации нет веских оснований для определения вашего собственного типа итератора, если вы не хотите делать что-то вроде проверки границ, которая не может быть выполнена с помощью простого указателя.

Одно небольшое преимущество заключается в том, что вы можете включить вложенные typedef для характеристик итератора, как это делают некоторые стандартные типы итераторов; но с помощью указателей они доступны от std::iterator_traits<T*> в любом случае.

Каковы негативные проблемы того, что я делаю, если этот подход всегда будет работать? Во-первых, я вижу, что я нарушаю инкапсуляцию данных.

Чтобы сделать интерфейс более согласованным с контейнерами в стиле STL, вы должны определить типы iterator и const_iterator (typedef псевдонимы для указателей) и обеспечить const перегрузки begin и end; и, возможно, cbegin и cend для совместимости С++ 11.

Существуют различные другие требования, которые вы, возможно, захотите выполнить; см. раздел 23.2 стандарта С++ для деталей gory. Но в целом важнее, чтобы итераторы соответствовали их требованиям, поскольку алгоритмы в стиле STL работают с итераторами, а не с контейнерами, и с помощью указателей вы уже соответствуете этим требованиям.