vector :: at vs. vector :: operator []

95

Я знаю, что at()это медленнее, чем []из-за проверки границ, которая также обсуждается в подобных вопросах, таких как C ++ Vector at / [] operator speed или :: std :: vector :: at () vs operator [] << удивительные результаты !! В 5-10 раз медленнее / быстрее! . Я просто не понимаю, для чего нужен этот at()метод.

Если у меня есть простой вектор, подобный этому: std::vector<int> v(10);и я решаю получить доступ к его элементам, используя at()вместо этого, []в ситуации, когда у меня есть индекс, iи я не уверен, что он находится в границах векторов, он заставляет меня обернуть его с помощью try-catch блок :

try
{
    v.at(i) = 2;
}
catch (std::out_of_range& oor)
{
    ...
}

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

if (i < v.size())
    v[i] = 2;

Итак, мой вопрос:
каковы преимущества использования vector :: at над vector :: operator [] ?
Когда мне следует использовать vector :: at, а не vector :: size + vector :: operator [] ?

LihO
источник
11
+1 очень хороший вопрос !! но я не думаю, что в () обычно используется.
Рохит Випин Мэтьюз,
10
Обратите внимание, что в вашем примере кода if (i < v.size()) v[i] = 2;существует возможный путь кода, который вообще не назначается 2ни одному элементу v. Если это правильное поведение, отлично. Но часто нет ничего разумного, что эта функция может делать, когда i >= v.size(). Таким образом, нет особой причины, по которой он не должен использовать исключение для указания неожиданной ситуации. Многие функции просто используют operator[]без проверки размера, документ, который iдолжен быть в диапазоне, и возлагают ответственность за полученный UB на вызывающего.
Стив Джессоп,

Ответы:

74

Я бы сказал, что vector::at()генерируемые исключения на самом деле не предназначены для перехвата непосредственно окружающим кодом. В основном они полезны для поиска ошибок в вашем коде. Если вам нужно выполнить проверку границ во время выполнения, потому что, например, индекс поступает из пользовательского ввода, вам действительно лучше всего использовать ifоператор. Таким образом, проектируйте свой код так, чтобы vector::at()он никогда не генерировал исключение, так что если оно произойдет и ваша программа прервется, это будет признаком ошибки. (точно так же assert())

pmdj
источник
1
+1 Мне нравится объяснение того, как разделить обработку неправильного ввода пользователя (проверка ввода; можно ожидать недопустимый ввод, поэтому это не считается чем-то исключительным) ... и ошибки в коде (разыменование итератора, выходящего за пределы диапазона, является исключительным вещь)
Боян Комазец
Итак, вы говорите, что я должен использовать size()+, []когда индекс зависит от ввода пользователя, использовать assertв ситуациях, когда индекс никогда не должен выходить за пределы для легкого исправления ошибок в будущем, и .at()во всех других ситуациях (на всякий случай может произойти что-то не так ... .)
LihO
8
@LihO: если ваша реализация предлагает отладочную реализацию, vectorто, вероятно, лучше использовать это как вариант «на всякий случай», а не at()везде. Таким образом, вы можете надеяться на немного более высокую производительность в режиме выпуска, на всякий случай, если она вам когда-нибудь понадобится.
Стив Джессоп,
3
Да, большинство реализаций STL в наши дни поддерживают режим отладки, который даже ограничивает проверку operator[], например gcc.gnu.org/onlinedocs/libstdc++/manual/… так что, если ваша платформа поддерживает это, вам, вероятно, лучше всего использовать его!
pmdj
1
@pmdj фантастическая точка, о которой я не знал ... но бесхозная ссылка. : P текущий: gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode.html
underscore_d
16

это заставляет меня обернуть его блоком try-catch

Нет, это не так (блок try / catch может быть восходящим). Это полезно, когда вы хотите, чтобы генерировалось исключение, а не ваша программа входила в область неопределенного поведения.

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

