Каковы правила преобразования типов для параметров и возвращаемые значения lambdas?

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

Смотрите этот пример (ideone), где я выделил то, что, как я подозреваю, несовместимо. Я бы подумал, что возвращаемое значение не является проблемой, поскольку вы всегда можете вызвать функцию и игнорировать возвращаемое значение, но преобразование из ссылки на значение выглядит странно для меня:

int main() {
    function<void(const int& i)> f;
    //       ^^^^ ^^^^^    ^
    f = [](int i) -> int { cout<<i<<endl; return i; };
    //     ^^^    ^^^^^^
    f(2);
    return 0;
}

Незначительный вопрос: зачем этот код компилируется и работает? Главный вопрос: каковы общие правила преобразования типов лямбда-параметров и возвращаемых значений при использовании вместе с std::function?

Ответ 1

Вы можете назначить лямбда объекту функции типа std::function<R(ArgTypes...)>, когда лямбда Lvalue-Callable для этой подписи. В свою очередь, Lvalue-Callable определяется в терминах операции INVOKE, что означает, что лямбда должна быть вызвана, когда она является lvalue и все его аргументы - значения требуемых типов и категорий значений (как будто каждый аргумент был результатом вызова нулевой функции с этим типом аргумента в качестве возвращаемого типа в своей сигнатуре).

То есть, если вы дадите свой лямбда id, чтобы мы могли ссылаться на его тип,

auto l = [](int i) -> int { cout<<i<<endl; return i; };

Чтобы присвоить его function<void(const int&)>, выражение

static_cast<void>(std::declval<decltype(l)&>()(std::declval<const int&>()))

должен быть хорошо сформирован.

Результат std::declval<const int&>() является ссылкой lvalue на const int, но нет проблемы с привязкой к аргументу int, так как это просто преобразование lvalue-to-r, которое считается точным соответствием для целей разрешения перегрузки:

return l(static_cast<int const&>(int{}));

Как вы заметили, возвращаемые значения отбрасываются если подпись объекта функции имеет тип возврата void; в противном случае типы возврата должны быть неявно конвертируемыми. Как отмечает Джонатан Вакели, С++ 11 имел неудовлетворительное поведение в этом отношении (Использование` std:: function < void (...) > `для вызова непустой функции; Неправомерно ли вызывать функцию std:: function < void (Args...) > по стандарту?), но она была исправлена, поскольку в LWG 2420; эта резолюция была применена как исправление после публикации для С++ 14. Большинство современных компиляторов С++ обеспечивают поведение С++ 14 (с поправками) как расширение, даже в режиме С++ 11.

Ответ 2

Определяется действие оператора присваивания:

function(std::forward<F>(f)).swap(*this);

(14882: 2011 20.8.11.2.1, пункт 18)

Конструктор, который он ссылается,

template <class F> function(F f);

требуется:

F должно быть CopyConstructible. F должен быть Callable (20.8.11.2) для типа аргумента ArgTypes и возвращаемого типа R.

где Callable определяется следующим образом:

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

INVOKE, в свою очередь, определяется как:

  • Определите INVOKE (f, t1, t2,..., tN) следующим образом:

    • ... случаи для обработки функций членов, опущенных здесь...
    • f(t1, t2, ..., tN) во всех остальных случаях.
  • Определите INVOKE(f, t1, t2, ..., tN, R) как static_cast<void>(INVOKE(f, t1, t2, ..., tN)), если R - cv void, иначе INVOKE(f, t1, t2, ..., tN) неявно преобразован в R.

Так как определение INVOKE становится простым вызовом функции, в этом случае аргументы могут быть преобразованы: Если ваш std::function<void(const int&)> принимает const int&, тогда он может быть преобразован в int для вызова. Пример ниже компилируется с помощью clang++ -std=c++14 -stdlib=libc++ -Wall -Wconversion:

int main() {
    std::function<void(const int& i)> f;
    f = [](int i) -> void { std::cout << i << std::endl; };
    f(2);
    return 0;
}

Тип возврата (void) обрабатывается специальным случаем static_cast<void> для определения INVOKE.

Обратите внимание, однако, что на момент написания следующего генерируется ошибка при компиляции с clang++ -std=c++1z -stdlib=libc++ -Wconversion -Wall, но не при компиляции с clang++ -std=c++1z -stdlib=libstdc++ -Wconversion -Wall:

int main() {
    std::function<void(const int& i)> f;
    f = [](int i) -> int { std::cout << i << std::endl; return i;};
    f(2);
    return 0;
}

Это связано с тем, что libc++ реализует поведение, указанное в С++ 14, вместо измененного поведения, описанного выше (спасибо на @Jonathan Wakely за указание на это). @Arunmu описывает черту типа libstdc++, ответственную за ту же самую вещь в своем сообщении. В этом отношении реализации могут вести себя несколько иначе при обработке вызовов с типами возвращаемых типов void в зависимости от того, реализуют ли они С++ 11, 14 или что-то более новое.

Ответ 3

преобразование из ссылки на значение выглядит странно для меня

Почему?

Это тоже выглядит странно?

int foo(int i) { return i; }

void bar(const int& ir) { foo(ir); }

Это точно то же самое. Функция, принимающая значение int по значению, вызывается другой функцией, принимая int по const-ссылке.

Внутри bar переменная ir копируется, а возвращаемое значение игнорируется. Именно это происходит внутри std::function<void(const int&)>, когда у него есть цель с подписью int(int).

Ответ 4

Просто добавив к ответу @ecatmur, g++/libstd ++ просто решил игнорировать возвращаемое значение callable, это так же нормально, как можно было бы сделать, чтобы игнорировать возвращаемое значение в регулярном коде:

static void
_M_invoke(const _Any_data& __functor, _ArgTypes&&... __args)
{
   (*_Base::_M_get_pointer(__functor))(  // Gets the pointer to the callable
           std::forward<_ArgTypes>(__args)...);
}   

Типом, который явно разрешает это в libstd ++, является:

template<typename _From, typename _To>     
using __check_func_return_type = __or_<is_void<_To>, is_convertible<_From, _To>>;