Range-based для цикла на временном диапазоне [дубликат]

этот вопрос уже есть ответ здесь:

благодаря некоторым ошибкам сегментации и предупреждениям в 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 ответов


обратите внимание, что используя временную как выражение непосредственно в порядке, его lefetime будет продлен. Но для f()[5], что f() returns является временным, и он построен внутри выражения, и он будет уничтожен после всего выражения, где он построен.

из C++20 вы можете использовать init-оператор для диапазон на основе цикла для решения таких проблем.

(выделено мной)

если range_expression возвращает временно, своя продолжительность жизни расширена до конца цикла, как указано привязкой к rvalue ссылка __диапазон, но помните, что срок службы любого временного в пределах 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 to f(), которые is привязка ссылки к временному, так что временный срок службы будет продлен до срока службы ссылки, который будет полным 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
}