Странное поведение undefined с сложным выражением лямбда

Я боролся с проблемой с лямбда-выражениями, которая ставила под угрозу мой проект. Я нашел решение, но я хотел бы точно понять, как и почему он работает, и если он надежный.

#include <iostream>
#include <functional>
#include <unordered_map>

typedef std::function<const int&(const int&)> Callback;

int f(int i, Callback callback) {
    if (i <= 2) return 1;
    return callback(i-1) + callback(i-2);
}

int main(void) {

    std::unordered_map<int, int> values;

    Callback callback = [&](const int& i) {
        if (values.find(i) == values.end()) {
            int v = f(i, callback);
            values.emplace(i, v);
        }
        return values.at(i);
    };

    std::cout << f(20, callback) << std::endl;

    return 0;

}

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

Если я скомпилирую код выше с помощью g++ -O0, и я запускаю программу, я получаю 6765, что на самом деле является 20-м числом Фибоначчи. Если я скомпилирован с -O1, -O2 или -O3, я получаю 262144, что является мусором.

Если я профилирую программу с помощью Valgrind (компиляция с -O0 -g), я получаю Conditional jump or move depends on uninitialised value(s) в строке std::cout << f(20, callback) << std::endl;, но трассировка стека не говорит ничего полезного.

Я не знаю, почему я закончил с этим:

Callback callback = [&](const int& i) -> const int& {

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

Можете ли вы помочь мне понять, что происходит?

Ответ 1

Без -> const int& возвращаемый тип лямбда int. Так как тип возврата Callback равен const int&, Callback заканчивается возвратом ссылки на временное значение, которое оно использует для хранения возвращаемого значения выражения лямбда. Undefined.

Для этого простого примера простое исправление заключается в том, чтобы вообще не использовать ссылки (Пример в Coliru), но я полагаю, что ваш реальный код обрабатывает более тяжеловесные объекты, чем int. Прискорбно, что std::function не указывается для предупреждения, когда вы назначаете функцию, которая возвращает ссылку без ссылки на std::function, тип возврата которой является ссылкой.

Ответ 2

Casey ответ. Я просто хотел бы что-то добавить. (Мне кажется, что добавление поверх Кейси хороших ответов становится обычным делом моего;-).) Собственно, мое замечание на gd1 комментарий Casey .

Я думаю, что OP использует g++ -std=c++1y, потому что в С++ 11 возвращаемый тип показанного там лямбда установлен в void. Действительно, компилятор выводит возвращаемый тип только в том случае, если тело лямбда содержит единственный оператор return. В противном случае компилятор предполагает, что этот тип void. В С++ 14 более эффективный автоматический вывод для возвращаемого типа (не только для лямбда, но и для функций).

Как можно видеть n3582 - документ, который был одобрен для С++ 14 и для которого реализация gcc ссылка - указывает, что

plain auto никогда не выводит ссылку, [...]

В примере OP возвращаемый тип int (не const int&). Я считаю, что можно использовать -> decltype(auto) для автоматического вывода, дающего ссылку.