Нечетное поведение возврата с помощью 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()
из приведенного выше примера совершенно безопасен для вызова, если вы не получаете доступ к цели оборванной ссылки.