Почему здесь законно создавать ссылку lvalue на prvalue?
У меня есть код ниже:
#include <vector>
#include <iostream>
int main(){
for(int& v : std::vector<int>{1,3,5,10}) {
std::cout << v << std::endl;
v++; // Does this cause undefined behavior?
}
return 0;
}
Насколько я понимаю, вектор является prvalue и не может привязываться к int&
, но этот работает правильно?
Это потому, что для цикла диапазона это просто расширение макроса и временная переменная будет создана для вектора?
Ответы
Ответ 1
Цикл for
на основе диапазона, безусловно, не является расширением макросов. Это отдельная конструкция языка. Тем не менее, хотя сам вектор является prvalue, его функции-члены все еще работают нормально. Таким образом, его operator[]
(или разыменование его итератора) возвращает нормальную ссылку lvalue и т.д.
Конечно, такие ссылки действительны только в том случае, если существует сам вектор. Его срок службы длится для всего цикла for
на основе диапазона (который задан спецификацией цикла t20 > на стандартном уровне), поэтому все хорошо.
Что касается категорий значений, это то же самое, что и (а также legal):
int &i = std::vector<int>{1, 2, 3}[0];
Конечно, в отличие от цикла в цикле for
на основе диапазона эта ссылка i
становится незамеченной. Но принцип тот же.
Рассмотрим также это: язык не знает, что ссылка lvalue, возвращаемая итератором operator *
или вектором operator[]
, относится к тому, чье время жизни привязано к вектору. Он просто возвращает ссылку lvalue, поэтому он может быть связан.
Ответ 2
В соответствии со спецификацией ISO С++, для цикла, основанного на диапазоне, формально определяется как эквивалентный
{
auto && __range = range_expression ;
for (auto __begin = begin_expr, __end = end_expr;
__begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}
Обратите внимание, что "эквивалент" не означает, что "расширяется как макрос", а скорее "имеет то же самое поведение, что и". Эта эквивалентность не является макросом в традиционном смысле (т.е. Не a #define
), а скорее дается в спецификации ISO как формальный способ определения семантики того, что делает цикл, основанный на диапазоне. Реализация цикла, основанного на диапазоне, может быть любым, что нравится компилятору, если поведение точно совпадает с поведением, приведенным выше.
В этом контексте range_expression
является prvalue, но затем привязывается к __range
, который является lvalue. Итераторы, получившие сканирование по __range
, также являются lvalues, и поэтому, когда итераторы разыменованы, чтобы получить отдельные значения в диапазоне, эти значения будут lvalues.
Надеюсь, это поможет!
Ответ 3
Цикл for-loop, основанный на диапазоне, не является расширением макросов, а истинным языком. Это продлевает срок жизни временного для всего тела.
Ответ 4
Вектор может быть prvalue, но int
внутри - lvalues.
вектор является prvalue и не может связываться с int &
Вектор не может привязываться к int&
, независимо от его категории значений. int
внутри вектора - lvalues, хотя и может связываться с int&
.
Это потому, что цикл диапазона - это просто расширение макроса...
Диапазон для не является расширением макроса. Это построение языка первого класса.
Ответ 5
Насколько я понимаю, вектор является prvalue и не может связываться с int &, но этот работает правильно?
Ошибка, которую, я полагаю, вы указали, заключается в том, что вы присваиваете категории значений объектам, а не выражениям; Вы считаете, что поскольку векторный объект является prvalue, то и его элементы, и, следовательно, int &v
не может быть привязан к этим prvalues.
Фактически категории значений являются атрибутом выражений, а не объектами: в этом смысле даже не имеет смысла рассуждать о том, что элементы диапазона имеют категорию значений и определяют, разрешено ли связывать переменную int &v
к этим объектам.
Таким образом, хотя истинно, что выражение, создающее вектор, является prvalue, созданный вектор является просто обычным объектом, а его элементы являются просто регулярными объектами, все они абсолютно правильны для ссылок из нестандартной ссылочной типизированной переменной.
Вместо этого интуитивное объяснение цикла, основанного на диапазоне, дает правильный ответ: переменная инициализируется каждым элементом диапазона по очереди, и он просто работает.
Кроме того, во время компиляции проверяются категории значений. Если код был фактически незаконным по той причине, что вы даете, вы получите ошибку компиляции, такую как:
error: non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
int &v = 10;
^ ~~
Поведение undefined, которое вы, возможно, ожидаете встретить во время выполнения, не имеет ничего общего с категориями значений. Возможно, вы ожидаете, что временное время жизни вектора закончится сразу после вычисления выражения диапазона, и, таким образом, переменная цикла является оборванной ссылкой в вектор, который был уничтожен к тому времени, когда тело цикла начнет выполняться.
Фактически временный вектор имеет время жизни, расширенное на всю продолжительность цикла, поэтому нет проблем с оборванными ссылками на эту учетную запись.