Поскольку маловероятно, что доступ за пределы вектора является частью нормального потока программы (в этом случае вы правы: проверьте заранее, sizeвместо того, чтобы позволить исключению всплыть), я согласен с вашей диагностикой: atпо сути бесполезно.

Александр К.
источник
Если я не поймаю out_of_rangeисключение, abort()вызывается.
LihO
@LihO: Не обязательно .. они try..catchмогут присутствовать в методе, вызывающем этот метод.
Naveen
12
По крайней мере, atэто полезно в той степени, в которой вы в противном случае сочли бы, что пишете что-то подобное if (i < v.size()) { v[i] = 2; } else { throw what_are_you_doing_you_muppet(); }. Люди часто думают о функциях генерирования исключений как о «проклятиях, я должен обрабатывать исключение», но если вы тщательно документируете, что может вызывать каждая из ваших функций, их также можно использовать как «отлично, я не необходимо проверить условие и выбросить исключение ».
Стив Джессоп,
@SteveJessop: Мне не нравится выбрасывать исключения для программных ошибок, так как они могут быть обнаружены другими программистами. Утверждения здесь гораздо полезнее.
Alexandre C.
6
@AlexandreC. Что out_of_rangeж , официальный ответ на это заключается в том logic_error, что другие программисты "должны" знать лучше, чем ловить logic_errorапстрим и игнорировать их. assertтакже можно проигнорировать, если ваши коллеги не хотят знать о своих ошибках, это просто сложнее, потому что им приходится компилировать ваш код NDEBUG;-) Каждый механизм имеет свои достоинства и недостатки.
Стив Джессоп,
11

Каковы преимущества использования vector :: at по сравнению с vector :: operator []? Когда мне следует использовать vector :: at, а не vector :: size + vector :: operator []?

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

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

В качестве реального примера у меня есть код, который разбивает C ++ на лексические элементы, а затем другой код, который перемещает индекс по вектору токенов. В зависимости от того, с чем я столкнулся, я могу увеличить и проверить следующий элемент, например:

if (token.at(i) == Token::Keyword_Enum)
{
    ASSERT_EQ(tokens.at(++i), Token::Idn);
    if (tokens.at(++i) == Left_Brace)
        ...
    or whatever

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

Тони Делрой
источник
10

at может быть понятнее, если у вас есть указатель на вектор:

return pVector->at(n);
return (*pVector)[n];
return pVector->operator[](n);

Помимо производительности, первое из них - это более простой и понятный код.

Brangdon
источник
... особенно, когда вам нужен указатель на n -й элемент вектора.
дельфин
4

Во- первых, независимо от того at()или operator[]медленнее , не уточняется. Когда нет ошибки границ, я бы ожидал, что они будут примерно с такой же скоростью, по крайней мере, при отладке сборок. Разница в том, что at()точно определяет, что произойдет, при наличии ошибки границ (исключение), где, как и в случае operator[], это неопределенное поведение - сбой во всех используемых мной системах (g ++ и VC ++), по крайней мере, когда используются обычные отладочные флаги. (Еще одно отличие состоит в том, что как только я уверен в своем коде, я могу получить существенное увеличение скорости operator[] , отключив отладку. Если этого требует производительность - я бы не стал этого делать, если в этом нет необходимости.)

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

Джеймс Канце
источник
4
Почему вы ожидаете, что они будут иметь примерно одинаковую скорость, если operator[]не принудительно проверять границы, а at()есть? Вы под этим подразумеваете проблемы с кешированием, предположениями и буфером ветвления?
Себастьян Мах
3
К сожалению, пропустил ваш атрибут "в режиме отладки". Однако я бы не стал измерять качество кода в режиме отладки. В режиме выпуска проверка требуется только для at().
Себастьян Мах
1
@phresnel Большая часть кода, который я доставил, находится в режиме «отладки». Вы отключаете проверку только тогда, когда этого действительно требуют проблемы с производительностью. (Microsoft до 2010 года была здесь небольшой проблемой, поскольку std::stringне всегда работала, если параметры проверки не соответствовали параметрам среды выполнения:, -MDи вам лучше отключить проверку -MDd, и вам лучше это дальше.)
Джеймс Канце
3
Я больше принадлежу к лагерю, который говорит: «Код санкционирован (гарантирован) стандартом»; конечно, вы можете работать в режиме отладки, но при кросс-платформенной разработке (включая, но не исключительно, случай одной и той же ОС, но разные версии компилятора), использование стандарта является лучшим выбором для выпусков, а режим отладки считается инструментом для программиста, чтобы получить эту вещь в основном правильную и надежную :)
Себастьян Мах,
1
С другой стороны, если критические части вашего приложения изолированы и защищены, например, безопасностью исключений (RAII ftw), тогда следует operator[]ли ограничивать каждый доступ ? Например, std::vector<color> surface(witdh*height); ...; for (int y=0; y!=height; ++y).... Я думаю, что принудительная проверка границ для доставленных двоичных файлов подпадает под преждевременную пессимизацию. Имхо, это должно быть только повязкой для плохо спроектированного кода.
Себастьян Мах,
1

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

