Диапазон на основе цикла для временного диапазона
Благодаря некоторым ошибкам сегментации и предупреждениям в valgrind я обнаружил, что этот код неверен и имеет какую-то висячую ссылку в цикле for-range.
#include<numeric>
#include<vector>
auto f(){
std::vector<std::vector<double>> v(10, std::vector<double>(3));
iota(v[5].begin(), v[5].end(), 0);
return v;
}
int main(){
for(auto e : f()[5])
std::cout << e << std::endl;
return 0;
}
Похоже, что begin
а также end
взято из временного и потеряно в цикле.
Конечно, способ сделать это
auto r = f()[5];
for(auto e : r)
std::cout << e << std::endl;
Однако мне интересно, почему именно for(auto e : f()[5])
является ошибкой, а также, если есть лучший способ или какой-то способ спроектировать f
или даже контейнер ( std::vector
) чтобы избежать этой ловушки.
С циклами итераторов более очевидно, почему эта проблема происходит (begin
а также end
приходят из разных временных объектов)
for(auto it = f()[5].begin(); it != f()[5].end(); ++it)
Но в цикле for-range, как в первом примере, кажется, что эту ошибку легко сделать.
2 ответа
Обратите внимание, что использование временного выражения в качестве прямого выражения допустимо, его время будет увеличено. Но для f()[5]
, какие f()
return является временным, и он создается внутри выражения, и он будет уничтожен после всего выражения, в котором он создан.
Начиная с C++20, вы можете использовать оператор init для цикла for на основе диапазона для решения таких проблем.
(акцент мой)
Если range_expression возвращает временное значение, его время жизни увеличивается до конца цикла, на что указывает привязка к rvalue-ссылке __range, но следует помнить, что время жизни любого временного объекта в range_expression не увеличивается.
Эта проблема может быть решена с помощью инструкции init:
for (auto& x : foo().items()) { /* .. */ } // undefined behavior if foo() returns by value for (T thing = foo(); auto& x : thing.items()) { /* ... */ } // OK
например
for(auto thing = f(); auto e : thing[5])
std::cout << e << std::endl;
Интересно, почему именно
for(auto e : f()[5])
это ошибка
Я просто отвечу на эту часть. Причина в том, что операторы, основанные на диапазоне, являются просто синтаксическим сахаром, примерно для:
{
auto&& __range = f()[5]; // (*)
auto __begin = __range.begin(); // not exactly, but close enough
auto __end = __range.end(); // in C++17, these types can be different
for (; __begin != __end; ++__begin) {
auto e = *__begin;
// rest of body
}
}
Посмотрите на эту первую строку. Что просходит? operator[]
на vector
возвращает ссылку на этот объект, так __range
связан с этой внутренней ссылкой. Но затем временный выходит из области видимости в конце строки, разрушая все его внутренние и __range
это сразу свисающая ссылка. Здесь нет продления жизни, мы никогда не привязываем ссылку к временному.
В более нормальном случае, for(auto e : f())
мы бы связали __range
в f()
непосредственно, что связывает ссылку с временным, так что временное время будет увеличено до времени жизни ссылки, которое будет полным for
заявление.
Чтобы добавить больше морщин, существуют другие случаи, когда косвенное связывание, как это, все равно будет продлевать срок службы. Мол, скажи:
struct X {
std::vector<int> v;
};
X foo();
for (auto e : foo().v) {
// ok!
}
Но вместо того, чтобы пытаться отследить все эти маленькие случаи, гораздо лучше, как предполагает songyuanyao, использовать новую инструкцию for с инициализатором... все время:
for (auto&& range = f(); auto e : range[5]) {
// rest of body
}
Хотя в некотором смысле это дает ложное чувство безопасности, поскольку, если бы вы сделали это дважды, у вас все равно была бы та же проблема...
for (auto&& range = f().g(); auto e : range[5]) {
// still dangling reference
}