Предпочитаете итераторы указателям?

Этот вопрос представляет собой ответ на вопрос с комментарием здесь, но был удален как часть сообщения.

Для тех из вас, кто не может видеть удаленные сообщения, комментарий был о моем использовании const char*s вместо string::const_iterators в этом ответе : «Итераторы, возможно, были лучшим путем с самого начала, поскольку кажется, что именно так обрабатываются ваши указатели».

Итак, мой вопрос заключается в следующем: удерживают ли итераторы string::const_iterator какое-либо внутреннее значение над const char*, так что переключение моего ответа на string::const_iterators имеет смысл?


person Jonathan Mee    schedule 17.03.2015    source источник
comment
Более простая отладка (на поддерживаемых компиляторах) — причина №1 для меня.   -  person Neil Kirk    schedule 17.03.2015


Ответы (2)


Вступление

Есть много преимуществ использования итераторов вместо указателей, среди них:

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


Отладка

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

Реализация стандартной библиотеки libstdc++, предоставленная gcc< /em> выдает диагностику при обнаружении ошибки (если Режим отладки включен).


Пример

#define _GLIBCXX_DEBUG 1 /* enable debug mode */

#include <vector>
#include <iostream>

int
main (int argc, char *argv[])
{
  std::vector<int> v1 {1,2,3};

  for (auto it = v1.begin (); ; ++it)
    std::cout << *it;
}
/usr/include/c++/4.9.2/debug/safe_iterator.h:261:error: attempt to 
    dereference a past-the-end iterator.

