Временный объект в диапазоне для

Я знаю, что в целом время жизни временного в цикле for на основе диапазона распространяется на весь цикл (я читал С++ 11: основанный на диапазоне для оператора: "время-init-время жизни"?). Поэтому делать такие вещи обычно хорошо:

for (auto &thingy : func_that_returns_eg_a_vector())
  std::cout << thingy;

Теперь я спотыкаюсь о проблемах с памятью, когда пытаюсь сделать то, что, как я думал, похоже на контейнер Qt QList:

#include <iostream>
#include <QList>

int main() {
  for (auto i : QList<int>{} << 1 << 2 << 3)
    std::cout << i << std::endl;
  return 0;
}

Проблема заключается в том, что valgrind показывает недопустимый доступ к памяти где-то внутри класса QList. Однако изменение примера, чтобы список сохранялся в переменной, дает правильный результат:

#include <iostream>
#include <QList>

int main() {
  auto things = QList<int>{} << 1 << 2 << 3;
  for (auto i : things)
    std::cout << i << std::endl;
  return 0;
}

Теперь мой вопрос: я делаю что-то немое в первом случае, в результате чего, например, undefined поведение (у меня недостаточно опыта чтения стандарта С++, чтобы ответить на это для себя)? Или это проблема с тем, как я использую QList или как QList реализовано?

Ответ 1

Поскольку вы используете С++ 11, вместо этого вы можете использовать список инициализации. Это пройдет valgrind:

int main() {
  for (auto i : QList<int>{1, 2, 3})
    std::cout << i << std::endl;
  return 0;
}

Проблема не полностью связана с диапазоном для или даже с С++ 11. Следующий код демонстрирует ту же проблему:

QList<int>& things = QList<int>() << 1;
things.end();

или

#include <iostream>

struct S {
    int* x;

    S() { x = NULL; }
    ~S() { delete x; }

    S& foo(int y) {
        x = new int(y);
        return *this;
    }
};

int main() {
    S& things = S().foo(2);
    std::cout << *things.x << std::endl;
    return 0;
}

Недопустимое чтение происходит потому, что временный объект из выражения S() (или QList<int>{}) разрушается после объявления (после С++ 03 и С++ 11 §12.2/5), поскольку у компилятора нет что метод foo() (или operator<<) вернет этот временный объект. Итак, теперь вы ссылаетесь на содержимое освобожденной памяти.

Ответ 2

Компилятор не может знать, что ссылка, которая является результатом трех вызовов operator <<, привязана к временному объекту QList<int>{}, поэтому срок жизни не продлевается. Компилятор не знает (и не может ожидать чего-либо знать) что-либо о возвращаемом значении функции, кроме ее типа. Если это ссылка, она не знает, к чему она может привязываться. Я уверен, что для того, чтобы правило продления жизни было применимо, привязка должна быть прямой.

Это должно работать, потому что список больше не является временным:

#include <iostream>
#include <QList>

int main() {
  auto things = QList<int>{};
  for (auto i : things << 1 << 2 << 3)
    std::cout << i << std::endl;
  return 0;
}

И это должно работать, потому что привязка является прямой, поэтому правило может применяться:

#include <iostream>
#include <QList>

int main() {
  for (auto i : QList<int>{1, 2, 3})
    std::cout << i << std::endl;
  return 0;
}