Почему здесь законно создавать ссылку 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, которое вы, возможно, ожидаете встретить во время выполнения, не имеет ничего общего с категориями значений. Возможно, вы ожидаете, что временное время жизни вектора закончится сразу после вычисления выражения диапазона, и, таким образом, переменная цикла является оборванной ссылкой в вектор, который был уничтожен к тому времени, когда тело цикла начнет выполняться.

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