В этом конкретном случае ввод пользователя действительно является хорошим примером. Представьте, что вы хотите семантически проанализировать структуру данных XML, которая использует индексы для ссылки на какой-то ресурс, который вы внутренне храните в файле std::vector. Теперь XML-дерево представляет собой дерево, поэтому вы, вероятно, захотите использовать рекурсию для его анализа. Глубоко внутри рекурсии могло быть нарушение прав доступа со стороны автора XML-файла. В этом случае вы обычно хотите выйти из всех уровней рекурсии и просто отвергнуть весь файл (или любую «более грубую» структуру). Вот здесь и пригодится. Вы можете просто написать код анализа, как если бы файл был действительным. Код библиотеки позаботится об обнаружении ошибок, и вы можете просто обнаружить ошибку на грубом уровне.

Кроме того, другие контейнеры, например std::map, также имеют std::map::atсемантику, немного отличающуюся от std::map::operator[]: at, могут использоваться на константной карте, а operator[]не могут. Теперь, если вы хотите написать код, не зависящий от контейнера, например, что-то, что может иметь дело с любым const std::vector<T>&или const std::map<std::size_t, T>&, ContainerType::atбыло бы вашим оружием.

Однако все эти случаи обычно возникают при обработке некорректного ввода данных. Если вы уверены в своем допустимом диапазоне, как обычно, вы можете использовать operator[], но еще лучше, итераторы с begin()и end().

ltjax
источник
1

Согласно этой статье, не считая производительности, нет никакой разницы в использовании atили operator[], только если доступ гарантирован в пределах размера вектора. В противном случае, если доступ основан только на емкости вектора, его безопаснее использовать at.

эй
источник
2
там есть драконы. что произойдет, если мы нажмем на эту ссылку? (подсказка: я это уже знаю, но в StackOverflow мы предпочитаем комментарии, которые не подвержены гниению ссылок, т. е. предоставляют краткое изложение того, что вы хотите сказать)
Себастьян Мах
Спасибо за чаевые. Сейчас это исправлено.
ahj
0

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

На самом деле есть только одно отличие: atпроверяет ли границы, а operator[]нет. Это применимо как к сборкам отладки, так и к сборкам выпуска, и это очень хорошо определено стандартами. Это так просто.

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

Шитал Шах
источник
1
Исключения в C ++ предназначены для механизма обработки ошибок, а не для отладки. Herb Sutter объясняет , почему бросать std::out_of_rangeили любую другую форму std::logic_error, на самом деле, логическую ошибку самого по себе здесь .
Big Temp
@BigTemp - Я не уверен, как ваш комментарий связан с этим вопросом и ответом. Да, исключения - это очень обсуждаемая тема, но вопрос здесь в различии между atи, []и мой ответ просто указывает на разницу. Я лично использую «безопасный» метод, когда производительность не является проблемой. Как говорит Кнут, не делайте преждевременной оптимизации. Кроме того, хорошо обнаруживать ошибки раньше, чем в производственной среде, независимо от философских различий.
Шитал Шах