Нечетное поведение возврата с std :: function, созданным из лямбда (C ++)

У меня проблемы с std :: functions, созданными из лямбда-выражений, если функция возвращает ссылку, но тип возвращаемого значения не вызывается явно как ссылка. Кажется, что 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;
}

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

Спасибо!


person Kevin    schedule 30.09.2015    source источник
comment
@Nawaz Почему вы удалили свой ответ?   -  person Barry    schedule 30.09.2015
comment
Clang ++ 3.7.0 также печатает мусор (для меня g ++ приводит к ошибке сегментации).   -  person jhnnslschnr    schedule 30.09.2015
comment
@jhnnslschnr Спасибо за проверку - не знаю, почему опубликованный ответ был удален (он казался правильным), но суть заключалась в том, что лямбда, возвращающая значение, успешно привязывается к std :: function со ссылочным типом возвращаемого значения, конечный результат заключается в том, что лямбда возвращает копию, а функция возвращает ссылку на временную копию. Я полагаю, что это ничем не отличается от возврата локальной переменной из функции со ссылочным типом возвращаемого значения.   -  person Kevin    schedule 30.09.2015


Ответы (3)


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

Когда выводится тип возвращаемого значения лямбда, ссылка и 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

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

Кто виноват?

Что касается ожидаемого поведения, ответ на самом деле положительный. Условия конструктивности 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 также должен возвращать ссылочный тип.

person Barry    schedule 30.09.2015
comment
В C ++ 14 это может быть -> decltype(auto). - person 0x499602D2; 30.09.2015
comment
Верно по поводу вывода типа, но вы не ответили на вторую половину вопроса. Упрощенное: почему function<const T &()> разрешено связываться с лямбдой, возвращающей T по значению? Это будет всегда создавать висящую ссылку. Я бы сказал, что это либо ошибка библиотеки, либо недосмотр в стандарте, поскольку этот случай можно проверить во время компиляции с помощью decltype. - person Arne Vogel; 01.10.2015
comment
@ArneVogel Стандартный дефект. gcc / clang идеально подходят для такой конструкции. Добавил формулировку. - person Barry; 01.10.2015
comment
Мы просто столкнулись с этой же проблемой напрямую с std :: function, поместив функцию, возвращающую значение, в std :: function, возвращающую const-ref. MSVC 2015 диагностирует это правильно, MSVC 2013 выбирает его только для указателей на функции, а не для объектов, созданных std :: bind, и ни gcc, ни clang на самом деле этого не замечают. Это качество реализации или стандартный дефект, который необходимо устранить? - person TBBle; 26.10.2015

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

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

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

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

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

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

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

person Arne Vogel    schedule 01.10.2015

Если вы используете:

return std::ref(in);

В вашей лямбде это будет работать.

Это сделает возвращаемый тип вашей лямбды std::reference_wrapper<std::vector<int>>, который неявно преобразуется в std::vector<int>&.

person user2445507    schedule 19.05.2018