Являются ли итераторы без разыменования "истекшим" итератором поведения массива undefined?

Учитывая int foo[] = {0, 1, 2, 3};, я хочу знать, являются ли итераторы, указывающие минус "один конец", недопустимыми. Например: auto bar = cend(foo) + 1;

Существует множество жалоб и предупреждений о том, что это "поведение w90" в Stack Вопросы переполнения: С++, что является результатом итератора + целого числа, когда прошлое -iterator? К сожалению, единственным источником является размахивание руки.

У меня больше проблем с покупкой, например:

int* bar;

Не инициализирован, но, конечно, не вызывает поведение undefined, и при достаточном количестве попыток я уверен, что могу найти экземпляр, где значение в этом неинициализированном bar имеет то же значение, что и cend(foo) + 1.

Одно из больших недоразумений заключается в том, что я не спрашиваю о разыменовании cend(foo) + 1. Я знаю, что это будет поведение undefined, и стандарт запрещает его.. Но отвечает вот так: qaru.site/info/4272/..., в которых приводятся только разыменование таких итератор является незаконным, не отвечайте на вопрос.

Я также знаю, что С++ гарантирует, что cend(foo) будет действительным, но может быть numeric_limits<int*>::max(), и в этом случае cend(foo) + 1 будет переполняться. Меня не интересует этот случай, если он не вызван в стандарте как причина, по которой мы не можем иметь итератор мимо "одного конца". Я знаю, что int* действительно просто хранит целочисленное значение и как таковое подвержено переполнению.

Я хотел бы получить цитату из достоверного источника, что перемещение итератора за пределы элемента "один конец" означает undefined.

Ответы

Ответ 1

Да, ваша программа имеет поведение undefined, если вы формируете такой указатель.

Это потому, что единственный способ, которым вы можете это сделать, - это увеличить допустимый указатель за границами объекта, который он указывает внутри, и это операция undefined.

[C++14: 5.7/5]: Когда выражение с интегральным типом добавляется или вычитается из указателя, результат имеет тип операнда указателя. Если операнд указателя указывает на элемент объекта массива, а массив достаточно большой, результат указывает на смещение элемента от исходного элемента, так что разность индексов результирующего и оригинального элементы массива равны интегральному выражению. Другими словами, если выражение P указывает на i-й элемент объекта массива, то выражения (P)+N равно, N+(P)) и (P)-N (где N имеет значение n) указывают на, соответственно, я + n-й и i-й-элементы элемента массива, если они существуют. Более того, если выражение P указывает на последний элемент объекта массива, выражение (P)+1 указывает один за последним элементом объекта массива, и если выражение Q указывает мимо последнего элемента объекта массива, выражение (Q)-1 указывает на последний элемент объекта массива. Если оба операнда указателя и результат указывают на элементы одного и того же объекта массива или один за последним элементом объекта массива, оценка не должна приводить к переполнению; в противном случае поведение undefined.

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

Как побочный элемент, я бы не назвал эти "истекшие" итераторы/указатели "конец-конец", термин в С++, который специально означает "один прошедший конец" итератор/указатель, который действителен (например, cend(foo) сам). Ты уже прошел мимо конца.;)

Ответ 2

TL; DR - поведение undefined для вычисления итератора за итератором, прошедшим через конец, потому что в процессе нарушается предварительное условие.


Легкость предоставила цитату, которая авторитетно охватывает указатели.

Для итераторов приращение "конца" (один-последний-последний-элемент) не запрещается вообще, но для большинства различных итераторов он запрещен:

Требования к итератору

Требования к InputIterator

Требования к входному итератору и предложение только incrementable if dereferenceable, в частности, включаются путем ссылки в итераторы прямого, двунаправленного и произвольного доступа.

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

Затем, прыгая вперед в последовательности, определяется в терминах индивидуального прироста, поэтому мы заключаем, что вычисление итератора last-the-one-past-the-end либо бессмысленно, либо незаконно для всех типов итераторов.

RandomAccessIterator requirements

Ответ 3

Мне неинтересен этот случай, если он не вызван в стандарте как причина, по которой мы не можем иметь итератор мимо "одного конца". Я знаю, что int * действительно просто содержит целочисленное значение и как таковое подвержено переполнению.

В стандарте не обсуждаются причины для создания вещей undefined. У вас есть логика в обратном направлении: факт, что она undefined является причиной того, что реализация может помещать объект в место, где выполнение такой вещи в противном случае вызывает переполнение. Если требуется, чтобы итератор "два раза в конец" был действительным, тогда для реализации потребуется не помещать объект где-нибудь, что может привести к переполнению такой операции.

Ответ 4

Как @Random842 сказал так хорошо:

Стандарт не описывает типы указателей как находящиеся в плоском линейном пространстве с минимумом и максимумом, а все между действительностью, поскольку вы, кажется, считаете, что они

Указатели не предполагаются существовать в плоском линейном пространстве. Вместо этого существуют действительные указатели и недопустимые указатели. Определены некоторые операции с указателями, другие - поведение undefined.

Во многих современных системах указатели реализованы в плоском линейном пространстве. Даже в этих системах неопределенность формирования некоторых указателей может открыть ваш компилятор С++ для некоторых оптимизаций; например, int foo[5]; bool test(int* it1) { int* it2 = cend(foo); return it1 <= it2; } можно оптимизировать до true, так как нет указателей, которые могут быть справедливо по сравнению с it2, которые не меньше или равны ему.

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

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

Сегментированная память является наиболее известной. В старых x86-системах каждый указатель представляет собой пару 16-битных значений. Место, на которое они ссылаются в линейном 20-битовом адресном пространстве, составляет high << 4 + low или segment << 4 + offset.

Объекты живут внутри сегмента и имеют постоянное значение сегмента. Это означает, что все определенные сравнения с указателем < могут просто сравнить offset, низкие 16 бит. Они не должны делать эту математику (которая в то время была дорогой), они могут отбрасывать высокие 16 бит и сравнивать значения смещения при заказе.

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

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

Теперь ваша память не сегментирована, так что это не ваша проблема, не так ли? Компилятор может свободно интерпретировать ваше формирование ptr+2 вдоль определенной ветки кода, чтобы означать, что ptr не является указателем на последний элемент массива и соответственно оптимизируется. Если это не так, ваш код может вести себя непредсказуемыми способами.

И есть примеры реальных компиляторов в дикой природе, используя такие методы (предполагая, что код не использует поведение undefined, доказывая инварианты от него, используя выводы для изменения поведения до поведения undefined), если не тот частный случай. undefined поведение может путешествовать во времени, даже если базовая аппаратная реализация "не будет иметь проблем с ней" без каких-либо оптимизаций.