Продлевается ли срок службы автоматической ссылки const внутри цикла for на основе диапазона до конца области видимости?

#c

Вопрос:

Я наткнулся на небольшой фрагмент кода, который очищает deque, находясь внутри цикла for на основе диапазона. Автоматическая ссылка const по — прежнему используется впоследствии. Вот небольшая репродукция.

 struct Foo
{
    int x;
}

deque<Foo> q1;
deque<Foo> q2;
q1.push_front({0});
q1.push_front({1});

for (const autoamp; ref : q1)
{
    if (ref.x == 1)
    {
        q1.clear();
        q2.push_front(ref);
        break;
    }
}
std::cout << q2.front().x << std::endl;
 

Этот q2, похоже, получил значение ref. Но я думаю, что q1.clear() они должны были удалить основные данные ref .

  1. Длится ли время жизни ссылки в области видимости, или это неопределенное поведение?
  2. Если это неопределенное поведение, почему это работает???

правка: пропущен отрыв от исходного кода, добавлен обратно, чтобы сузить обсуждение.

Комментарии:

1. неопределенное поведение не означает, что код не будет работать «правильно». Допустимые выходные данные являются частью набора всех возможных результатов. Вот почему UB так опасен. Код может работать на одной машине, но не на другой.

2. Содержимое if не выполняется. q1.front().x равно 0.

3. Ссылка не имеет срока службы, это делает объект, на который она ссылается. Когда срок службы этого объекта заканчивается, ссылка становится недействительной. Продление срока службы путем привязки к ссылке const применяется только к временным объектам.

Ответ №1:

Длится ли время жизни ссылки в области видимости, или это неопределенное поведение?

Для начала, следующий цикл на основе диапазона для:

 for (auto constamp; ref : q1) {
  // ...
}
 

расширяется в соответствии с [stmt.ranged]/1 до

 {
  auto amp;amp;range = (q1);
  for (auto begin_ = range.begin(), end_ = range.end(); begin_ != end_;
         begin_) {
    auto constamp; ref = *begin_;
    // ...
  }
}
 

Теперь, согласно документам для std::deque<T>::clear :

Удаляет все элементы из контейнера. После этого вызова функция size() возвращает ноль.

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

если if (ref.x == 1) ветвь когда-либо введена, то после точки q1.clear(); итератора, на который ссылается объект, на который указывает объект ref (здесь: элемент deque), становится недействительным, и любое дальнейшее использование итератора является неопределенным поведением: например, его увеличение и разыменование в следующей итерации цикла.

Теперь, в этом конкретном примере, поскольку объявление для диапазона auto constamp; сравнивается только auto amp; с тем , если *begin_ разыменование (до аннулирования итераторов) разрешается в значение, а не в ссылку, это (временное) значение будет привязано к ref ссылке и продлит срок ее действия. Однако std::deque является контейнером последовательности и должен соответствовать требованиям [sequence.reqmts], где, в частности, в таблице 78 указано:

/3 В таблицах 77 и 78, X обозначает класс контейнера последовательности, a обозначает значение типа X […]

Таблица 78:

  • Выражение: a.front()
  • Тип возвращаемого значения: reference; ( const_­reference для константы a )
  • Операционная семантика: *a.begin()

Хотя это и не совсем водонепроницаемо, представляется весьма вероятным, что разыменование std::deque итератора (например begin() , или end() ) приводит к ссылочному типу, с чем согласны как Clang, так и GCC:

 #include <deque>
#include <type_traits>

struct S{};

int main() {
    std::deque<S> d;
    static_assert(std::is_same_v<decltype(*d.begin()), Samp;>, "");
               // Accepted by both clang and gcc.   
}
 

в этом случае использование ref в

  q2.push_front(ref);
 

является UB после q1.clear() .

Мы также можем отметить, что end() итератор , сохраненный как end_ , также становится недействительным при вызове clear() , который является еще одним источником UB, как только инвариант расширенного цикла проверяется на следующей итерации.


Если это неопределенное поведение, почему это работает???

Пытаться рассуждать о неопределенном поведении-бесполезное занятие.

Комментарии:

1. Вероятно, немного неточно для этого вопроса: почему range вместо простого использования вводится новая переменная q1 ?

2. @mch q1 здесь может представлять выражение с побочными эффектами. Таким образом, вы хотите оценить это выражение только один раз.

3. @mch — У вас может быть произвольное выражение для диапазона. Даже тот, который создает временный объект. Его нужно продлить.

4. std::deque тип итератора это LegacyRandomAccessIterator, который является LegacyBidirectionalIterator , который является LegacyForwardIterator , который является LegacyIterator , где результат оценки *it «не указан» , поэтому он может возвращать временное, а затем должно применяться продление срока службы (если он возвращает ссылку, то UB).

5. @RichardCritten — LegacyBidirectionalIterator указывает *a-- как бытие std::iterator_traits<It>::reference . Что в данном случае является правильной ссылкой. Эти названные требования предназначены для «включения» друг друга. Многопроходные гарантии означают, что мы можем дополнительно уточнить, что *it делает.