Objects involved in the operation:
iterator "this" @ 0x0x7fff828696e0 {
type = N11__gnu_debug14_Safe_iteratorIN9__gnu_cxx17__normal_iteratorIPiNSt9__cxx19986vectorIiSaIiEEEEENSt7__debug6vectorIiS6_EEEE (mutable iterator);
  state = past-the-end;
  references sequence with type `NSt7__debug6vectorIiSaIiEEE' @ 0x0x7fff82869710
}
123

Этого бы не произошло, если бы мы работали с указателями, независимо от того, находимся мы в режиме отладки или нет.

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



(Потенциально) лучшая безопасность типов

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


Рассмотрим приведенный ниже пример:

#include <vector>

<суп>

struct A     { };
struct B : A { };

<суп>

                                                      // .-- oops
                                                      // v
void  it_func (std::vector<B>::iterator beg, std::vector<A>::iterator end);

void ptr_func (B * beg, A * end);
                     // ^-- oops

<суп>

int
main (int argc, char *argv[])
{
  std::vector<B> v1;

   it_func (v1.begin (), v1.end  ());               // (A)
  ptr_func (v1.data  (), v1.data () + v1.size ());  // (B)
}

Доработка

  • (A) может, в зависимости от реализации, быть ошибкой времени компиляции, поскольку std::vector<A>::iterator и std::vector<B>::iterator потенциально не одного типа.
  • Однако (B) будет компилироваться всегда, поскольку существует неявное преобразование из B* в A*.
person Filip Roséen - refp    schedule 17.03.2015
comment
В вашем примере безопасности типов вы сказали, что в случае (B) происходит неявное преобразование из B* в A* и, следовательно, нет ошибки компиляции. Может ли это привести к ошибкам во время выполнения? Потому что иначе я бы сказал, что недостатков нет, а на самом деле лучше. Не могли бы вы немного расширить это? - person Fabio says Reinstate Monica; 17.03.2015
comment
@FabioTurati Это очень натянутый пример, я не тратил много времени на то, чтобы превратить его в сценарий реального мира; но допустим, вы работаете с невиртуальной функцией-членом void print() и ожидаете, что it_func вызовет B::print, но вместо этого вызовет A::print из-за неявного преобразования.. вы хотели написать std::vector<B>::iterator, но ты этого не сделал. Я обновлю фрагмент немного более реалистичным сценарием. - person Filip Roséen - refp; 17.03.2015

Итераторы предназначены для обеспечения абстракции над указателями.

Например, увеличение итератора всегда манипулирует итератором так, что если в коллекции есть следующий элемент, он ссылается на этот следующий элемент. Если он уже ссылался на последний элемент в коллекции, после приращения это будет уникальное значение, которое не может быть разыменовано, но будет сравниваться с равным другому итератору, указывающему один за концом той же коллекции (обычно получается с помощью collection.end() ).

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

Например, вы можете использовать std::sort для сортировки элементов в строке или векторе. Поскольку указатели предоставляют необходимые возможности, вы также можете использовать их для сортировки элементов в собственном массиве (в стиле C).

В то же время да, определение (или использование) итератора, отдельного от указателя, может предоставить дополнительные возможности, которые не являются строго обязательными. Просто, например, некоторые итераторы обеспечивают по крайней мере некоторую степень проверки, чтобы гарантировать, что (например) при сравнении двух итераторов они оба являются итераторами в одной и той же коллекции, и что вы не пытаетесь получить доступ за пределы. Необработанный указатель не может (или, по крайней мере, обычно не будет) предоставлять такую ​​возможность.

Многое из этого восходит к менталитету «не плати за то, чем не пользуешься». Если вам действительно нужны и нужны только возможности нативных указателей, их можно использовать в качестве итераторов, и вы обычно получаете код, практически идентичный коду, полученному при прямом манипулировании указателями. В то же время, в тех случаях, когда вам нужны дополнительные возможности, такие как обход многопоточного дерева RB или дерева B+ вместо простого массива, итераторы позволяют вам сделать это, сохраняя при этом единый простой интерфейс. Аналогичным образом, в случаях, когда вы не возражаете платить дополнительно (с точки зрения хранения и/или времени выполнения) за дополнительную безопасность, вы также можете получить ее (и она отделена от таких вещей, как отдельный алгоритм, так что вы можете получить ее там, где нужно). вы хотите, чтобы он не был вынужден использовать его в других местах, которые могут, например, иметь слишком критичные требования к времени для его поддержки.

На мой взгляд, многие люди упускают суть, когда речь заходит об итераторах. Многие люди с радостью переписывают что-то вроде:

for (size_t i=0; i<s.size(); i++)

... во что-то вроде:

for (std::string::iterator i = s.begin; i != s.end(); i++)

...и вести себя так, как будто это большое достижение. Я так не думаю. В таком случае, вероятно, мало (если вообще есть) выгоды от замены целочисленного типа итератором. Аналогичным образом, если вы возьмете опубликованный вами код и измените char const * на std::string::iterator, вряд ли это даст много (если вообще что-нибудь). На самом деле такие преобразования часто делают код более многословным и менее понятным, ничего не получая взамен.

Если вы собирались изменить код, вы должны (на мой взгляд) сделать это, пытаясь сделать его более универсальным, сделав его по-настоящему универсальным (чего std::string::iterator на самом деле делать не собирается).

Например, рассмотрим ваш split (скопировано из поста, на который вы ссылаетесь):

vector<string> split(const char* start, const char* finish){
    const char delimiters[] = ",(";
    const char* it;
    vector<string> result;

    do{
        for (it = find_first_of(start, finish, begin(delimiters), end(delimiters));
            it != finish && *it == '(';
            it = find_first_of(extractParenthesis(it, finish) + 1, finish, begin(delimiters), end(delimiters)));
        auto&& temp = interpolate(start, it);
        result.insert(result.end(), temp.begin(), temp.end());
        start = ++it;
    } while (it <= finish);
    return result;
}

В настоящее время это ограничено использованием на узких струнах. Если кто-то хочет работать с широкими строками, строками UTF-32 и т. д., заставить его сделать это относительно сложно. Точно так же, если кто-то захочет сопоставить [ или '{' вместо (, код для этого тоже придется переписать.

Если бы была возможность поддерживать различные типы строк, мы могли бы сделать код более универсальным, примерно так:

template <class InIt, class OutIt, class charT>
void split(InIt start, InIt finish, charT paren, charT comma, OutIt result) {
    typedef std::iterator_traits<OutIt>::value_type o_t;
    charT delimiters[] = { comma, paren };
    InIt it;

    do{
        for (it = find_first_of(start, finish, begin(delimiters), end(delimiters));
            it != finish && *it == paren;
            it = find_first_of(extractParenthesis(it, finish) + 1, finish, begin(delimiters), end(delimiters)));
        auto&& temp = interpolate(start, it);
        *result++ = o_t{temp.begin(), temp.end()};
        start = ++it;
    } while (it != finish);
}

Это не тестировалось (или даже не компилировалось), так что на самом деле это просто набросок общего направления, в котором вы можете развивать код, а не фактический, готовый код. Тем не менее, я думаю, что общая идея должна быть по крайней мере очевидна — мы не просто меняем ее на «использовать итераторы». Мы меняем его на универсальный, а итераторы (передаются как параметры шаблона, типы которых здесь не указаны напрямую) являются лишь частью этого. Чтобы продвинуться дальше, мы также отказались от жесткого кодирования символов скобок и запятых. Хотя это и не является строго необходимым, я также изменяю параметры, чтобы они больше соответствовали соглашению, используемому стандартными алгоритмами, поэтому (например) вывод также записывается через итератор, а не возвращается как коллекция.

Хотя это может быть не сразу очевидно, последнее действительно добавляет гибкости. Просто, например, если кто-то просто хотел распечатать строки после их разделения, он мог бы передать std::ostream_iterator, чтобы каждый результат записывался непосредственно в std::cout по мере его создания, вместо того, чтобы получать вектор строк, а затем печатать их отдельно. из.

person Jerry Coffin    schedule 17.03.2015
comment
Очень интересный ответ. Должен признать, что я не совсем понял ваш пример в конце, но я понял вашу общую мысль о том, что итераторы не обязательно намного лучше, чем обычные указатели, по крайней мере, не всегда, и они также имеют дополнительную стоимость. И это правда, что код становится более подробным и менее читабельным. Вы дали мне новую точку зрения на это. +1, вполне заслуженно! - person Fabio says Reinstate Monica; 18.03.2015