Почему С++ не поддерживает диапазон, основанный на цикле для динамических массивов?
Почему С++ не поддерживает диапазон, основанный на цикле над динамическими массивами? То есть, что-то вроде этого:
int* array = new int[len];
for[] (int i : array) {};
Я только что придумал инструкцию for[]
, чтобы рифмовать с помощью new[]
и delete[]
. Насколько я понимаю, среда выполнения имеет размер доступного массива (иначе delete[]
не может работать), поэтому теоретически диапазон, основанный на цикле, также можно заставить работать. В чем причина, по которой он не работал?
Ответы
Ответ 1
int* array = new int[len];
for[] (int i : array) {}
Существует несколько моментов, которые необходимо решить; Я буду решать их по очереди.
Знает ли время выполнения размер массива?
В определенных условиях он должен. Как вы указали, вызов delete[]
вызовет деструктор каждого элемента (в резервном порядке) и, следовательно, должен знать, сколько его есть.
Однако, не указывая, что количество элементов должно быть известно и доступно, стандарт С++ позволяет реализации опускать его всякий раз, когда вызов деструктора не требуется (std::is_trivially_destructible<T>::value
оценивается как true
).
Может ли время выполнения различать указатель и массив?
В общем, нет.
Когда у вас есть указатель, он может указывать на что-либо:
- отдельный элемент или элемент в массиве
- первый элемент в массиве или любой другой
- массив в стеке или массив в куче,
- просто массив или часть массива более крупного объекта.
Именно по этой причине delete[]
существует, и использование delete
здесь было бы неверным. С помощью delete[]
вы указываете состояние пользователя: этот указатель указывает на первый элемент массива, выделенного кучей.
Затем реализация может предположить, что, например, в 8 байтах, предшествующих этому первому элементу, он может найти размер массива. Без вас, гарантируя это, эти 8 байтов могут быть чем угодно.
Тогда почему бы не пройти весь путь и не создать for[] (int i : array)
?
Есть две причины:
- Как уже упоминалось, сегодня реализация может превышать размер по ряду элементов; с этим новым синтаксисом
for[]
, это было бы невозможно для каждого типа.
- Это не стоит.
Давайте будем честными, new[]
и delete[]
являются реликтами более старого времени. Они невероятно неудобны:
- количество элементов должно быть известно заранее и не может быть изменено,
- элементы должны быть по умолчанию конструктивными или иначе C-ish,
и небезопасно использовать:
- количество элементов недоступно для пользователя.
Как правило, нет смысла использовать new[]
и delete[]
в современном С++. В большинстве случаев предпочтительнее a std::vector
; в немногих случаях, когда емкость избыточна, a std::dynarray
все еще лучше (потому что он отслеживает размер).
Поэтому, без уважительной причины продолжать использовать эти утверждения, нет мотивации включать новые семантические конструкции, специально предназначенные для их обработки.
И должен ли кто-нибудь быть достаточно мотивированным, чтобы сделать такое предложение:
- торможение текущей оптимизации, нарушение философии С++ "Вы не платите за то, что вы не используете", вероятно, будет против них,
- включение нового синтаксиса, когда современные предложения на С++ пошли на большие расстояния, чтобы как можно больше избежать его (с точки зрения библиотеки, определенной
std::variant
), также могут быть против них.
Я рекомендую вам просто использовать std::vector
.
Ответ 2
В чем причина, по которой он не работал?
Цикл, основанный на диапазоне, например
for(auto a : y) {
// ...
}
является просто синтаксическим сахаром для следующего выражения
auto endit = std::end(y);
for(auto it = std::begin(y); it != endit; ++it) {
auto a = *it;
// ...
}
Так как std::begin()
и std::end()
не могут использоваться с простым указателем, это не может быть применено с указателем, выделенным с помощью new[]
.
Насколько я понимаю, среда выполнения имеет размер доступного массива (иначе delete[]
не может работать)
Как delete[]
отслеживает блок памяти, который был выделен new[]
(который не обязательно имеет тот же размер, который был указан пользователем), это совершенно другая вещь, и компилятор, скорее всего, не даже знаю, как именно это реализовано.
Ответ 3
Если у вас есть это:
int* array = new int[len];
Проблема здесь в том, что ваша переменная с именем array
не является массивом. Это указатель. Это означает, что он содержит только адрес одного объекта (в этом случае первый элемент массива создается с помощью new
).
Для диапазона, основанного на работе, для компилятора нужны два адреса, начало и конец массива.
Итак, проблема заключается в том, что компилятор не имеет достаточной информации для этого:
// array is only a pointer and does not have enough information
for(int i : array)
{
}
Ответ 4
Это не связано с динамическими массивами, оно более общее. Конечно, для динамических массивов существует где-то размер, чтобы иметь возможность вызвать деструкторы (но помните, что стандарт ничего не говорит об этом, просто вызов delete []
работает по назначению).
Проблема заключается в указателях в целом, так как задан указатель, который вы не можете сказать, соответствует ли он любому типу... что?
Массивы распадаются на указатели, но дают указатель, что вы можете сказать?
Ответ 5
array
не является массивом, а указателем и нет информации о размере для "массива". Таким образом, компилятор не может выводить begin
и end
этого массива.
См. синтаксис диапазон для цикла:
{
auto && __range = range_expression ;
for (auto __begin = begin_expr, __end = end_expr;
__begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}
range_expression - любое выражение, которое представляет подходящую последовательность (либо массив, либо объект, для которого функции начала и конца члена или бесплатные функции, см. ниже) или список с расширенными командами.
auto
работает во время компиляции. Таким образом, begin_expr
и end_expr
не вычитают время выполнения.
Ответ 6
Причина в том, что, учитывая только значение указателя array
, компилятор (и ваш код) не имеет информации о том, на что он указывает. Известно только, что array
имеет значение, которое является адресом одного int
.
Он может указывать на первый элемент статически выделенного массива. Он может указывать на элемент в середине динамически распределенного массива. Он может указывать на элемент структуры данных. Он может указывать на элемент массива, который находится внутри структуры данных. Список продолжается.
Ваш код сделает ПРЕДПОЛОЖЕНИЯ о том, на что указывает указатель. Он может считать, что это массив из 50 элементов. Ваш код может получить доступ к значению len
и принять array
точки в (первом элементе) массива элементов len
. Если ваш код правильно, все работает по назначению. Если ваш код ошибочно (например, обращается к 50-му элементу массива с 5 элементами), то поведение просто undefined. Это undefined, потому что возможности бесконечны - ведение бухгалтерского учета для отслеживания того, на что указывает произвольный указатель, НАСТОЯЩИМ указывает (помимо информации, что есть int
на этом адресе) будет огромным.
Вы начинаете с ПРЕДПОЛОЖЕНИЯ, что array
указывает на результат от new int[len]
. Но эта информация не сохраняется в значении array
, поэтому компилятор не имеет возможности вернуться к значению len
. Это будет необходимо для вашего подхода, основанного на диапазоне.
Пока да, данный array = new int[len]
, механизм, вызываемый delete [] array
, будет работать, чтобы array
имел элементы len
и освободил их. Но delete [] array
также имеет поведение undefined, если array
получается из чего-то другого, кроме выражения new []
. Даже
int *array = new int;
delete [] array;
дает поведение undefined. "Время выполнения" не требуется для разработки, в данном случае, что array
на самом деле является адресом одного динамически распределенного int
(а не фактического массива). Таким образом, это не требуется, чтобы справиться с этим.