Нечетное поведение возврата с помощью std:: function, созданной из lambda (С++)

У меня возникают проблемы с std:: функциями, созданными из lambdas, если функция возвращает ссылку, но тип возврата явно не вызывается как ссылка. Кажется, что std:: function создается отлично без предупреждений, но после ее вызова возвращается значение, когда ожидается ссылка, в результате чего вещи взорвутся. Здесь очень надуманный пример:

#include <iostream>
#include <vector>
#include <functional>

int main(){
   std::vector<int> v;
   v.push_back(123);
   std::function<const std::vector<int>&(const std::vector<int>&)> callback =
      [](const std::vector<int> &in){return in;};
   std::cout << callback(v).at(0) << std::endl;
   return 0;
}

Это выдает мусор, однако если лямбда изменена, чтобы явно возвращать ссылку на константу, она работает нормально. Я могу понять, что компилятор думает, что лямбда возвращается по значению без подсказки (когда я изначально столкнулся с этой проблемой, лямбда прямо возвращала результат из функции, которая возвращала ссылку на const, и в этом случае я бы подумал, что const reference return lambda будет выводимым, но, по-видимому, нет.) Я удивлен тем, что компилятор позволяет построить std:: function из лямбда с несоответствующими типами возврата. Ожидается ли такое поведение? Я что-то пропускаю в стандарте, который допускает такое несоответствие? Я вижу это с g++ (GCC) 4.8.2, не пробовал ни с чем другим.

Спасибо!

Ответы

Ответ 1

Почему это сломано?

Когда выводится тип возврата лямбда, отбрасываются эталонная и cv-квалификация. Таким образом, возвращаемый тип

[](const std::vector<int> &in){return in;};

просто std::vector<int>, а не std::vector<int> const&. В результате, если мы разделим лямбду и std::function на часть вашего кода, мы получим:

std::vector<int> lambda(std::vector<int> const& in)
{
    return in;
}

std::vector<int> const& callback(std::vector<int> const& in)
{
    return lambda(in);
}

lambda возвращает временный. Он фактически просто скопировал свой вклад. Это временное ограничение привязывает ссылочный возврат в callback. Но временные ограничения, связанные с ссылкой в ​​выражении return, не продлевают срок службы, поэтому временное уничтожается в конце оператора return. Таким образом, в этот момент:

callback(v).at(0)
-----------^

у нас есть оборванная ссылка на уничтоженную копию v.

Решение состоит в том, чтобы явно указать тип возвращаемого значения лямбда для ссылки:

 [](const std::vector<int> &in)-> const std::vector<int>& {return in;}
 [](const std::vector<int> &in)-> decltype(auto) {return in;} // C++14

Теперь нет копий, нет времен, ни оборванных ссылок, ни поведения undefined.

Кто виноват?

Что касается ожидаемого поведения, ответ на самом деле да. Условиями конструктивности a std::function являются [func.wrap.func.con]:

f является вызываемым (20.9.12.2) для типов аргументов ArgTypes... и возвращает тип R.

где, [func.wrap.func]:

Вызываемый объект f типа f доступен для типов аргументов ArgTypes и возвращает тип R, если выражение INVOKE (f, declval<ArgTypes>()..., R), рассматриваемый как неоцененный операнд (п. 5), хорошо (20.9.2).

где [func.require], мой удар:

Определите INVOKE(f, t1, t2, ..., tN, R) как static_cast<void>(INVOKE (f, t1, t2, ..., tN)), если R - cv void, иначе INVOKE(f, t1, t2, ..., tN) неявно преобразован в R.

Итак, если бы мы имели:

T func();
std::function<T const&()> wrapped(func);

Это действительно соответствует всем стандартным требованиям: INVOKE(func) является корректным и, пока он возвращает T, T неявно конвертируется в T const&. Так что это не ошибка gcc или clang. Вероятно, это стандартный дефект, так как я не понимаю, почему вы хотели бы позволить такую ​​конструкцию. Это никогда не будет действительным, поэтому формулировка, вероятно, потребует, чтобы, если R является ссылочным типом, то f должен также возвращать ссылочный тип.

Ответ 2

Я немного поработал над конструктором std::function. Кажется, эта часть является надзором во взаимодействии std::function и стандартной Callable concept. std::function<R(Args...)>::function<F>(F) требует F быть Callable как R(Args...), что само по себе представляется разумным. Callable для R(Args...) требует F типа возврата (если заданные аргументы типов Args... неявно конвертируются в R, что также само по себе представляется разумным. Теперь, когда R является const R_ &, это будет разрешить неявное преобразование R_ в const R_ &, потому что ссылки на const ссылаются на rvalues.Это не обязательно небезопасно. Например, рассмотрим функцию f(), которая возвращает int, но считается вызываемой как const int &().

const int &result = f();
if ( f == 5 )
{
   // ...
}

Здесь нет проблем из-за правил С++ для продления срока службы временного. Однако следующее поведение undefined:

std::function<const int &()> fWrapped{f};
if ( fWrapped() == 5 )
{
   // ...
}

Это связано с тем, что продление жизни не применяется здесь. Временное создается внутри std::function operator() и уничтожается перед сравнением.

Следовательно, конструктор std::function, вероятно, не должен полагаться только на Callable, но применять дополнительное ограничение, чтобы неявное преобразование значения r в значение const l для привязки к ссылке запрещено. Альтернативно, Callable можно было изменить, чтобы никогда не допускать этого преобразования, за счет отказа от некоторого безопасного использования (хотя бы из-за продления срока службы).

Чтобы сделать вещи еще более сложными, fWrapped() из приведенного выше примера совершенно безопасен для вызова, если вы не получаете доступ к цели оборванной ссылки.